1. 程式人生 > >RecyclerView Adapter 優雅封裝,一個Adapter搞定所有列表

RecyclerView Adapter 優雅封裝,一個Adapter搞定所有列表

專案中,我們用得最多的元素就是列表了,在Android 中,實現列表用原生的RecyclerView就能滿足需求,關於RecyclerView 的基礎使用這裡不做過多的介紹,網上有太多的博文介紹了。本篇文章將介紹自己封裝的一個Adapter,幫你快速高效的新增一個列表(包括單 Item 列表和多item列表)。

理念

1, 構造一個通用的Adapter模版,避免每新增一個列表就要寫一個Adapter,避免寫Adapter中的大量重複程式碼。
2,通過組裝的方式來構建Adapter,將每一種(ViewType不同的)Item抽象成一個單獨元件,Adapter 就是一個殼,我們只需要向Adapter中新增Item就行,這樣做的好處就是減少耦合,去掉一種item 或者新增一種item對於列表是沒有任何影響的。
3,高內聚,低耦合,擴充套件方便。

思路

為每一種 viewType 定義一個Cell,Cell就是上面提到的獨立元件,它負責建立ViewHolder,資料繫結和邏輯處理。它有2個重要的方法,onCreateViewHolder 負責建立ViewHolder,onBindViewHolder負責資料繫結,這兩個方法的定義和生命週期同Adapter種的2個方法一樣,事實上,Adapter 中的onCreateViewHolder和onBindViewHolder 最終呼叫的是Cell中的方法。

一種 ViewType 對應一個Cell
看一個示例:


cell_simple.png

如上圖:以豆瓣APP的首頁為例,文章包含圖片和視訊的兩個Item 的佈局是不同的,因此,可以新增兩個Cell(ImageCell和VideoCell)來分別處理這兩種Item。

有了Cell之後,要向列表新增新增Header和Footer 的需求就很簡單了,我們直接新增一個HeaderCell和FooterCell 就可以了,也不用更改Adapter程式碼,是不是很方便。此外,還可以用Cell實現列表LoadMore(載入更多)狀態、Loadding(載入中)狀態、Empty(空頁面)狀態、Error(出錯)狀態 View的顯示。

包結構


rv_pakage.png

介紹:1,base:base包下面為Lib的主要程式碼,一個Cell介面和三個抽象類,分別抽取了Adapter,ViewHolder,Cell的公共邏輯。
2,cell:cell包下面有4個cell,分別顯示列表的LoadMore,Loading,Empty,Error狀態。
3,fragment:

有一個Fragment抽象類,定義了一個UI模版(不需要額外添加布局檔案),要實現列表的介面只需要繼承AbsBaseFragment,實現幾個方法新增資料就OK。

具體程式碼

1,Cell 介面定義
/**
 * Created by zhouwei on 17/1/19.
 */

public interface Cell {
    /**
     * 回收資源
     *
     */
    public void releaseResource();

    /**
     * 獲取viewType
     * @return
     */
    public  int getItemType();

    /**
     * 建立ViewHolder
     * @param parent
     * @param viewType
     * @return
     */
    public RVBaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType);

    /**
     * 資料繫結
     * @param holder
     * @param position
     */
    public  void onBindViewHolder(RVBaseViewHolder holder, int position);
}

定義了4個方法,除了回收資源的方法releaseResource(),其它三個和Adapter中的一樣。

2,RVBaseCell
/**
 * Created by zhouwei on 17/1/19.
 */

public  abstract class RVBaseCell<T> implements Cell {

    public RVBaseCell(T t){
        mData = t;
    }
    public T mData;

    @Override
    public void releaseResource() {
        // do nothing
        // 如果有需要回收的資源,子類自己實現
    }
}

抽象類,接受一個範型T(Cell接受的資料實體),實現了releaseResource方法,但什麼事也沒幹,因為有很多簡單的Cell沒有資源回收,就不需要實現。如果子類Cell 有資源回收,重寫這個方法就可以了。

3, RVBaseViewHolder
/**
 * Created by zhouwei on 17/1/19.
 */

public class RVBaseViewHolder extends RecyclerView.ViewHolder{
    private SparseArray<View> views;
    private View mItemView;
    public RVBaseViewHolder(View itemView) {
        super(itemView);
        views = new SparseArray<>();
        mItemView = itemView;

    }

    /**
     * 獲取ItemView
     * @return
     */
    public View getItemView() {
        return mItemView;
    }

    public View getView(int resId) {
        return retrieveView(resId);
    }

    public TextView getTextView(int resId){
        return retrieveView(resId);
    }

    public ImageView getImageView(int resId){
        return retrieveView(resId);
    }

    public Button getButton(int resId){
        return retrieveView(resId);
    }

    @SuppressWarnings("unchecked")
    protected <V extends View> V retrieveView(int viewId){
        View view = views.get(viewId);
        if(view == null){
            view = mItemView.findViewById(viewId);
            views.put(viewId,view);
        }
        return (V) view;
    }

    public void setText(int resId,CharSequence text){
          getTextView(resId).setText(text);
    }

    public void setText(int resId,int strId){
        getTextView(resId).setText(strId);
    }

}

以前寫Adapter的時候,每一種viewType 都對應了一個ViewHolder,其中有大量的findViewById繫結檢視,有了RVBaseViewHolder,再也不需要定義ViewHolder了,通過id獲取View就行,View用SparseArray 儲存進行了複用,避免每一次都find。

4,RVBaseAdapter
/**
 * Created by zhouwei on 17/1/19.
 */

public   abstract class RVBaseAdapter<C extends RVBaseCell>  extends RecyclerView.Adapter<RVBaseViewHolder>{
    public static final String TAG = "RVBaseAdapter";
    protected List<C> mData;

    public RVBaseAdapter(){
        mData = new ArrayList<>();
    }

    public void setData(List<C> data) {
        addAll(data);
        notifyDataSetChanged();
    }

    public List<C> getData() {
        return mData;
    }

    @Override
    public RVBaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        for(int i=0;i<getItemCount();i++){
            if(viewType == mData.get(i).getItemType()){
                return mData.get(i).onCreateViewHolder(parent,viewType);
            }
        }

        throw new RuntimeException("wrong viewType");
    }

    @Override
    public void onBindViewHolder(RVBaseViewHolder holder, int position) {
        mData.get(position).onBindViewHolder(holder,position);
    }

    @Override
    public void onViewDetachedFromWindow(RVBaseViewHolder holder) {
        super.onViewDetachedFromWindow(holder);
        Log.e(TAG,"onViewDetachedFromWindow invoke...");
        //釋放資源
        int position = holder.getAdapterPosition();
        //越界檢查
        if(position<0 || position>=mData.size()){
            return;
        }
        mData.get(position).releaseResource();
    }


    @Override
    public int getItemCount() {
        return mData == null ? 0:mData.size();
    }

    @Override
    public int getItemViewType(int position) {
        return mData.get(position).getItemType();
    }

    /**
     * add one cell
     * @param cell
     */
    public void add(C cell){
         mData.add(cell);
         int index = mData.indexOf(cell);
         notifyItemChanged(index);
    }

    public void add(int index,C cell){
        mData.add(index,cell);
        notifyItemChanged(index);
    }

    /**
     * remove a cell
     * @param cell
     */
    public void remove(C cell){
        int indexOfCell = mData.indexOf(cell);
        remove(indexOfCell);
    }

    public void remove(int index){
        mData.remove(index);
        notifyItemRemoved(index);
    }

    /**
     *
     * @param start
     * @param count
     */
    public void remove(int start,int count){
        if((start +count) > mData.size()){
            return;
        }
        int size = getItemCount();
        for(int i =start;i<size;i++){
            mData.remove(i);
        }

        notifyItemRangeRemoved(start,count);
    }




    /**
     * add a cell list
     * @param cells
     */
    public void addAll(List<C> cells){
        if(cells == null || cells.size() == 0){
            return;
        }
        Log.e("zhouwei","addAll cell size:"+cells.size());
        mData.addAll(cells);
        notifyItemRangeChanged(mData.size()-cells.size(),mData.size());
    }

    public void addAll(int index,List<C> cells){
        if(cells == null || cells.size() == 0){
            return;
        }
        mData.addAll(index,cells);
        notifyItemRangeChanged(index,index+cells.size());
    }

    public void clear(){
        mData.clear();
        notifyDataSetChanged();
    }


    /**
     * 如果子類需要在onBindViewHolder 回撥的時候做的操作可以在這個方法裡做
     * @param holder
     * @param position
     */
    protected abstract void onViewHolderBound(RVBaseViewHolder holder, int position);

}

RVBaseAdapter 繼承 RecyclerView.Adapter,接受的是RVBaseCell型別,儲存了一個Cell 列表。其中還有有新增、移除,清空、更新資料的方法。
注意其中幾個方法:
1,getItemViewType: 呼叫的是對應position Cell 的getItemViewType 方法。

2,onCreateViewHolder:呼叫Cell 的onCreateViewHolder 建立ViewHolder。

3,onBindViewHolder: 呼叫對應Cell的onBindViewHolder 方法繫結資料

4,onViewDetachedFromWindow: 資源回收

5,RVSimpleAdapter
/**
 * Created by zhouwei on 17/1/23.
 */

public class RVSimpleAdapter extends RVBaseAdapter{
    public static final int ERROR_TYPE = Integer.MAX_VALUE -1;
    public static final int EMPTY_TYPE = Integer.MAX_VALUE -2;
    public static final int LOADING_TYPE = Integer.MAX_VALUE -3;
    public static final int LOAD_MORE_TYPE = Integer.MAX_VALUE -4;

    private EmptyCell mEmptyCell;
    private ErrorCell mErrorCell;
    private LoadingCell mLoadingCell;
    private LoadMoreCell mLoadMoreCell;
    //LoadMore 是否已顯示
    private boolean mIsShowLoadMore = false;

    public RVSimpleAdapter(){
        mEmptyCell = new EmptyCell(null);
        mErrorCell = new ErrorCell(null);
        mLoadingCell = new LoadingCell(null);
        mLoadMoreCell = new LoadMoreCell(null);
    }
    @Override
    protected void onViewHolderBound(RVBaseViewHolder holder, int position) {

    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
        //處理GridView 佈局
        if(manager instanceof GridLayoutManager){
            final GridLayoutManager gridLayoutManager = (GridLayoutManager) manager;
            gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    int viewType = getItemViewType(position);
                    return (viewType == ERROR_TYPE|| viewType == EMPTY_TYPE || viewType == LOADING_TYPE
                    ||viewType == LOAD_MORE_TYPE) ? gridLayoutManager.getSpanCount():1;
                }
            });
        }

    }

    @Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        // 處理StaggeredGridLayoutManager 顯示這個Span
        int position = holder.getAdapterPosition();
        int viewType = getItemViewType(position);
        if(isStaggeredGridLayout(holder)){
            if(viewType == ERROR_TYPE|| viewType == EMPTY_TYPE || viewType == LOADING_TYPE
                    ||viewType == LOAD_MORE_TYPE){

                StaggeredGridLayoutManager.LayoutParams params = (StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams();
                //設定顯示整個span
                params.setFullSpan(true);
            }
        }

    }

    private boolean isStaggeredGridLayout(RecyclerView.ViewHolder holder) {
        ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams();
        if (layoutParams != null && layoutParams instanceof StaggeredGridLayoutManager.LayoutParams) {
            return true;
        }
        return false;
    }

    /**
     * 顯示LoadingView
     * <p>請求資料時呼叫,資料請求完畢時呼叫{@link #hideLoading }</p>
     * @see #showLoadingKeepCount(int)
     */
    public void showLoading(){
      clear();
      add(mLoadingCell);
    }

    public void showLoading(View loadingView){
        if(loadingView == null){
            showLoading();
        }
        clear();
        mLoadingCell.setLoadingView(loadingView);
        add(mLoadingCell);
    }
    /**
     * 顯示LoadingView
     * <p>列表顯示LoadingView並保留keepCount個Item</p>
     * @param keepCount 保留的條目數量
     */
    public void showLoadingKeepCount(int keepCount){
        if(keepCount < 0 || keepCount>mData.size()){
            return;
        }
        remove(keepCount,mData.size() - keepCount);
        if(mData.contains(mLoadingCell)){
            mData.remove(mLoadingCell);
        }
        add(mLoadingCell);
    }
    /**
     * hide Loading view
     */
    public void hideLoading(){
        if(mData.contains(mLoadingCell)){
            mData.remove(mLoadingCell);
        }
    }

    /**
     * 顯示錯誤提示
     * <p>當網路請求發生錯誤,需要在介面給出錯誤提示時,呼叫{@link #showError}</p>
     * @see #showErrorKeepCount(int)
     */
    public void showError(){
       clear();
       add(mErrorCell);
    }

    /**
     * 顯示錯誤提示
     * <p>當網路請求發生錯誤,需要在介面給出錯誤提示時,呼叫{@link #showErrorKeepCount(int)},並保留keepCount 條Item</p>
     * @param keepCount 保留Item數量
     */
    public void showErrorKeepCount(int keepCount){
        if(keepCount < 0 || keepCount>mData.size()){
            return;
        }
        remove(keepCount,mData.size() - keepCount);
        if(mData.contains(mErrorCell)){
            mData.remove(mErrorCell);
        }
        add(mErrorCell);

    }

    /**
     * 隱藏錯誤提示
     */
    public void hideErorr(){
       if(mData.contains(mErrorCell)){
           remove(mErrorCell);
       }
    }

    /**
     * 顯示LoadMoreView
     * <p>當列表滑動到底部時,呼叫{@link #showLoadMore()} 提示載入更多,載入完資料,呼叫{@link #hideLoadMore()}
     * 隱藏LoadMoreView,顯示列表資料。</p>
     *
     */
    public void showLoadMore(){
       if(mData.contains(mLoadMoreCell)){
           return;
       }
       add(mLoadMoreCell);
       mIsShowLoadMore = true;
    }

    /**
     * 隱藏LoadMoreView
     * <p>呼叫{@link #showLoadMore()}之後,載入資料完成,呼叫{@link #hideLoadMore()}隱藏LoadMoreView</p>
     */
    public void hideLoadMore(){
       if(mData.contains(mLoadMoreCell)){
           remove(mLoadMoreCell);
           mIsShowLoadMore = false;
       }
    }

    /**
     * LoadMore View 是否已經顯示
     * @return
     */
    public boolean isShowLoadMore() {
        return mIsShowLoadMore;
    }

    /**
     *
     * @param keepCount
     */
    public void showEmptyKeepCount(int keepCount){
        if(keepCount < 0 || keepCount>mData.size()){
            return;
        }
        remove(keepCount,mData.size() - keepCount);
        if(mData.contains(mEmptyCell)){
            mData.remove(mEmptyCell);
        }
        add(mEmptyCell);

    }

    /**
     * 顯示空view
     * <p>當頁面沒有資料的時候,呼叫{@link #showEmpty()}顯示空View,給使用者提示</p>
     */
    public void showEmpty(){
      clear();
      add(mEmptyCell);
    }

    /**
     * 隱藏空View
     */
    public void hideEmpty(){
      if(mData.contains(mEmptyCell)){
          remove(mEmptyCell);
      }
    }
}

RVSimpleAdapter 是RVBaseAdapter 的預設實現類,添加了顯示LoadMore View、Loading View 、Empty View、ErrorView 的功能。

6,AbsBaseFragment
/**
 * Created by zhouwei on 17/2/3.
 */

public abstract class AbsBaseFragment<T> extends Fragment {
    protected RecyclerView mRecyclerView;
    protected RVSimpleAdapter mBaseAdapter;
    private FrameLayout mToolbarContainer;
    protected SwipeRefreshLayout mSwipeRefreshLayout;
    /**
     * RecyclerView 最後可見Item在Adapter中的位置
     */
    private int mLastVisiblePosition = -1;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view =  inflater.inflate(R.layout.base_fragment_layout,null);
        return view;
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        mSwipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.base_refresh_layout);
        mToolbarContainer = (FrameLayout) view.findViewById(R.id.toolbar_container);
        mRecyclerView = (RecyclerView) view.findViewById(R.id.base_fragment_rv);
        mRecyclerView.setLayoutManager(initLayoutManger());
        mBaseAdapter = initAdapter();
        mRecyclerView.setAdapter(mBaseAdapter);
        mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                setRefreshing(true);
                onPullRefresh();
            }
        });
        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {

                RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
                if(layoutManager instanceof LinearLayoutManager){
                    mLastVisiblePosition = ((LinearLayoutManager)layoutManager).findLastVisibleItemPosition();
                }else if(layoutManager instanceof GridLayoutManager){
                    mLastVisiblePosition = ((GridLayoutManager)layoutManager).findLastVisibleItemPosition();
                }else if(layoutManager instanceof StaggeredGridLayoutManager){
                    StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
                    int []lastPositions = new int[staggeredGridLayoutManager.getSpanCount()];
                    staggeredGridLayoutManager.findLastVisibleItemPositions(lastPositions);
                    mLastVisiblePosition = findMax(lastPositions);
                }
            }

            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                View firstView = recyclerView.getChildAt(0);
                int top = firstView.getTop();
                int topEdge = recyclerView.getPaddingTop();
                //判斷RecyclerView 的ItemView是否滿屏,如果不滿一屏,上拉不會觸發載入更多
                boolean isFullScreen = top < topEdge;

                RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
                int itemCount = manager.getItemCount();
                //因為LoadMore View  是Adapter的一個Item,顯示LoadMore 的時候,Item數量+1了,導致 mLastVisibalePosition == itemCount-1
                // 判斷兩次都成立,因此必須加一個判斷條件 !mBaseAdapter.isShowLoadMore()
                if(newState == RecyclerView.SCROLL_STATE_IDLE && mLastVisiblePosition == itemCount-1 && isFullScreen && !mBaseAdapter.isShowLoadMore()){
                   //最後一個Item了
                   mBaseAdapter.showLoadMore();
                   onLoadMore();
                }
            }
        });
        View toolbarView = addToolbar();
        if(toolbarView!=null && mToolbarContainer!=null
                ){
            mToolbarContainer.addView(toolbarView);
        }
        onRecyclerViewInitialized();

    }

    /**
     * hide load more progress
     */
    public void hideLoadMore(){
        if(mBaseAdapter!=null){
            mBaseAdapter.hideLoadMore();
        }
    }

    /**
     * 獲取組數最大值
     * @param lastPositions
     * @return
     */
    private int findMax(int[] lastPositions) {
        int max = lastPositions[0];
        for (int value : lastPositions) {
            if (value > max) {
                max = value;
            }
        }
        return max;
    }

    /**
     * 設定重新整理進度條的顏色
     * see{@link SwipeRefreshLayout#setColorSchemeResources(int...)}
     * @param colorResIds
     */
    public void setColorSchemeResources(@ColorRes int... colorResIds){
        if(mSwipeRefreshLayout!=null){
            mSwipeRefreshLayout.setColorSchemeResources(colorResIds);
        }
    }

    /**
     * 設定重新整理進度條的顏色
     * see{@link SwipeRefreshLayout#setColorSchemeColors(int...)}
     * @param colors
     */
    public void setColorSchemeColors(int... colors){
        if(mSwipeRefreshLayout!=null){
            mSwipeRefreshLayout.setColorSchemeColors(colors);
        }
    }

    /**
     * 設定重新整理進度條背景色
     *  see{@link SwipeRefreshLayout#setProgressBackgroundColorSchemeResource(int)} (int)}
     * @param colorRes
     */
    public void setProgressBackgroundColorSchemeResource(@ColorRes int colorRes) {
         if(mSwipeRefreshLayout!=null){
             mSwipeRefreshLayout.setProgressBackgroundColorSchemeResource(colorRes);
         }
    }

    /**
     * 設定重新整理進度條背景色
     *  see{@link SwipeRefreshLayout#setProgressBackgroundColorSchemeColor(int)}
     * @param color
     */
    public void setProgressBackgroundColorSchemeColor(@ColorInt int color) {
       if(mSwipeRefreshLayout!=null){
           mSwipeRefreshLayout.setProgressBackgroundColorSchemeColor(color);
       }
    }

    /**
     * Notify the widget that refresh state has changed. Do not call this when
     * refresh is triggered by a swipe gesture.
     *
     * @param refreshing Whether or not the view should show refresh progress.
     */
    public void setRefreshing(boolean refreshing){
        if(mSwipeRefreshLayout== null){
            return;