RecyclerView更全解析之 - 为它优雅的添加头部和底部
maxuefeng
8年前
<h3>1.概述</h3> <p>这一期我们来动态为RecyclerView去加载头部和底部,为上一期的RecyclerView列表数据添加广告轮播图,至于广告轮播大家可以看一下这一期 Android无限广告轮播 - 自定义BannerView ,这里我就不多讲了,直接拿过来用。</p> <p> </p> <p style="text-align:center"><img src="https://simg.open-open.com/show/91da3e055ceafcd92cfc32e7017d8448.gif"></p> <p style="text-align:center">这里写图片描述</p> <h3>2.基本思路</h3> <p>我们开始接触RecyclerView的时候肯定接触过ListView,这个我们再熟悉不过了。后来我们用着用着RecyclerView发现它可能有很多坑的地方可能大家觉得它不如ListView,其实我们发现后来出的这些新的控件其实给了用户更多的自定义,更多的完全由开发者去实现这其实也是有利于扩展的。我们自己写代码理因也如此,到后面大家也会发现我们要做一些高级功能如仿QQ侧滑删除淘宝拖拽排序会 so easy,我们到后面再唠。</p> <p>为了RecyclerView添加头部和底部,网上很多可以说是各显神通千奇百怪,其实我们ListView就有addHeaderView(View view)方法。所以我也想用recyclerView.addHeaderView(),但是锤子发现并没有这个方法直接就报错,所以只好决定仿照Google的ListView的源码去写了。因为我们到后面还涉及到下拉刷新上拉加载问题,如果一次不搞好加班每天都加不过来怎么还有时间去重构。</p> <h3>3.基本实现</h3> <p>3.1 瞅瞅ListView的addHeaderView</p> <pre> <code class="language-java">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> <p>接下来我就挑一下关键代码,某些就省略了,强迫症自己去阅读源码吧</p> <pre> <code class="language-java">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; } }</code></pre> <p>其实关键源码也不多,还有英文注释不像上一次读源码完全没有注释,感觉那Google工程师有点打酱油。到这里也知道了,其实ListView并不是支持直接添加头部和底部,而是在内部写了一个包裹类,做了一系列的处理才可以,那么接下来我们也就参照这种方式,因为这估计是最权威的代码了就模仿你了。</p> <p>3.2 先构建WrapRecyclerAdapter</p> <pre> <code class="language-java">/** * 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; } }); } } }</code></pre> <p>接下来我们直接在上一期的列表基础上加两个头部和底部测试一下</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/0db91aa604b884559ed863c51e3ee00d.gif"></p> <p style="text-align:center">这里写图片描述</p> <p>那赶紧把轮播图加载进来吧,千万别。还有事情没做最忌讳的就是过度设计谁也看不懂一层套一层还有就是半吊子总感觉少了点什么,顺便说个题外话刚才群里有人说看看博客装起B来一套一套的,哈哈。</p> <p>3.3 先构建WrapRecyclerView</p> <p>我们最好还是模仿ListView的结构搞就搞到西,自定义一个WrapRecyclerView,可以添加删除头部和底部View,这个就比较简单了</p> <pre> <code class="language-java">/** * 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); } }; }</code></pre> <p>就不测试了,相信我是测试了没问题才贴的代码,还是那就话简简单单几行代码so easy,接下来直接整合轮播图。</p> <p>3.4 实战整合轮播图</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/91da3e055ceafcd92cfc32e7017d8448.gif" alt="RecyclerView更全解析之 - 为它优雅的添加头部和底部" width="515" height="893"></p> <p style="text-align:center">这里写图片描述</p> <p> </p> <p> </p> <p> </p>