是時候自己來造個輪子了--增強型RecyclerView

簡述

該增強型RecyclerView鸯绿,增加了以下特性:

  • 上拉滑動到底部,加載更多
  • 支持添加Header頭視圖
  • 支持加載數據為空時,顯示特定視圖
  • 支持拖拽漠嵌,側滑刪除

下拉刷新實現通過給RecyclerView包一層SwipRefreshLayout來實現敬矩。

本文重點分享上拉加載更多的實現概行,同時實現添加頭部視圖,側滑弧岳,拖拽功能實現占锯,該實現存在以下注意點:

  1. 如何判斷RecyclerView滑動到了底部
  2. 通常RecyclerView顯示的item布局相同,怎么做到上拉加載更多時出現一個底欄視圖
  3. 滑動到底欄出現加載更多動畫缩筛,這個動畫什么時候結束消略?動畫生命周期是?
  4. ReclcerViewyou多種布局瞎抛,如果是網格布局(有多列)艺演,怎么做到讓上拉加載更多的動畫視圖和頭部視圖占用一整行?
  5. 自定義了RecyclerView桐臊,如何做到像使用標準RecyclerView那樣使用胎撤?
  6. 如何實現item的拖拽和側滑刪除?

實現的注意點解析

如何判斷RecyclerView滑動到了底部

關于布局的邏輯設置断凶,就找LayoutManager伤提。的確,通過查閱官方API手冊认烁,有findLastVisibleItemPosition()/findLastCompletelyVisibleItePosition()肿男,在滑動監(jiān)聽里,使用這兩個方法就能實現判斷

怎么做到上拉加載更多時出現一個底欄視圖

這里涉及到RecyclerView如何實現多布局顯示的知識却嗡,通過getItemViewType()舶沛,底部上拉加載更多視圖設定一種ItemViewType值,對應新建一個ViewHolder.

滑動到底欄出現加載更多動畫窗价,這個動畫什么時候結束如庭?動畫生命周期是?

動畫的開始時刻是列表滑倒底部撼港,當滑倒底部時坪它,在客戶類(Fragment/Activity)里接口回調骤竹,開始網絡請求數據,動畫的結束時刻是網絡加載完成往毡,刷新列表時

ReclcerViewyou多種布局瘤载,如果是網格布局(有多列),怎么做到讓上拉加載更多的動畫視圖和頭部視圖占用一整行卖擅?

和布局相關的鸣奔,找LayoutManager,這里要找GridLayoutManager惩阶,它提供了setSpanSizeLookup(GridLayoutManager.SpanSizeLookup)挎狸,通過這個方法,可以根據位置來設置item占用一整行還是正常顯示

自定義了RecyclerView断楷,如何做到像使用標準RecyclerView那樣使用锨匆?

使用裝飾器設計模式,能很好的實現對用戶透明使用效果

如何實現item的拖拽和側滑刪除冬筒?

使用android提供的ItemTouchHelper工具類恐锣,能快速的實現

核心代碼

EnhanceRecyclerView

public class EnhanceRecyclerView extends RecyclerView {

private static final String TAG = "EnhanceRecyclerView";

private OnLoadMoreListener mOnLoadMoreListener;
private InternalAdapter mInternalAdapter;
private View mEmptyView;
private @LayoutRes int mHeaderResId;
private AdapterDataObserver mAdapterDataObserver = new EnhanceAdapterDataObserver();
/**
 * 滾動方向
 */
private int mScrollDy = 0;

public EnhanceRecyclerView(Context context) {
    super(context);
}

public EnhanceRecyclerView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
}

public EnhanceRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
}


@Override
public void onScrolled(int dx, int dy) {
    super.onScrolled(dx, dy);
    mScrollDy = dy;
}

@Override
public void onScrollStateChanged(int state) {
    super.onScrollStateChanged(state);
    switch (state) {
        case SCROLL_STATE_IDLE:
            LayoutManager layoutManager = getLayoutManager();
            int itemCount = getAdapter().getItemCount();
            int lastVisibleItemPosition = 0;

            if (layoutManager instanceof GridLayoutManager) {
                GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
                lastVisibleItemPosition = gridLayoutManager.findLastVisibleItemPosition();
            } else if (layoutManager instanceof LinearLayoutManager) {
                LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
                lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
            }

            if (lastVisibleItemPosition >= itemCount - 1) {
                if (getParent() instanceof SwipeRefreshLayout) {
                    SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) getParent();
                    if (swipeRefreshLayout.isRefreshing()) {
                        break;
                    }
                }
                if (mOnLoadMoreListener != null && mScrollDy > 0) {
                    mInternalAdapter.setLoadingIndicatorViewVisible(VISIBLE);
                    mOnLoadMoreListener.onLoadMore();
                }
            }
            break;
    }
}


/**
 * 重寫此方法,設置GridLayout的上拉加載更多視圖的位置
 *
 * @param layout
 */
@Override
public void setLayoutManager(LayoutManager layout) {
    if (layout instanceof GridLayoutManager) {
        final GridLayoutManager externalGridLayoutManager = (GridLayoutManager) layout;
        final int spanCount = externalGridLayoutManager.getSpanCount();
        int orientation = externalGridLayoutManager.getOrientation();

        final GridLayoutManager innerGridLayoutManager = new GridLayoutManager(getContext(), spanCount, orientation, false);
        innerGridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                int headerViewCount = mInternalAdapter.getHeaderViewCount();
                int footViewCount = mInternalAdapter.getFootViewCount();
                if (position < headerViewCount) {
                    return spanCount;
                }

                int totalItemCount = innerGridLayoutManager.getItemCount();
                if (position >= totalItemCount - footViewCount) {
                    return spanCount;
                }

                return externalGridLayoutManager.getSpanSizeLookup().getSpanSize(position - headerViewCount);
            }
        });
        super.setLayoutManager(innerGridLayoutManager);
    } else {
        super.setLayoutManager(layout);
    }
}

public View getEmptyView() {
    return mEmptyView;
}

public final void setEmptyView(View emptyView) {
    mEmptyView = emptyView;
    setupEmptyViewHierarchy(emptyView);
}

protected void setupEmptyViewHierarchy(View emptyView) {
    ((ViewGroup) getParent().getParent()).addView(emptyView,0);
}

public void addHeaderResId(@LayoutRes int resId) {
    mHeaderResId = resId;
    if (mInternalAdapter != null) {
        mInternalAdapter.setExternalHeaderResId(resId);
    }
}



@Override
public void setAdapter(Adapter adapter) {
    mInternalAdapter = new InternalAdapter(adapter);
    super.setAdapter(mInternalAdapter);
    //addHeaderView方法依賴于setAdapter方法
    if (mHeaderResId > 0) {
        addHeaderResId(mHeaderResId);
    }
    mInternalAdapter.registerAdapterDataObserver(mAdapterDataObserver);
    mAdapterDataObserver.onChanged();
}

public void setOnLoadMoreListener(OnLoadMoreListener onLoadMoreListener) {
    mOnLoadMoreListener = onLoadMoreListener;
}


public void loadMoreOnSuccess() {
    if (mInternalAdapter != null) {
        mInternalAdapter.loadMoreOnSuccess();
    }
}

public void loadMoreOnError() {
    if (mInternalAdapter != null) {
        mInternalAdapter.loadMoreOnError();
    }
}

public void loadMoreOnComplete() {
    if (mInternalAdapter != null) {
        mInternalAdapter.loadMoreOnComplete();
    }
}


public final void notifyDataSetChanged() {
    mInternalAdapter.notifyDataSetChanged();
}

public final void notifyItemChanged(int position) {
    mInternalAdapter.notifyItemChanged(position);
}

public final void notifyItemChanged(int position, Object payload) {
    position = position + mInternalAdapter.getHeaderViewCount();
    mInternalAdapter.notifyItemChanged(position, payload);
}

public final void notifyItemRangeChanged(int positionStart, int itemCount) {
    positionStart = positionStart + mInternalAdapter.getHeaderViewCount();
    mInternalAdapter.notifyItemRangeChanged(positionStart, itemCount);
}

public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
    positionStart = positionStart + mInternalAdapter.getHeaderViewCount();
    mInternalAdapter.notifyItemRangeChanged(positionStart, itemCount, payload);
}

public final void notifyItemInserted(int position) {
    position = position + mInternalAdapter.getHeaderViewCount();
    mInternalAdapter.notifyItemInserted(position);
}

public final void notifyItemMoved(int fromPosition, int toPosition) {
    fromPosition = fromPosition + mInternalAdapter.getHeaderViewCount();
    toPosition = toPosition + mInternalAdapter.getHeaderViewCount();
    mInternalAdapter.notifyItemMoved(fromPosition, toPosition);
}

public final void notifyItemRangeInserted(int positionStart, int itemCount) {
    positionStart = positionStart + mInternalAdapter.getHeaderViewCount();
    mInternalAdapter.notifyItemRangeInserted(positionStart, itemCount);
}

public final void notifyItemRemoved(int position) {
    position = position + mInternalAdapter.getHeaderViewCount();
    mInternalAdapter.notifyItemRemoved(position);
}

public final void notifyItemRangeRemoved(int positionStart, int itemCount) {
    positionStart = positionStart + mInternalAdapter.getHeaderViewCount();
    mInternalAdapter.notifyItemRangeRemoved(positionStart, itemCount);
}


public InternalAdapter getInternalAdapter() {
    return mInternalAdapter;
}

/**
 * 上拉加載更多回調
 */
public interface OnLoadMoreListener {
    void onLoadMore();
}

private class EnhanceAdapterDataObserver extends AdapterDataObserver {

    @Override
    public void onChanged() {
        super.onChanged();
        if (getEmptyView() != null && getAdapter() != null) {
            int itemCount = getAdapter().getItemCount();
            if (itemCount == 0) {
                getEmptyView().setVisibility(VISIBLE);
                setVisibility(GONE);
            } else {
                getEmptyView().setVisibility(GONE);
                setVisibility(VISIBLE);
            }
        }
    }

    @Override
    public void onItemRangeChanged(int positionStart, int itemCount) {
        super.onItemRangeChanged(positionStart, itemCount);
        onChanged();
    }

    @Override
    public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
        super.onItemRangeChanged(positionStart, itemCount, payload);
        onChanged();
    }

    @Override
    public void onItemRangeInserted(int positionStart, int itemCount) {
        super.onItemRangeInserted(positionStart, itemCount);
        onChanged();
    }

    @Override
    public void onItemRangeRemoved(int positionStart, int itemCount) {
        super.onItemRangeRemoved(positionStart, itemCount);
        onChanged();
    }

    @Override
    public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
        super.onItemRangeMoved(fromPosition, toPosition, itemCount);
        onChanged();
    }
}
}

InternalAdapter

public class InternalAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

private static final String TAG = "InternalAdapter";

private static final int HEADER_ITEM_TYPE = 170118;
private static final int FOOTER_ITEM_TYPE = 170116;

private RecyclerView.Adapter<RecyclerView.ViewHolder> mExternalAdapter;
private int mBodyItemCount;
private FooterView mFooterView;
private @LayoutRes int mExternalHeaderResId;


public InternalAdapter(RecyclerView.Adapter<RecyclerView.ViewHolder> externalAdapter) {
    mExternalAdapter = externalAdapter;
    mBodyItemCount = externalAdapter.getItemCount();
}


@Override
public int getItemViewType(int position) {
    if(isHeaderView(position)){
        return HEADER_ITEM_TYPE;
    }

    else if (isFootView(position)) {
        return FOOTER_ITEM_TYPE;
    }
    return mExternalAdapter.getItemViewType(position);
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    switch (viewType) {
        case HEADER_ITEM_TYPE:
            View headerView = LayoutInflater.from(parent.getContext()).inflate(mExternalHeaderResId, parent, false);
            return new HeaderView(headerView);
        case FOOTER_ITEM_TYPE:
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_footer_indicator, parent, false);
            mFooterView = new FooterView(view);
            return mFooterView;
        default:
            return mExternalAdapter.onCreateViewHolder(parent, viewType);
    }
}

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if(isHeaderView(position)){
        return;
    }
    if (isFootView(position)) {
        return;
    }
    if(mExternalHeaderResId > 0){
        position = position - getHeaderViewCount();
    }
    mExternalAdapter.onBindViewHolder(holder, position);
}

@Override
public int getItemCount() {
    mBodyItemCount = mExternalAdapter.getItemCount();
    if(mBodyItemCount == 0){
        return 0;
    }
    else{
        return getHeaderViewCount() + mBodyItemCount + getFootViewCount();
    }
}





private boolean isHeaderView(int position){
    return mExternalHeaderResId > 0 && position == 0;
}

private boolean isFootView(int position) {
    return (position >= mBodyItemCount + getHeaderViewCount());
}

public int getFootViewCount() {
    return 1;
}

public int getHeaderViewCount(){
    return mExternalHeaderResId > 0 ? 1 : 0;
}





public void setLoadingIndicatorViewVisible(int visible){
    if(mFooterView != null){
        mFooterView.setLoadingIndicatorViewVisible(visible);
    }
}

public void setExternalHeaderResId(int externalHeaderResId) {
    mExternalHeaderResId = externalHeaderResId;
}

public void loadMoreOnSuccess(){
    setLoadingIndicatorViewVisible(View.GONE);
}

public void loadMoreOnError(){
    setLoadingIndicatorViewVisible(View.GONE);
}

public void loadMoreOnComplete(){
    setLoadingIndicatorViewVisible(View.GONE);
}






static class HeaderView extends RecyclerView.ViewHolder{

    HeaderView(View itemView) {
        super(itemView);
    }
}

static class FooterView extends RecyclerView.ViewHolder {

    @Bind(R.id.item_footer_indicator)
    LoadingIndicatorView mLoadingIndicatorView;

    FooterView(View itemView) {
        super(itemView);
        ButterKnife.bind(this, itemView);
        mLoadingIndicatorView.setVisibility(View.GONE);
    }

    void setLoadingIndicatorViewVisible(int visible){
        mLoadingIndicatorView.setVisibility(visible);
    }
}
}

底部FooterView的布局item_footer_indicator.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@android:color/transparent"
>

<com.sugary.refreshrecyclerview.enhancerecycler.indicator.LoadingIndicatorView
    android:id="@+id/item_footer_indicator"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    app:indicator_color="@color/indicator_loading_more_orange"
    />

</RelativeLayout>

LoadingIndicatorView

public class LoadingIndicatorView extends View {

//Sizes (with defaults in DP)
public static final int DEFAULT_SIZE = 50;

private Paint mPaint;

private BaseIndicatorController mIndicatorController;

private boolean mHasAnimation;


public LoadingIndicatorView(Context context) {
    this(context, null);
}

public LoadingIndicatorView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public LoadingIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(attrs);
}

private void init(AttributeSet attrs) {

    TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.LoadingIndicatorView);
    int indicatorColor = a.getColor(R.styleable.LoadingIndicatorView_indicator_color, Color.GRAY);
    a.recycle();

    mPaint = new Paint();
    mPaint.setColor(indicatorColor);
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setAntiAlias(true);

    mIndicatorController = new BallPulseIndicator();
    mIndicatorController.setTarget(this);
}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = measureDimension(dp2px(DEFAULT_SIZE), widthMeasureSpec);
    int height = measureDimension(dp2px(DEFAULT_SIZE), heightMeasureSpec);
    setMeasuredDimension(width, height);
}

private int measureDimension(int defaultSize, int measureSpec) {
    int result;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    if (specMode == MeasureSpec.EXACTLY) {
        result = specSize;
    } else if (specMode == MeasureSpec.AT_MOST) {
        result = Math.min(defaultSize, specSize);
    } else {
        result = defaultSize;
    }
    return result;
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    if (!mHasAnimation) {
        mHasAnimation = true;
        mIndicatorController.initAnimation();
    }
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    drawIndicator(canvas);
}





void drawIndicator(Canvas canvas) {
    mIndicatorController.draw(canvas, mPaint);
}


private int dp2px(int dpValue) {
    return (int) getContext().getResources().getDisplayMetrics().density * dpValue;
}





@Override
public void setVisibility(int v) {
    if (getVisibility() != v) {
        super.setVisibility(v);
        if (v == GONE || v == INVISIBLE) {
            mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.END);
        } else {
            mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.START);
        }
    }
}

@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    if (mHasAnimation) {
        mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.START);
    }
}

@Override
protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.CANCEL);
}

}

小結:

列表數據刷新舞痰,改成了調用EnhanceRecylerView方法土榴,用自己建的Adapter刷新數據無效(這是這個輪子的缺陷,有待改進)响牛。

底部滑動動畫使用了他人的開源動畫

在自制增強型RecyclerView過程中玷禽,也刷了一些資料,推薦閱讀呀打。

參考資料

RecyclerView必知必會(五星推薦)

Github:RecyclerView優(yōu)秀文集

Github:BeautifulRefreshLayout

Github:BaseRecyclerViewAdapterHelper

Github:XRecyclerView

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末矢赁,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子贬丛,更是在濱河造成了極大的恐慌撩银,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件豺憔,死亡現場離奇詭異额获,居然都是意外死亡,警方通過查閱死者的電腦和手機焕阿,發(fā)現死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門咪啡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人暮屡,你說我怎么就攤上這事∫闾遥” “怎么了褒纲?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵准夷,是天一觀的道長。 經常有香客問我莺掠,道長衫嵌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任彻秆,我火速辦了婚禮楔绞,結果婚禮上,老公的妹妹穿的比我還像新娘唇兑。我一直安慰自己酒朵,他們只是感情好,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布扎附。 她就那樣靜靜地躺著蔫耽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪留夜。 梳的紋絲不亂的頭發(fā)上匙铡,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機與錄音碍粥,去河邊找鬼鳖眼。 笑死,一個胖子當著我的面吹牛嚼摩,可吹牛的內容都是我干的具帮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼低斋,長吁一口氣:“原來是場噩夢啊……” “哼蜂厅!你這毒婦竟也來了?” 一聲冷哼從身側響起膊畴,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤掘猿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后唇跨,有當地人在樹林里發(fā)現了一具尸體稠通,經...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年买猖,在試婚紗的時候發(fā)現自己被綠了改橘。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡玉控,死狀恐怖飞主,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤碌识,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布碾篡,位于F島的核電站,受9級特大地震影響筏餐,放射性物質發(fā)生泄漏开泽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一魁瞪、第九天 我趴在偏房一處隱蔽的房頂上張望穆律。 院中可真熱鬧,春花似錦导俘、人聲如沸峦耘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贡歧。三九已至,卻和暖如春赋秀,著一層夾襖步出監(jiān)牢的瞬間利朵,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工猎莲, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绍弟,地道東北人。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓著洼,卻偏偏與公主長得像樟遣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子身笤,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內容