效果图
简述
这篇文章主要描述如何扩展原生的Adapter
,让其支持添加header、footer,以及item的事件处理,这将会是一篇很长的文章。
首先的考虑是我需要一个支持添加任意数量的header和footer,并且互相不干扰的Adapter
。主要表现在客户端的调用上数据是数据,header是header,footer是footer,而且他们的顺序关系也是很显然的。另外再将header和footer进行一下扩展,将给客户端使用的和库内部本身需要扩展的header、footer进行区分,这样就产生了下面这个顺序关系(已经在第一篇进行了阐述):
{
item_sys_header - item_header -
item_data -
item_footer - item_sys_footer
}
这个顺序以及每个域的数量都是很关键。
有了上面这个顺序的关系图,这时候数据结构也就定下来了:
protected List<View> headerViews = new ArrayList<>();
protected List<View> footerViews = new ArrayList<>();
//逻辑上设计为系统头部也可以是多个 ,但是实现上系统头部实现为仅有一个
private List<View> sysHeaderViews = new ArrayList<>();
private View sysFooterView;
//真实的数据部分
protected List<T> datas;
这里需要对上面的数据结构补充说明一下:
数据结构的访问权限是为了扩展用;系统header有计划进行扩展(声明中可以看到),但是目前来说只支持一个(将会在稍后的实现中看到);系统footer没有打算扩展(声明中可以看到),所以只支持一个。
既然有了上面这个关系图,那么接下来的思路就很明显了,我们按照这个顺序进行。
1. 系统头部
这部分对应到item_sys_header部分,起初的考虑是用于内部扩展用。
数据操作
首先目测需要这么几个方法对系统头部的操作
int getSysHeaderViewCount();
void setSysHeaderView(View view);
void removeSysHeaderView(View view);
这里的setSysHeaderView()
方法为什么用一个view参数而不是一个layoutId呢,我的考虑是如果用了id,那么内部就要去创建这个view以及viewHolder,这样客户端需要通过viewHolder去绑定数据,这样的操作不是我希望的。移除的操作removeSysHeaderView()
与设置操作相对应。
前面已经说了,系统头部在设计的时候是支持任意数量的,并且系统头部的是在最顶层的。所以调用设置方法时将会清除所有的系统header(这么做的目的是可能在某个版本中将会进行多个的扩充),然后才进行添加(这时候一定只有一个,索引就是0),接着是局部刷新(这些是Adapter
的常规操作)。下面就是相关代码:
final
public int getSysHeaderViewCount(){
return sysHeaderViews.size();
}
final
public void setSysHeaderView(View view) {
if (null == view) {
return;
}
int index = getSysHeaderViewCount();
if (index > 0) {
sysHeaderViews.clear();
notifyItemRangeRemoved(0, index);
}
sysHeaderViews.add(view);
notifyItemInserted(0);
}
对于移除操作就不太一样,因为面对的是多个,所以具体要删除哪一个需要先检索出在sysHeaderViews
中的位置index,然后进行相应索引位置的移除操作,接着还是局部刷新。下面就是相关代码:
final
public void removeSysHeaderView(View view) {
if (null == view || !sysHeaderViews.contains(view)) {
return;
}
int index = sysHeaderViews.indexOf(view);
sysHeaderViews.remove(view);
notifyItemRemoved(index);
}
onCreateViewHolder过程
系统头部数据的操作已经结束,我们再调用了刷新操作后,Adapter
将会处理一系列操作,首先我们看看onCreateViewHolder()
,这里将会根据viewType创建对应的viewHolder。在我们这个Adapter
的扩展中很明显是item的类型已经不止一种了,所以我们需要复写getItemViewType()
方法为系统头部关联一个类型。系统header可能很多,每一个都是单独的view对象,在创建viewHolder的时候也是需要区分的,因此每一个系统header都是一个单独的类型,所以我们这里采取了将view的hashCode作为与之对应的类型。
@Override
final
public int getItemViewType(int position) {
int shc = getSysHeaderViewCount();
//处理系统头部
if (shc > 0 && position < shc) {
return sysHeaderViews.get(position).hashCode();
}
return 0;
}
在onCreateViewHolder()
的过程中我们需要根据viewType创建viewHolder,首先检查viewType是否是系统header以及是哪一个系统header
@Override
final
public BaseViewHolder onCreateViewHolder(ViewGroup parent, final int viewType) {
//处理系统头部
View sysHeaderView = getSysFooterViewByHashCode(viewType);
if (null != sysHeaderView) {
return new BaseViewHolder(sysHeaderView);
}
return null;
}
private View getSysFooterViewByHashCode(int hashCode) {
return this.getViewByHashCodeFromList(sysHeaderViews, hashCode);
}
private View getViewByHashCodeFromList(List<View> views, int hashCode) {
if (null == views) {
return null;
}
for (View v : views) {
if (v.hashCode() == hashCode) {
return v;
}
}
return null;
}
onBindViewHolder过程
viewHolder创建完毕之后,按照流程就到了onBindViewHolder
进行数据的绑定操作,但是看看我们向客户端提供的设置方法,传入的是一个view,这就意味着系统header数据的绑定操作是在客户端进行的,换句话说就是在这里针对系统header没有要数据绑定的操作(传入的position在系统头部范围内不做任何处理)。那么代码就是下面这个样子:
@Override
final
public void onBindViewHolder(BaseViewHolder holder, int position) {
int shc = getSysHeaderViewCount();
//item_sys_header
if (0 != shc && position < shc) {
return;
}
}
到这里,系统header的处理部分就结束了,这时候已经可以添加和删除一个系统header了。这里可能有的同学就会问了,最重要的三个方法为什么都加了final,这样不是都没法操作了吗,至于为什么要这样做,将会在稍后慢慢说明。
2. 用户头部
这部分对应到item_header部分,是提供给客户端使用的。
流程仍然按照上面系统header部分。
数据操作
首先对于这部分功能提供一下部分几个方法进行头部的操作
int getHeaderViewCount();
void addHeaderView(View view);
void removeHeaderView(View view);
以上参数的说明与系统header一致,这里不再赘述。
这部分在按顺序在系统header(item_sys_header)后面,所以在数据操作上需要考虑item_sys_header部分。下面是具体的操作代码:
final
public int getHeaderViewCount() {
return headerViews.size();
}
final
public void addHeaderView(View view) {
if (null != view && headerViews.contains(view)) {
headerViews.remove(view);
}
headerViews.add(view);
int shc = getSysHeaderViewCount();
int index = shc + headerViews.indexOf(view);
notifyItemInserted(index);
}
final
public void removeHeaderView(View view) {
if (null == view || !headerViews.contains(view)) {
return;
}
int shc = getSysHeaderViewCount();
int index = shc + headerViews.indexOf(view);
headerViews.remove(view);
notifyItemRemoved(index);
}
onCreateViewHolder过程
这部分仍然与系统header一样,item类型也是view对应的hashCode,这里对getItemViewType()
和onCreateViewHolder()
进行完善,添加对用户header的支持。代码如下:
@Override
final
public int getItemViewType(int position) {
int shc = getSysHeaderViewCount();
int hc = getHeaderViewCount();
int hAll = shc + hc;
//处理系统头部
if (shc > 0 && position < shc) {
return sysHeaderViews.get(position).hashCode();
}
//处理头部
if (hc > 0 && position >= shc && position < hAll) {
position = position - shc;
return headerViews.get(position).hashCode();
}
return 0;
}
@Override
final
public BaseViewHolder onCreateViewHolder(ViewGroup parent, final int viewType) {
//处理系统头部
View sysHeaderView = getSysFooterViewByHashCode(viewType);
if (null != sysHeaderView) {
return new BaseViewHolder(sysHeaderView);
}
//处理头部
View headerView = getHeaderViewByHashCode(viewType);
if (null != headerView) {
return new BaseViewHolder(headerView);
}
return null;
}
onBindViewHolder过程
用户header的数据绑定处理仍然与处理系统header一致,增加了用户header处理的代码如下:
@Override
final
public void onBindViewHolder(BaseViewHolder holder, int position) {
int shc = getSysHeaderViewCount();
//item_sys_header
if (0 != shc && position < shc) {
return;
}
int hc = getHeaderViewCount();
int hAll = shc + hc;
//处理用户header
if (0 != hc && position < hAll) {
return;
}
}
3. 正常的数据部分
这部分是除header、footer外客户端处理的真实数据部分。
对于这部分的实现有以下几方面的考虑:
多item类型支持
这个扩展库一定是支持多种item类型的,但是对于header和footer的类型已经在内部做了处理(本应该是这样),从上面来看多类型的支持的重载方法被设置为final,所以需要额外提供一个方法来只针对数据部分多item类型的支持。在安全起见和必要性方面将getItemViewType()
方法做了屏蔽。额外增加的方法如下:
/**
* 初始化数据域类型,总是从0开始,已经除去头部
* @param positionOffsetHeaders
* @return
*/
protected int mapDataSectionItemViewTypeToItemLayoutId(int positionOffsetHeaders) {
return 0;
}
从方法名来看,除了header和footer外的数据域部分item类型对应的都是item的布局文件id,这样在onCreateViewHolder()
内部封装是有好处的,再一次说明viewHolder的创建对于客户端直接使用api是屏蔽的。
onCreateViewHolder
如果就单一般的展示用,那么这个方法也可以被屏蔽了,但是为了更好的扩展(在后面的滑动菜单以及粘性头部时),还是向上面一样再提供一个下面这样的方法供子类使用:
abstract
public BaseViewHolder onCreateHolder(ViewGroup parent, int viewType);
onBindViewHolder
同上面一样,我们需要保护非数据域的逻辑,但是数据的绑定操作一定是客户端来做的,所以我们还是需要额外提供一个下面这样的方法供客户端使用:
/**
* 绑定数据
* @param holder
* @param position 数据域索引从0开始,已经除去头部
*/
abstract
public void convert(BaseViewHolder holder, int position);
仅在这篇文章中提及的功能而言,对于onBindViewHolder()
的操作我们只是又提取了一个convert()方法,但是如果翻看源代码的话会发现对这块的提取操作是下面这样的(在粘性头部的扩展中会找到原因):
/**
* 为了子类扩展
* @param holder
* @param position
*/
@CallSuper
protected void onBindHolder(BaseViewHolder holder, int position) {
this.convert(holder, position);
}
/**
* 绑定数据
* @param holder
* @param position 数据域索引从0开始,已经除去头部
*/
abstract
public void convert(BaseViewHolder holder, int position);
经过上面的篇幅阐述,已经简单描述了header的添加删除以及数据域部分的处理,屏蔽了部分非必要方法以及为进一步扩展等额外添加了一些处理方法,这么做的目的就是为了简单、易扩展。
大概总结一下就是:
- 多类型的支持分为两类:数据部分映射为item布局id;非数据部分映射为view的hashCode
- viewHolder创建分为两种情况:对于客户端使用直接屏;对于扩展则有间接的映射方法
- 数据绑定分为两类:数据域对于客户端有必须实现的间接映射方法;非数据域则直接屏蔽
相信看到这里已经知道footer怎么加进去了,与header和数据域都是类似的,主要抓住一点就是正确的计算索引,具体的就不在往下阐述了,可以去翻看源代码。
4. 为数据域的item添加事件
我们知道RecyclerView
对于viewHolder有特殊的要求,必须是RecyclerView.ViewHolder
的子类才行,而它内部有个itemView属性,指的就是我们创建的item根视图,有了这个万事已经具备。还记得代码是这样子写的:
@Override
public BaseViewHolder onCreateHolder(ViewGroup parent, int viewType) {
View itemView = inflater.inflate(viewType, parent, false);
return new BaseViewHolder(itemView);
}
我们仿照ListView添加以下两种事件。
/**
* 点击事件
*/
public interface OnItemClickListener {
void onItemClick(View itemView, int position);
}
/**
* 长按事件
*/
public interface OnItemLongClickListener {
void onItemLongClick(View itemView, int position);
}
接下来只需要在onCreateViewHolder()
里面添加事件回调就可以了,这里需要说明两点:1. 写这么复杂的原因是还是为了扩展 2. 计算正确的索引
@Override
final
public BaseViewHolder onCreateViewHolder(ViewGroup parent, final int viewType) {
//省略其他操作...
//事件只针对正常数据项
final BaseViewHolder holder = onCreateHolder(parent, viewType);
this.initItemListener(holder/*, viewType*/);
return holder;
}
protected void initItemListener(final BaseViewHolder holder/*, final int viewType*/){
if (null == holder) {
return;
}
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
HeaderFooterAdapter.this.onItemClick(holder, v);
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return HeaderFooterAdapter.this.onItemLongClick(holder, v);
}
});
}
protected void onItemClick(final BaseViewHolder holder, View view){
if (null == onItemClickListener) {
return;
}
int hAll = getHeaderViewCount() + getSysHeaderViewCount();
onItemClickListener.onItemClick(view, holder.getAdapterPosition() - hAll);
}
protected boolean onItemLongClick(final BaseViewHolder holder, View view){
if (null == onItemLongClickListener) {
return false;
}
int hAll = getHeaderViewCount() + getSysHeaderViewCount();
onItemLongClickListener.onItemLongClick(view, holder.getAdapterPosition() - hAll);
return true;
}
5. 支持GridLayoutManager与StaggeredGridLayoutManager
写到这里我们扩展的Adapter
已经支持header、footer的增删操作、更加简化的api处理以及为数据域添加事件监听响应。
写一个demo测试后发现一切正常,但是这仅限于LayoutManager
为LinearLayoutManager
的情况,其他情况会发现是失效的状态。我们需要的是在任何情况下我们加进去的header和footer一定要是横向填充的,显然还需要处理。
发现Adapter
中并没有提供相关可以使用的方法,其实这种布局的处理是交给LayoutManager
处理的,所以我们需要从LayoutManager
着手处理这些问题,最后发现不同的LayoutManager
处理的方式不同,那么我们需要分别对待。首先明确一点LayoutManager
是设置在RecyclerView
而不是Adapter
,所以第一点我们需要在Adapter
中获取RecyclerView
对象。
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
if (this.recyclerView == recyclerView) {
return;
}
this.recyclerView = recyclerView;
}
@Override
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
super.onDetachedFromRecyclerView(recyclerView);
this.recyclerView = null;
}
获取到RecyclerView
也就获取到了设置的LayoutManager
对象,这样我们就可以针对不同的LayoutManager
进行处理。在处理之前,首先有以下两点需要考虑:
- 除了数据域(header和footer)一定是横跨的,并且这部分处理需要内部搞定且对外屏蔽
- 客户端设置的数据域有可能有些item也是需要横跨的,这时候我们需要额外提供一个方法供客户端使用。很简单方法,如下:
/**
* 设置是否横跨
* @param position
* @return
*/
protected boolean isFullSpanWithItemView(int position) {
return false;
}
所以我们就有了以下的代码来分别针对GridLayoutManager
和StaggeredGridLayoutManager
进行处理。
private void adapterGridLayoutManager() {
final RecyclerView.LayoutManager layoutManager = null == recyclerView ? null : recyclerView.getLayoutManager();
if (null == layoutManager) {
return;
}
if (layoutManager instanceof GridLayoutManager) {
final GridLayoutManager glm = (GridLayoutManager) layoutManager;
final GridLayoutManager.SpanSizeLookup ssl = glm.getSpanSizeLookup();
glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return !isDataItemView(position) ? glm.getSpanCount() : ssl.getSpanSize(position);
}
});
}}
private void adapterStaggeredGridLayoutManager(BaseViewHolder holder) {
final RecyclerView.LayoutManager layoutManager = null == recyclerView ? null : recyclerView.getLayoutManager();
if (null == layoutManager) {
return;
}
if (layoutManager instanceof StaggeredGridLayoutManager) {
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
int position = holder.getAdapterPosition();
if (null != lp && lp instanceof StaggeredGridLayoutManager.LayoutParams && !isDataItemView(position)) {
((StaggeredGridLayoutManager.LayoutParams) lp).setFullSpan(true);
}
}
}
/**
* 用来判断item是否为真实数据项,除了头部、尾部、系统尾部等非真实数据项,结构为: * item_header - item_data - item_footer - item_sys_footer
* @param position
* @return true:将保留LayoutManager的设置 false:该item将会横跨整行(对GridLayoutManager,StaggeredLayoutManager将很有用)
*/
private boolean isDataItemView(int position) {
int shc = getSysHeaderViewCount();
int hc = shc + getHeaderViewCount();
int dc = getDataSectionItemCount();
boolean isHeaderOrFooter = position >= 0 && position >= hc && position < (hc + dc);
if (!isHeaderOrFooter) {
return isHeaderOrFooter;
}
return this.isFullSpanWithItemView(position - (shc + hc));
}
最后将以上处理加进去,注意一下两种方式在注册的位置不太一样:
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
if (this.recyclerView == recyclerView) {
return;
}
this.recyclerView = recyclerView;
this.adapterGridLayoutManager();
}
@Override
public void onViewAttachedToWindow(BaseViewHolder holder) {
super.onViewAttachedToWindow(holder);
this.adapterStaggeredGridLayoutManager(holder);
}
到这里,为Adapter
添加header、footer、事件以及适配LayoutManager
就结束了,下一篇将阐述下数据域的操作。