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

簡述

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

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

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

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

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

實現的注意點解析

如何判斷RecyclerView滑動到了底部

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

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

這里涉及到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優秀文集

Github:BeautifulRefreshLayout

Github:BaseRecyclerViewAdapterHelper

Github:XRecyclerView

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容