前言:
在使用RecyclerView替换之前常用的ListView开发的时候,我们会发现一个问题,RecyclerView中没有提供给我们添加头部尾部的方法,那么我们就可以参考ListView的实现方式来为RecyclerView扩展,使其支持添加头部和添加尾部。
一、 最终效果
我们希望RecyclerView提供如下两个方法,addHeaderView(View view); addFooterView(View view);先来瞄一眼最终的效果:
二、 ListView实现方案
既然我们是看ListView提供了这个方法才决定对RecyclerView进行改造的,那么ListView中是怎么处理的呢?
public void addHeaderView(View v) {
addHeaderView(v, null, true);
}
在addHeaderView(View v)方法中调用了一个三参数的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;
// 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();
}
}
}
可以看到这里使用了一个叫 HeaderViewListAdapter的包装类,并且使用了一个FixedViewInfo的内部类来保存添加的信息。既然是一个ListView的Adapter那我们自然最关心的就是该Adapter中的getCount()、getView()等方法:
public int getCount() {
if (mAdapter != null) {
return getFootersCount() + getHeadersCount() + mAdapter.getCount();
} else {
return getFootersCount() + getHeadersCount();
}
}
public View getView(int position, View convertView, ViewGroup parent) {
// Header (negative positions will throw an IndexOutOfBoundsException)
int numHeaders = getHeadersCount();
if (position < numHeaders) {
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;
}
通过以上简单的分析,我们基本就知道了ListView是如何实现添加头部和添加尾部的,就是给ListView的Adapter使用了一个包装类,对头部和尾部条目做了处理,其余还是使用的被包装类的方法。
三、WrapAdapter包装类编写
既然了解了ListView添加头部和尾部的实现方案,那么我们就按照该方案来实现自己的Recycler.Adapter的包装类吧。
我们知道ListView的Adapter中我们通常关系的是getCount(),getView()等方法,那么Recycler.Adapter中我们关心哪些方法呢?没错就是onCreateViewHolder()、onBindViewHolder()、getItemCount()、getItemViewType(),那么只需要对这几个方法包装下就好了。
1. 添加View描述类创建
把ListView中描述头部尾部的FixedViewInfo类拿过来稍作修改变为我们的描述类;
/**
* A class that represents a fixed view in a list, for example a header at the top
* or a footer at the bottom.
*/
public class FixedViewInfo {
/** The view to add to the list */
public View view;
/** The data backing the view. This is returned from {RecyclerView.Adapter#getItemViewType(int)}. */
public int viewType;
}
2. 创建存储头部尾部集合
private ArrayList mHeaderViewInfos = new ArrayList<>();
private ArrayList mFooterViewInfos = new ArrayList<>();
3. 修改getItemCount()方法
@Override
public int getItemCount() {
return mHeaderViewInfos.size() + mRealAdapter.getItemCount() + mFooterViewInfos.size();
}
比较简单,只需要把真正的AdapterView的个数加上头部和尾部的个数就可以啦。
4. 修改getItemViewType()方法
@Override
public int getItemViewType(int position) {
if (isHeaderPosition(position)) {
return mHeaderViewInfos.get(position).viewType;
} else if (isFooterPosition(position)) {
return mFooterViewInfos.get(position - mHeaderViewInfos.size()
- mRealAdapter.getItemCount()).viewType;
} else {
return mRealAdapter.getItemViewType(position - mHeaderViewInfos.size());
}
}
这里只需要对头部和尾部做相应处理即可,那么怎么判断是头部或者尾部呢?对头,就是通过该方法传入的position参数来判断的:
private boolean isHeaderPosition(int position) {
return position < mHeaderViewInfos.size();
}
private boolean isFooterPosition(int position) {
return position >= mHeaderViewInfos.size() + mRealAdapter.getItemCount();
}
5. 修改onCreateViewHolder()方法
把两个软柿子挑出来搞完之后我们还是要面对比较复杂的onCreateViewHolder(),这里我们也和getItemViewType类似,只有针对头部和尾部处理即可。
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
if (isHeader(viewType)) {
int whichHeader = Math.abs(viewType - BASE_HEADER_VIEW_TYPE);
View headerView = mHeaderViewInfos.get(whichHeader).view;
return createHeaderFooterViewHolder(headerView);
} else if (isFooter(viewType)) {
int whichFooter = Math.abs(viewType - BASE_FOOTER_VIEW_TYPE);
View footerView = mFooterViewInfos.get(whichFooter).view;
return createHeaderFooterViewHolder(footerView);
} else {
return mRealAdapter.onCreateViewHolder(viewGroup, viewType);
}
}
这里把创建头部和尾部ViewHolder封装到一个createHeaderFooterViewHolder()方法中:
private RecyclerView.ViewHolder createHeaderFooterViewHolder(View view) {
if (isStaggeredGrid) {
StaggeredGridLayoutManager.LayoutParams params = new StaggeredGridLayoutManager.LayoutParams(
StaggeredGridLayoutManager.LayoutParams.MATCH_PARENT, StaggeredGridLayoutManager.LayoutParams.WRAP_CONTENT);
params.setFullSpan(true);
view.setLayoutParams(params);
}
return new RecyclerView.ViewHolder(view) {
};
}
这里为什么判断 isStaggeredGrid我们稍后再讲解,除了这一点外,还是比较简单的,创建了一个Recycler.ViewHolder。
6. 修改onBindViewHolder()方法
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if (position < mHeaderViewInfos.size()) {
// Headers don't need anything special
} else if (position < mHeaderViewInfos.size() + mRealAdapter.getItemCount()) {
// This is a real position, not a header or footer. Bind it.
mRealAdapter.onBindViewHolder(viewHolder, position - mHeaderViewInfos.size());
} else {
// Footers don't need anything special
}
}
我们只需要处理真正的item数据即可,头部和尾部不做处理。
OK,那我们就修改好了,那么来运行一把吧。
什么鬼,怎么添加的头部不是占据一行呢?就是要在我们跳过的createHeaderFooterViewHolder()中isStaggeredGrid来处理的,就是设置如果配置的为StaggeredGridLayoutManager则要占据一行,但是还有一个问题那就是如果是GridLayoutManager也是要占据一行的,这里提供了一个方法来处理占据一行的问题:
/**
* adjust the LayoutManager SpanSize
*
* @param recycler
* @version 1.0
*/
public void adjustSpanSize(RecyclerView recycler) {
if(recycler.getLayoutManager() instanceof GridLayoutManager) {
final GridLayoutManager layoutManager = (GridLayoutManager) recycler.getLayoutManager();
layoutManager.setSpanSizeLookup(new SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
boolean isHeaderOrFooter =
isHeaderPosition(position) || isFooterPosition(position);
return isHeaderOrFooter ? layoutManager.getSpanCount() : 1;
}
});
}
if(recycler.getLayoutManager() instanceof StaggeredGridLayoutManager) {
this.isStaggeredGrid = true;
}
}
这么一来,无论是StaggeredGridLayoutManager还是GridLayoutManager添加的头部和尾部都会占据一行啦。
四、WrapRecyclerView包装类编写
其实第三步骤,我们就完成了封装,但是为了使用方便,这里再提供一个对RecyclerView的封装,其实比较简单,主要的在setAdapter中,其他的都是对WrapAdapter的简单代理。
@Override
public void setAdapter(Adapter adapter) {
if(adapter instanceof WrapAdapter) {
mWrapAdapter = (WrapAdapter) adapter;
super.setAdapter(adapter);
} else {
mWrapAdapter = new WrapAdapter(adapter);
super.setAdapter(mWrapAdapter);
}
if(shouldAdjustSpanSize) {
mWrapAdapter.adjustSpanSize(this);
}
}
/**
* Adds a header view
*
* @param view
* @version 1.0
*/
public void addHeaderView(View view) {
if (null == view) {
throw new IllegalArgumentException("the view to add must not be null!");
} else if(mWrapAdapter == null) {
throw new IllegalStateException("You must set a adapter before!");
} else {
mWrapAdapter.addHeaderView(view);
}
}
/**
* Adds a footer view
*
* @param view
* @version 1.0
*/
public void addFooterView(View view) {
if (null == view) {
throw new IllegalArgumentException("the view to add must not be null!");
} else if(mWrapAdapter == null) {
throw new IllegalStateException("You must set a adapter before!");
} else {
mWrapAdapter.addFooterView(view);
}
}