前面已经说过RecyclerView的使用,RecyclerView通过其高度的可定制性深受大家的青睐,也有非常多的使用者开始对它进行封装或者改造,从而满足越来越多的需求,相信用过的一定会爱不释手。
在实际开发中经常会遇到给列表添加标题这样的需求,可是要给RecyclerView加个headerView或者footerView却发现没有这样的方法,怎么办呢?网上搜到的大多数方案都是通过控制Adapter的ItemType来设置的,不管是添加headerView还是footerView,它们都是Item的一种,只不过显示在特定的位置,那么我们完全可以通过为其设置ItemType来完成。所以核心就是根据不同的ItemType去加载不同的布局。
有了思路,我们就简单实现下吧
源代码
参考:
适配器代码如下:
public class HeaderAndFooterAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int ITEM_TYPE_HEADER = 1; private static final int ITEM_TYPE_FOOTER = 2; private Context mContext; private List<String> mDatas = new ArrayList<>(); public HeaderAndFooterAdapter(Context mContext, List<String> mDatas) { this.mDatas = mDatas; this.mContext = mContext; } /*根据getItemViewType()方法返回的不同类型创建不同的ViewHolder*/ @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == ITEM_TYPE_HEADER) { View view = LayoutInflater.from(mContext).inflate(android.R.layout.simple_list_item_1, parent, false); HeaderHolder viewHolder = new HeaderHolder(view); return viewHolder; } else if (viewType == ITEM_TYPE_FOOTER) { View view = LayoutInflater.from(mContext).inflate(android.R.layout.simple_list_item_1, parent, false); FootHolder viewHolder = new FootHolder(view); return viewHolder; } else { View view = LayoutInflater.from(mContext).inflate(R.layout.item_recyclerview_main, parent, false); ItemHolder viewHolder = new ItemHolder(view); return viewHolder; } } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (holder instanceof HeaderHolder) { HeaderHolder headerHolder = (HeaderHolder) holder; headerHolder.textViewHeader.setText(mDatas.get(position)); } else if (holder instanceof FootHolder) { FootHolder footHolder = (FootHolder) holder; footHolder.textViewFoot.setText(mDatas.get(position)); } else { ItemHolder itemHolder = (ItemHolder) holder; itemHolder.textViewItem.setText(mDatas.get(position)); } } @Override public int getItemCount() { return mDatas == null ? 0 : mDatas.size(); } /*根据位置来返回不同的item类型*/ @Override public int getItemViewType(int position) { if (position == 0) { return ITEM_TYPE_HEADER; } else if (position + 1 == getItemCount()) { return ITEM_TYPE_FOOTER; } else return 0; } /*头部Item*/ class HeaderHolder extends RecyclerView.ViewHolder { public TextView textViewHeader; public HeaderHolder(View itemView) { super(itemView); textViewHeader = (TextView) itemView.findViewById(android.R.id.text1); } } /*底部Item*/ class FootHolder extends RecyclerView.ViewHolder { public TextView textViewFoot; public FootHolder(View itemView) { super(itemView); textViewFoot = (TextView) itemView.findViewById(android.R.id.text1); } } class ItemHolder extends RecyclerView.ViewHolder { public TextView textViewItem; public ItemHolder(View itemView) { super(itemView); textViewItem = (TextView) itemView.findViewById(R.id.tv_main_item); } } }
-
getItemViewType
由于我们增加了headerView和footerView首先需要复写的就是getItemCount和getItemViewType。getItemCount很好理解,对于getItemType,返回的就是item所代表的type值。
-
onCreateViewHolder
可以看到,我们分别判断viewType,如果是headerView或者是footView,我们则为其单独创建ViewHolder。
-
onBindViewHolder
onBindViewHolder比较简单,只要根据不同的ViewHolder来进行绑定数据即可。
public class MainActivity extends AppCompatActivity { private RecyclerView mRecyclerView; private List<String> mDatas = new ArrayList<>(); private HeaderAndFooterAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mRecyclerView = (RecyclerView) findViewById(R.id.ryv_main_content); LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); mRecyclerView.setLayoutManager(linearLayoutManager); mAdapter=new HeaderAndFooterAdapter(this,mDatas); mRecyclerView.setAdapter(mAdapter); initData(); mAdapter.notifyDataSetChanged(); } private void initData() { mDatas.add("列表头"); for (int i=0;i<20;i++){ mDatas.add("item"+i); } mDatas.add("列表尾"); } }
效果如下:
好了,现在问题来了,假设我们现在已经完成了RecyclerView的编写,忽然有个需求,需要在列表上加个HeaderView,此时我们该怎么办呢?打开我们的Adapter,然后按照我们上述的原理,添加特殊的ViewType,然后修改代码完成。这是比较常规的做法了,但是有个问题是,如果需要添加多个viewType,那么可能我们的Adapter需要修改的幅度就比较大了,比如getItemType、getItemCount、onBindViewHolder、onCreateViewHolder等,几乎所有的方法都要进行改变。如果一个项目中多个RecyclerView都需要在其列表中添加headerView,想想都头大了,所以直接改Adapter的代码是非常不划算的,最好能够设计一个类,可以无缝的为原有的Adapter添加headerView和footerView。我也是看到了鸿洋大神的这篇文章才有了收获,思路是通过类似装饰者模式,去设计一个类,增强原有Adapter的功能,使其支持addHeaderView和addFooterView。这样我们就可以不去改动我们之前已经完成的代码,灵活的去扩展功能了。幸好大学的课本没丢,又翻出出来,重新看看装饰者模式,哈哈~~
初步实现
装饰类
public class HeaderAndFooterWrapper<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int BASE_ITEM_TYPE_HEADER = 0x10; private static final int BASE_ITEM_TYPE_FOOTER = 0x20; private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>(); private SparseArrayCompat<View> mFootViews = new SparseArrayCompat<>(); private RecyclerView.Adapter mInnerAdapter; public HeaderAndFooterWrapper(RecyclerView.Adapter adapter) { mInnerAdapter = adapter; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (mHeaderViews.get(viewType) != null) { ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mHeaderViews.get(viewType)); return holder; } else if (mFootViews.get(viewType) != null) { ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mFootViews.get(viewType)); return holder; } return mInnerAdapter.onCreateViewHolder(parent, viewType); } @Override public int getItemViewType(int position) { if (isHeaderViewPos(position)) { return mHeaderViews.keyAt(position); } else if (isFooterViewPos(position)) { return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount()); } return mInnerAdapter.getItemViewType(position - getHeadersCount()); } private int getRealItemCount() { return mInnerAdapter.getItemCount(); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (isHeaderViewPos(position)) { return; } if (isFooterViewPos(position)) { return; } mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount()); } @Override public int getItemCount() { return getHeadersCount() + getFootersCount() + getRealItemCount(); } private boolean isHeaderViewPos(int position) { return position < getHeadersCount(); } private boolean isFooterViewPos(int position) { return position >= getHeadersCount() + getRealItemCount(); } public void addHeaderView(View view) { mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view); } public void addFootView(View view) { mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view); } public int getHeadersCount() { return mHeaderViews.size(); } public int getFootersCount() { return mFootViews.size(); } }
注意这几个方法:
-
getItemViewType
可以看到我们的返回值是mHeaderViews.keyAt(position),这个值其实就是我们 addHeaderView时的key,footerView是一样的处理方式,也就是说每一个headerView都会创建不同的itemType。
-
onCreateViewHolder
可以看到,我们分别判断viewType,如果是HeaderView或者是FooterView,我们则为其单独创建ViewHolder,这里的 ViewHolder我就直接拿鸿洋大神的用了,大家也可以自己写,只需要将对应的 headerView作为itemView传入ViewHolder的构造即可。而这里也就体现出SparseArrayCompat的作用了,对于headerView假设我们有多个,那么onCreateViewHolder返回的ViewHolder中的itemView应该对应不同的headerView,如果是List,那么不同的headerView应该对应着:list.get(0),list.get(1)等。但是onCreateViewHolder方法并没有position参数,只有itemType参数,如果itemType还是固定的一个值,那么你是没有办法根据参数得到不同的headerView的。
所以,我利用SparseArrayCompat,将其itemType作为key,value为我们的headerView,在 onCreateViewHolder中,直接通过itemType,即可获得我们的headerView,然后构造ViewHolder对象。
效果:
效果差不多,我们再来看看GridLayoutMananger的效果:
虽然效果不是我们想要的,但逻辑是正确的,好了,现在改下代码吧
针对GridLayoutManager
在HeaderAndFooterWrapper类中复写onAttachedToRecyclerView方法
@Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { mInnerAdapter.onAttachedToRecyclerView(recyclerView); RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); if (layoutManager instanceof GridLayoutManager) { final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup(); gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { int viewType = getItemViewType(position); if (mHeaderViews.get(viewType) != null) { return gridLayoutManager.getSpanCount(); } else if (mFootViews.get(viewType) != null) { return gridLayoutManager.getSpanCount(); } if (spanSizeLookup != null) return spanSizeLookup.getSpanSize(position); return 1; } }); gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount()); } }
现在看一下运行效果:
对于StaggeredGridLayoutManager
当然了,我们怎么能忘了最重要的StaggeredGridLayoutManager呢 ?StaggeredGridLayoutManager并没有setSpanSizeLookup这样的方法,但是处理依然不复杂,重写onViewAttachedToWindow方法,如下:
@Override public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) { mInnerAdapter.onViewAttachedToWindow(holder); int position = holder.getLayoutPosition(); if (isHeaderViewPos(position) || isFooterViewPos(position)) { ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams) { StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp; p.setFullSpan(true); } } }
哦了,就是这样
参考: