RecyclerView更全解析之 - 為它優雅的添加頭部和底部

maxuefeng 9年前發布 | 12K 次閱讀 Android開發 移動開發 RecyclerView

1.概述

這一期我們來動態為RecyclerView去加載頭部和底部,為上一期的RecyclerView列表數據添加廣告輪播圖,至于廣告輪播大家可以看一下這一期 Android無限廣告輪播 - 自定義BannerView ,這里我就不多講了,直接拿過來用。

 

這里寫圖片描述

2.基本思路

我們開始接觸RecyclerView的時候肯定接觸過ListView,這個我們再熟悉不過了。后來我們用著用著RecyclerView發現它可能有很多坑的地方可能大家覺得它不如ListView,其實我們發現后來出的這些新的控件其實給了用戶更多的自定義,更多的完全由開發者去實現這其實也是有利于擴展的。我們自己寫代碼理因也如此,到后面大家也會發現我們要做一些高級功能如仿QQ側滑刪除淘寶拖拽排序會 so easy,我們到后面再嘮。

為了RecyclerView添加頭部和底部,網上很多可以說是各顯神通千奇百怪,其實我們ListView就有addHeaderView(View view)方法。所以我也想用recyclerView.addHeaderView(),但是錘子發現并沒有這個方法直接就報錯,所以只好決定仿照Google的ListView的源碼去寫了。因為我們到后面還涉及到下拉刷新上拉加載問題,如果一次不搞好加班每天都加不過來怎么還有時間去重構。

3.基本實現

3.1 瞅瞅ListView的addHeaderView

public void addHeaderView(View v, Object data, boolean isSelectable) {
        // 一些基本信息封裝
        final FixedViewInfo info = new FixedViewInfo();
        info.view = v;
        info.data = data;
        info.isSelectable = isSelectable;
        mHeaderViewInfos.add(info);
        mAreAllItemsSelectable &= isSelectable;

    // 首先判斷是不是空,我所以前如果沒設置Adapter就是添加不了頭部咯  Soga
    // Wrap the adapter if it wasn't already wrapped.
    if (mAdapter != null) {
        // 判斷有沒有被包裹過
        if (!(mAdapter instanceof HeaderViewListAdapter)) {
            mAdapter = new HeaderViewListAdapter(mHeaderViewInfos,
                mFooterViewInfos, mAdapter);
        }

        // In the case of re-adding a header view, or adding one later on,
        // we need to notify the observer.
        if (mDataSetObserver != null) {
            // 觀察者模式不多寫了
            mDataSetObserver.onChanged();
        }
    }
}</code></pre> 

接下來我就挑一下關鍵代碼,某些就省略了,強迫癥自己去閱讀源碼吧

public class HeaderViewListAdapter implements WrapperListAdapter, Filterable {

    private final ListAdapter mAdapter;


    // These two ArrayList are assumed to NOT be null.
    // They are indeed created when declared in ListView and then shared.
    // 存放頭部和底部集合
    ArrayList<ListView.FixedViewInfo> mHeaderViewInfos;
    ArrayList<ListView.FixedViewInfo> mFooterViewInfos;

    public HeaderViewListAdapter(ArrayList<ListView.FixedViewInfo> headerViewInfos,
                                 ArrayList<ListView.FixedViewInfo> footerViewInfos,
                                 ListAdapter adapter) {
        // 這才是最原始的列表Adapter
        mAdapter = adapter;
        // ......
    }

    // 獲取條數
    public int getCount() {
        if (mAdapter != null) {
            // 三者相加 = 底部條數 + 頭部條數 + Adapter的條數
            return getFootersCount() + getHeadersCount() + mAdapter.getCount();
        } else {
            return getFootersCount() + getHeadersCount();
        }
    }

    // getView方法這個應該都很熟悉
    public View getView(int position, View convertView, ViewGroup parent) {
        // Header (negative positions will throw an IndexOutOfBoundsException)
        // 根據當前位置判斷是不是頭部
        int numHeaders = getHeadersCount();
        if (position < numHeaders) {
            // 如果是頭部直接返回傳遞過來的View
            return mHeaderViewInfos.get(position).view;
        }

        // Adapter部分
        final int adjPosition = position - numHeaders;
        int adapterCount = 0;
        if (mAdapter != null) {
            adapterCount = mAdapter.getCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getView(adjPosition, convertView, parent);
            }
        }

        // 底部部分
        // Footer (off-limits positions will throw an IndexOutOfBoundsException)
        return mFooterViewInfos.get(adjPosition - adapterCount).view;
    }

    // 獲取View的類型這個RecyclerView也雷同
    public int getItemViewType(int position) {
        int numHeaders = getHeadersCount();
        if (mAdapter != null && position >= numHeaders) {
            int adjPosition = position - numHeaders;
            int adapterCount = mAdapter.getCount();
            if (adjPosition < adapterCount) {
                return mAdapter.getItemViewType(adjPosition);
            }
        }
        return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
    }
}

其實關鍵源碼也不多,還有英文注釋不像上一次讀源碼完全沒有注釋,感覺那Google工程師有點打醬油。到這里也知道了,其實ListView并不是支持直接添加頭部和底部,而是在內部寫了一個包裹類,做了一系列的處理才可以,那么接下來我們也就參照這種方式,因為這估計是最權威的代碼了就模仿你了。

3.2 先構建WrapRecyclerAdapter

/**
 * Created by Darren on 2016/12/29.
 * Email: 240336124@qq.com
 * Description: 可以添加頭部和底部的Adapter
 */
public class WrapRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private final static String TAG = "WrapRecyclerAdapter";
    // 用來存放底部和頭部View的集合  比Map要高效一些
    // 可以點擊進入看一下官方的解釋
    /**
     * SparseArrays map integers to Objects.  Unlike a normal array of Objects,
     * there can be gaps in the indices.  It is intended to be more memory efficient
     * than using a HashMap to map Integers to Objects, both because it avoids
     * auto-boxing keys and its data structure doesn't rely on an extra entry object
     * for each mapping.
     */
    private SparseArray<View> mHeaderViews;
    private SparseArray<View> mFooterViews;

    // 基本的頭部類型開始位置  用于viewType
    private static int BASE_ITEM_TYPE_HEADER = 10000000;
    // 基本的底部類型開始位置  用于viewType
    private static int BASE_ITEM_TYPE_FOOTER = 20000000;

    // 列表的Adapter
    private RecyclerView.Adapter mAdapter;

    public WrapRecyclerAdapter(RecyclerView.Adapter adapter) {
        this.mAdapter = adapter;
        mHeaderViews = new SparseArray<>();
        mFooterViews = new SparseArray<>();
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        // viewType 可能就是 SparseArray 的key
        if (isHeaderViewType(viewType)) {
            View headerView = mHeaderViews.get(viewType);
            return createHeaderFooterViewHolder(headerView);
        }

        if (isFooterViewType(viewType)) {
            View footerView = mFooterViews.get(viewType);
            return createHeaderFooterViewHolder(footerView);
        }
        return mAdapter.onCreateViewHolder(parent, viewType);
    }

    /**
     * 是不是底部類型
     */
    private boolean isFooterViewType(int viewType) {
        int position = mFooterViews.indexOfKey(viewType);
        return position >= 0;
    }

    /**
     * 創建頭部或者底部的ViewHolder
     */
    private RecyclerView.ViewHolder createHeaderFooterViewHolder(View view) {
        return new RecyclerView.ViewHolder(view) {

        };
    }

    /**
     * 是不是頭部類型
     */
    private boolean isHeaderViewType(int viewType) {
        int position = mHeaderViews.indexOfKey(viewType);
        return position >= 0;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (isHeaderPosition(position) || isFooterPosition(position)) {
            return;
        }
        // 計算一下位置
        position = position - mHeaderViews.size();
        mAdapter.onBindViewHolder(holder, position);
    }

    @Override
    public int getItemViewType(int position) {
        if (isHeaderPosition(position)) {
            // 直接返回position位置的key
            return mHeaderViews.keyAt(position);
        }
        if (isFooterPosition(position)) {
            // 直接返回position位置的key
            position = position - mHeaderViews.size() - mAdapter.getItemCount();
            return mFooterViews.keyAt(position);
        }
        // 返回列表Adapter的getItemViewType
        position = position - mHeaderViews.size();
        return mAdapter.getItemViewType(position);
    }

    /**
     * 是不是底部位置
     */
    private boolean isFooterPosition(int position) {
        return position >= (mHeaderViews.size() + mAdapter.getItemCount());
    }

    /**
     * 是不是頭部位置
     */
    private boolean isHeaderPosition(int position) {
        return position < mHeaderViews.size();
    }

    @Override
    public int getItemCount() {
        // 條數三者相加 = 底部條數 + 頭部條數 + Adapter的條數
        return mAdapter.getItemCount() + mHeaderViews.size() + mFooterViews.size();
    }

    /**
     * 獲取列表的Adapter
     */
    private RecyclerView.Adapter getAdapter() {
        return mAdapter;
    }

    // 添加頭部
    public void addHeaderView(View view) {
        int position = mHeaderViews.indexOfValue(view);
        if (position < 0) {
            mHeaderViews.put(BASE_ITEM_TYPE_HEADER++, view);
        }
        notifyDataSetChanged();
    }

    // 添加底部
    public void addFooterView(View view) {
        int position = mFooterViews.indexOfValue(view);
        if (position < 0) {
            mFooterViews.put(BASE_ITEM_TYPE_FOOTER++, view);
        }
        notifyDataSetChanged();
    }

    // 移除頭部
    public void removeHeaderView(View view) {
        int index = mHeaderViews.indexOfValue(view);
        if (index < 0) return;
        mHeaderViews.removeAt(index);
        notifyDataSetChanged();
    }

    // 移除底部
    public void removeFooterView(View view) {
        int index = mFooterViews.indexOfValue(view);
        if (index < 0) return;
        mFooterViews.removeAt(index);
        notifyDataSetChanged();
    }

    /**
     * 解決GridLayoutManager添加頭部和底部不占用一行的問題
     *
     * @param recycler
     * @version 1.0
     */
    public void adjustSpanSize(RecyclerView recycler) {
        if (recycler.getLayoutManager() instanceof GridLayoutManager) {
            final GridLayoutManager layoutManager = (GridLayoutManager) recycler.getLayoutManager();
            layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    boolean isHeaderOrFooter =
                            isHeaderPosition(position) || isFooterPosition(position);
                    return isHeaderOrFooter ? layoutManager.getSpanCount() : 1;
                }
            });
        }
    }
}

接下來我們直接在上一期的列表基礎上加兩個頭部和底部測試一下

這里寫圖片描述

那趕緊把輪播圖加載進來吧,千萬別。還有事情沒做最忌諱的就是過度設計誰也看不懂一層套一層還有就是半吊子總感覺少了點什么,順便說個題外話剛才群里有人說看看博客裝起B來一套一套的,哈哈。

3.3 先構建WrapRecyclerView

我們最好還是模仿ListView的結構搞就搞到西,自定義一個WrapRecyclerView,可以添加刪除頭部和底部View,這個就比較簡單了

/**
 * Created by Darren on 2016/12/29.
 * Email: 240336124@qq.com
 * Description: 可以添加頭部和底部的RecyclerView
 */
public class WrapRecyclerView extends RecyclerView {
    // 包裹了一層的頭部底部Adapter
    private WrapRecyclerAdapter mWrapRecyclerAdapter;
    // 這個是列表數據的Adapter
    private RecyclerView.Adapter mAdapter;

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

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

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

    @Override
    public void setAdapter(Adapter adapter) {
        // 為了防止多次設置Adapter
        if (mAdapter != null) {
            mAdapter.unregisterAdapterDataObserver(mDataObserver);
            mAdapter = null;
        }

        this.mAdapter = adapter;

        if (adapter instanceof WrapRecyclerAdapter) {
            mWrapRecyclerAdapter = (WrapRecyclerAdapter) adapter;
        } else {
            mWrapRecyclerAdapter = new WrapRecyclerAdapter(adapter);
        }

        super.setAdapter(mWrapRecyclerAdapter);

        // 注冊一個觀察者
        mAdapter.registerAdapterDataObserver(mDataObserver);

        // 解決GridLayout添加頭部和底部也要占據一行
        mWrapRecyclerAdapter.adjustSpanSize(this);
    }

    // 添加頭部
    public void addHeaderView(View view) {
        // 如果沒有Adapter那么就不添加,也可以選擇拋異常提示
        // 讓他必須先設置Adapter然后才能添加,這里是仿照ListView的處理方式
        if (mWrapRecyclerAdapter != null) {
            mWrapRecyclerAdapter.addHeaderView(view);
        }
    }

    // 添加底部
    public void addFooterView(View view) {
        if (mWrapRecyclerAdapter != null) {
            mWrapRecyclerAdapter.addFooterView(view);
        }
    }

    // 移除頭部
    public void removeHeaderView(View view) {
        if (mWrapRecyclerAdapter != null) {
            mWrapRecyclerAdapter.removeHeaderView(view);
        }
    }

    // 移除底部
    public void removeFooterView(View view) {
        if (mWrapRecyclerAdapter != null) {
            mWrapRecyclerAdapter.removeFooterView(view);
        }
    }

    private AdapterDataObserver mDataObserver = new AdapterDataObserver() {
        @Override
        public void onChanged() {
            if (mAdapter == null) return;
            // 觀察者  列表Adapter更新 包裹的也需要更新不然列表的notifyDataSetChanged沒效果
            if (mWrapRecyclerAdapter != mAdapter)
                mWrapRecyclerAdapter.notifyDataSetChanged();
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            if (mAdapter == null) return;
            // 觀察者  列表Adapter更新 包裹的也需要更新不然列表的notifyDataSetChanged沒效果
            if (mWrapRecyclerAdapter != mAdapter)
                mWrapRecyclerAdapter.notifyItemRemoved(positionStart);
        }

        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            if (mAdapter == null) return;
            // 觀察者  列表Adapter更新 包裹的也需要更新不然列表的notifyItemMoved沒效果
            if (mWrapRecyclerAdapter != mAdapter)
                mWrapRecyclerAdapter.notifyItemMoved(fromPosition, toPosition);
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount) {
            if (mAdapter == null) return;
            // 觀察者  列表Adapter更新 包裹的也需要更新不然列表的notifyItemChanged沒效果
            if (mWrapRecyclerAdapter != mAdapter)
                mWrapRecyclerAdapter.notifyItemChanged(positionStart);
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
            if (mAdapter == null) return;
            // 觀察者  列表Adapter更新 包裹的也需要更新不然列表的notifyItemChanged沒效果
            if (mWrapRecyclerAdapter != mAdapter)
                mWrapRecyclerAdapter.notifyItemChanged(positionStart,payload);
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            if (mAdapter == null) return;
            // 觀察者  列表Adapter更新 包裹的也需要更新不然列表的notifyItemInserted沒效果
            if (mWrapRecyclerAdapter != mAdapter)
                mWrapRecyclerAdapter.notifyItemInserted(positionStart);
        }
    };
}

就不測試了,相信我是測試了沒問題才貼的代碼,還是那就話簡簡單單幾行代碼so easy,接下來直接整合輪播圖。

3.4 實戰整合輪播圖

RecyclerView更全解析之 - 為它優雅的添加頭部和底部

這里寫圖片描述

 

 

 

 本文由用戶 maxuefeng 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!