之前我在GitHub上开源了一个可以实现RecyclerView列表分组的通用Adapter: GroupedRecyclerViewAdapter。也在博客上写了一篇专门介绍它的实现和使用的文章:《Android 可分组的RecyclerViewAdapter》。有一些朋友在看了我的博文和使用我的开源库后,会主动的给我反馈一些问题和提出一些建议。我很高兴,这使我觉得我所做的这些不仅可以提升自己,同时也能帮助到别人(虽然我很少写博客和开源东西)。前几天就有朋友向我反馈,说他不仅需要一个分组的列表,同时还希望能给列表添加头部和尾部。其实GroupedRecyclerViewAdapter的每个分组都是可以设置头部和尾部的。如果只是给一个普通的列表添加头部和尾部,只需要用GroupedRecyclerViewAdapter实现只有一个分组的列表就可以。但是他希望的是可以实现多个分组的同时,给整个大列表也设置头部和尾部,这样的需求是我以前从来没有想过的,但我还是给出了自己的建议:列表的第一个分组只要头部,不要尾部和子项,把它当做整个大列表的头部,尾部的实现也一样。这样也能实现他的需求,但就是处理逻辑复杂了一点。其实我在设计GroupedRecyclerViewAdapter的时候,为了能让它有更好的扩展性和能方便实现更多的复杂布局,所以给它的头部、尾部和子项都支持了多种类型的ViewTtype,有兴趣的朋友欢迎去看一下。
ListView添加头部和尾部的实现原理
在我们以前使用ListView的时候,ListView为我们提供了添加头部(addHeaderView())和尾部(addFooterView())的方法,让我们可以很方便的给列表添加头部和尾部,而且头部和尾部跟我们自己的ListView Adapter完全没有任何关系。那么ListView是如何做到的呢。让我们一起打开ListView的源码,看一下它是如何给列表添加头部(addHeaderView())的。(添加尾部的原理是一样的,这里就不单独说了)
public void addHeaderView(View v) {
addHeaderView(v, null, true);
}
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;
// 将ListView的Adapter包装成HeaderViewListAdapter。
if (mAdapter != null) {
if (!(mAdapter instanceof HeaderViewListAdapter)) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
}
if (mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
}
上面就是ListView添加头部的方法的源码,其中它会判断当前ListView的Adapter是不是HeaderViewListAdapter,如果不是,这把当前的Adapter包装成HeaderViewListAdapter。所以HeaderViewListAdapter就是实现ListView添加头部的关键,那么我们就去看一下HeaderViewListAdapter到底是个什么东西。
public class HeaderViewListAdapter implements WrapperListAdapter, Filterable{
}
HeaderViewListAdapter实现了WrapperListAdapter接口,而WrapperListAdapter接口是ListAdapter的子接口,所以HeaderViewListAdapter就是ListAdapter的一个实现类,跟普通的ListView Adapter没有太大区别。HeaderViewListAdapter接收一个普通的ListView Adapter和ListView头部列表和尾部列表,并且对它们统一管理。下面分析一下它的核心代码。
//构造方法:接收从外部传进来的头部列表信息和尾部列表信息,还有被包装的普通ListAdapter。
public HeaderViewListAdapter(ArrayList<ListView.FixedViewInfo> headerViewInfos,
ArrayList<ListView.FixedViewInfo> footerViewInfos,
ListAdapter adapter) {
}
//返回整个列表的item个数,其实就是普通Adapter的item个数加上头尾部的个数。
public int getCount() {
if (mAdapter != null) {
return getFootersCount() + getHeadersCount() + mAdapter.getCount();
} else {
return getFootersCount() + getHeadersCount();
}
}
/返回当前列表项的ViewType。
public int getItemViewType(int position) {
int numHeaders = getHeadersCount();
if (mAdapter != null && position >= numHeaders) {
int adjPosition = position - numHeaders;
int adapterCount = mAdapter.getCount();
//如果当前列表项是普通的列表项,则交由mAdapter处理。
//传给mAdapter的position需要除掉头尾部的处理。
if (adjPosition < adapterCount) {
return mAdapter.getItemViewType(adjPosition);
}
}
//返回当前列表项时头部或者尾部
return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
}
public View getView(int position, View convertView, ViewGroup parent) {
//如果当前列表项是头部,则返回对应的头部布局。
int numHeaders = getHeadersCount();
if (position < numHeaders) {
return mHeaderViewInfos.get(position).view;
}
//如果当前列表项是普通的列表项,则交由mAdapter处理。
//传给mAdapter的position需要除掉头尾部的处理。
final int adjPosition = position - numHeaders;
int adapterCount = 0;
if (mAdapter != null) {
adapterCount = mAdapter.getCount();
if (adjPosition < adapterCount) {
return mAdapter.getView(adjPosition, convertView, parent);
}
}
//如果当前列表项是尾部,则返回对应的尾部布局。
return mFooterViewInfos.get(adjPosition - adapterCount).view;
}
这就是HeaderViewListAdapter的核心代码,我们可以看到,它的代码非常的简单,就是在getCount()、getItemViewType()、getView()中做了一些处理。在getCount()中它返回的个数里加上了头部和尾部的个数。在getItemViewType()、getView()中,它判断如果当前列表项是头部或者尾部的时候自己处理,否则就交由被包装的普通Adapter处理。所以HeaderViewListAdapter的主要作用就是管理ListView的头部和尾部的。
HeaderViewListAdapter的设计非常的巧妙,只需要把我们设置给ListView的Adapter包装一下,就可以让我们的ListView具有了添加头部和尾部的功能,而且丝毫不会影响到我们原来的Adapter。甚至于我们根本就不知道在我们给ListView添加头部的时候,ListView已经将我们原来的Adapter包装成HeaderViewListAdapter,我们也无需关心他的实现逻辑。
根据HeaderViewListAdapter的设计思路,我们是不是也可以给我们的RecyclerView.Adapter实现一个包装类,只要对我们自己的Adapter包装一下,就可以让我们的列表具有了添加头部和尾部的功能呢?带着这样的想法,于是我就自己动手写了一个专门用来包装RecyclerView的Adapter的包装类:HeaderViewAdapter。
HeaderViewAdapter的代码实现
HeaderViewAdapter的设计思路和实现的效果跟HeaderViewListAdapter是完全一样的,在代码的实现上会有所不同,毕竟ListView的Adapter和RecyclerView的Adapter是完全不同的两个东西。
public class HeaderViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
//被包装的Adapter。
private RecyclerView.Adapter mAdapter;
//用于存放HeaderView
private final List<FixedViewInfo> mHeaderViewInfos = new ArrayList<>();
//用于存放FooterView
private final List<FixedViewInfo> mFooterViewInfos = new ArrayList<>();
//用于监听被包装的Adapter的数据变化的监听器。它将被包装的Adapter的数据变化映射成HeaderViewAdapter的变化。
private RecyclerView.AdapterDataObserver mObserver = new RecyclerView.AdapterDataObserver() {
//这里是具体的代码实现,因为篇幅的关系,在这里就不放出来了。
};
public HeaderViewAdapter(RecyclerView.Adapter adapter) {
this.mAdapter = adapter;
if (mAdapter != null) {
//注册mAdapter的数据变化监听
mAdapter.registerAdapterDataObserver(mObserver);
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 根据viewType查找对应的HeaderView 或 FooterView。如果没有找到则表示该viewType是普通的列表项。
View view = findViewForInfos(viewType);
if (view != null) {
return new ViewHolder(view);
} else {
//交由mAdapter处理。
return mAdapter.onCreateViewHolder(parent, viewType);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
// 如果是HeaderView 或者是 FooterView则不绑定数据。
// 因为HeaderView和FooterView是由外部传进来的,它们不由列表去更新。
if (isHeader(position) || isFooter(position)) {
return;
}
//将列表实际的position调整成mAdapter对应的position。
//交由mAdapter处理。
int adjPosition = position - getHeadersCount();
mAdapter.onBindViewHolder(holder, adjPosition);
}
@Override
public int getItemCount() {
return mHeaderViewInfos.size() + mFooterViewInfos.size()
+ (mAdapter == null ? 0 : mAdapter.getItemCount());
}
@Override
public int getItemViewType(int position) {
//如果当前item是HeaderView,则返回HeaderView对应的itemViewType。
if (isHeader(position)) {
return mHeaderViewInfos.get(position).itemViewType;
}
//如果当前item是HeaderView,则返回HeaderView对应的itemViewType。
if (isFooter(position)) {
return mFooterViewInfos.get(position - mHeaderViewInfos.size() - mAdapter.getItemCount()).itemViewType;
}
//将列表实际的position调整成mAdapter对应的position。
//交由mAdapter处理。
int adjPosition = position - getHeadersCount();
return mAdapter.getItemViewType(adjPosition);
}
/**
* 判断当前位置是否是头部View。
*
* @param position 这里的position是整个列表(包含HeaderView和FooterView)的position。
* @return
*/
public boolean isHeader(int position) {
return position < getHeadersCount();
}
/**
* 判断当前位置是否是尾部View。
*
* @param position 这里的position是整个列表(包含HeaderView和FooterView)的position。
* @return
*/
public boolean isFooter(int position) {
return getItemCount() - position <= getFootersCount();
}
/**
* 获取HeaderView的个数
*
* @return
*/
public int getHeadersCount() {
return mHeaderViewInfos.size();
}
/**
* 获取FooterView的个数
*
* @return
*/
public int getFootersCount() {
return mFooterViewInfos.size();
}
/**
* 添加HeaderView
*
* @param view
*/
public void addHeaderView(View view) {
addHeaderView(view, generateUniqueViewType());
}
private void addHeaderView(View view, int viewType) {
//包装HeaderView数据并添加到列表
FixedViewInfo info = new FixedViewInfo();
info.view = view;
info.itemViewType = viewType;
mHeaderViewInfos.add(info);
notifyDataSetChanged();
}
/**
* 添加FooterView
*
* @param view
*/
public void addFooterView(View view) {
addFooterView(view, generateUniqueViewType());
}
private void addFooterView(View view, int viewType) {
// 包装FooterView数据并添加到列表
FixedViewInfo info = new FixedViewInfo();
info.view = view;
info.itemViewType = viewType;
mFooterViewInfos.add(info);
notifyDataSetChanged();
}
/**
* 生成一个唯一的数,用于标识HeaderView或FooterView的type类型,并且保证类型不会重复。
*
* @return
*/
private int generateUniqueViewType() {
int count = getItemCount();
while (true) {
//生成一个随机数。
int viewType = (int) (Math.random() * Integer.MAX_VALUE) + 1;
//判断该viewType是否已使用。
boolean isExist = false;
for (int i = 0; i < count; i++) {
if (viewType == getItemViewType(i)) {
isExist = true;
break;
}
}
//判断该viewType还没被使用,则返回。否则进行下一次循环,重新生成随机数。
if (!isExist) {
return viewType;
}
}
}
/**
* 根据viewType查找对应的HeaderView 或 FooterView。没有找到则返回null。
*
* @param viewType 查找的viewType
* @return
*/
private View findViewForInfos(int viewType) {
for (FixedViewInfo info : mHeaderViewInfos) {
if (info.itemViewType == viewType) {
return info.view;
}
}
for (FixedViewInfo info : mFooterViewInfos) {
if (info.itemViewType == viewType) {
return info.view;
}
}
return null;
}
/**
* 用于包装HeaderView和FooterView的数据类
*/
private class FixedViewInfo {
//保存HeaderView或FooterView
View view;
//保存HeaderView或FooterView对应的viewType。
int itemViewType;
}
private static class ViewHolder extends RecyclerView.ViewHolder {
ViewHolder(View itemView) {
super(itemView);
}
}
}
为了让大家看代码方便一点,我删除了一些不是很重要的代码,而且把代码的实现细节讲解写到了每个方法的注释上,相信大家都能很容易的看懂。如果大家想看完整的代码,请移步我的GitHub。
HeaderViewAdapter的使用
我已经把HeaderViewAdapter和相关的类打包成一个引用库放到GitHub上,欢迎大家使用和star。
在ListView的设计上,对HeaderViewListAdapter的所有操作的是由ListView自己完成的。但是我们无法把对HeaderViewAdapter的操作交由RecyclerView来处理,所以需要自己对HeaderViewAdapter进行操作(包装、添加头部和尾部等),其实这也很简单,几句代码的事情而已。而且它可以适用于任何的RecyclerView,没有任何使用上的限制。
//需要包装的adapter
LinearAdapter adapter = new LinearAdapter(this);
//对adapter进行包装。
HeaderViewAdapter headerViewAdapter = new HeaderViewAdapter(adapter);
//添加HeaderView和FooterView
headerViewAdapter.addHeaderView(headerView);
headerViewAdapter.addFooterView(footerView);
//设置Adapter
recyclerView.setAdapter(headerViewAdapter);
无论我们的RecyclerView使用什么LayoutManager,HeaderViewAdapter都需要保证列表的头部和尾部能占满一行,否则布局就会很难看。使用LinearLayoutManager的时候不需要做特殊的处理,HeaderViewAdapter也已经帮我们处理了StaggeredGridLayoutManager的情况。至于GridLayoutManager的情况,我在HeaderViewAdapter的库里提供了一个HeaderViewGridLayoutManager的子类。所以大家在使用GridLayoutManager的时候,应该使用HeaderViewGridLayoutManager。
recyclerView.setLayoutManager(new HeaderViewGridLayoutManager(this, 2, headerViewAdapter));
为了让我们的RecyclerView添加头部和尾部的时候,更接近于ListView的体验。所以我在库里提供了一个RecyclerView子类:HeaderRecyclerView。HeaderRecyclerView封装了对HeaderViewAdapter的所以操作,这使我们只需要操作HeaderRecyclerView,而无需直接跟HeaderViewAdapter打交道,这使得我们使用HeaderRecyclerView的时候就如同以前使用ListView一样。
HeaderRecyclerView rvList = (HeaderRecyclerView) findViewById(R.id.rv_list);
//这是普通的adapter
GridAdapter adapter = new GridAdapter(this);
rvList.setLayoutManager(new GridLayoutManager(this, 2));
//直接设置普通的adapter,不需要直接进行包装。
rvList.setAdapter(adapter);
//添加HeaderView和FooterView。直接操作HeaderRecyclerView。
rvList.addHeaderView(headerView);
rvList.addFooterView(footerView);
效果图: