概述
RecycleViewUtil
是新增的一个主要针对RecycleView的一个工具类.该工具类中提供了部分RecycleView可能会使用到的方法,当中也包含了一些用来增强HeaderRecycleAdapter
功能的扩展方法.
通过该工具类也能够非常easy在普通的adapter中实现相应的相关扩展功能.
可实现的功能有:
- 任意调整RecycleViewAdapter中的item数量
- 可依据RecycleView已确定的某一边长自己主动调整item数量以填充整个RecycleView(并不再有多余的item)
- 可依据RecycleView已确定的某一边长任意指定须要填充整个RecycleView的item的数量
对于实现的功能假设认为有点不理解,以下会有更具体的分析.
调整任意数量的item数量
使用过RecycleView的都知道数据部分都是由adapter进行负责的,而item的数量取决于数据量的大小,所以当须要更改item数量时,势必通过调整数据量的大小进行更改.
使用这个原理进行工作的包含给RecycleView加入header/footer等.
- [x] 扩展adapter,支持加入头尾部的ExtraViewWrapAdapter
- [x] 支持分组显示并为每一个分组加入一个header的HeaderRecycleaAdapter
而这里调整的方式与之不同的是:
- 数据全然不须要有不论什么的变化,而是支持对RecycleView中的某个属性进行更改从而实现的调整.
- 该调整方式是通过反射进行的,不须要对控件进行不论什么继承与改动
关于这个实现的作用最后会给出一个样例參考,在某些情况下还是有实际的需求的.
RecycleView中实际控制item数量的变量
依据之前我们已知的RecycleView的item数量由adapter.getItemCount()
决定,所以直接查找ReccyleView中哪些地方调用了这种方法就能够得到保存item数量的值在哪里了.
//搜索adapter.getItemCount()得到多个方法都有涉及
//分发layout
void dispatchLayout()
//验证viewholder的位置,应该是用于缓存重用部分
boolean validateViewHolderForOffsetPosition(ViewHolder holder)
//測量工作中
void onMeasure(int widthSpec, int heightSpec)
而以上的方法中都是将adapter的itemCount赋值给一个内部成员变量mState
,所以我们简单看看这个State
类型是什么.因为State
尽管不大,可是总代码量也不少,所以仅仅给出我们关注的部分
public static class State {
/**
* Number of items adapter has.
*/
int mItemCount = 0;
}
这里能够看到也有凝视,确实mState.mItemCount
是我们我们要找的用于存放itemCount的变量,找到了首先当然看一下它能不能直接赋值或者是处理;非常遗憾的是,它对外并不公开(这也是非常好理解的,为了安全问题),所以我们须要对其进行改动就仅仅能是通过反射的方式进行了.
在进行反射尝试改动之前,我们先看一下RecycleView中的辅助类ItemDecoration
.看到这个类应该非常多人都用过,对RecycleView的分隔线加入什么的都会用这个类进行操作.那么这个类中须要子类重写的方法有几个.
//绘制界面(分隔线等),此部分绘制后item再进行绘制
public void onDraw(Canvas c, RecyclerView parent, State state);
//绘制界面,此部分在全部item绘制会再进行绘制
public void onDrawOver(Canvas c, RecyclerView parent, State state)
关于以上的相关的想进一步了解能够查看一下StickItemDecoration,在这个篇文章中也细致说明了怎样绘制固定头部.
这里能够看到事实上參数中就有State
,而在其state.getItemCount()
方法中更是有说明
Returns the total number of items that can be laid out. Note that this number is not necessarily equal to the number of items in the adapter, so you should always use this number for your position calculations and never access the adapter directly.
返回能被布局的总的item数量.请注意此值不一定与adapter.itemCount相等,因此在计算位置时应该总是使用此值而不是直接使用adapter.
从这里能够看出,state.mItemCount
才是真正终于决定RecycleView的item数量.
反射实现改动数据
关于反射实现state.mItemCount
字段的数据改动就不具体说明了,看一下源代码或者知道反射怎么用的就明白了,仅仅是逻辑上须要处理一些可能出现的问题或者异常而已
自己定义adapter用法
对于想将此方法使用于自己定义adapter中,那么前面分析了应该在在adapter.getItemCount()
中设置最好了.那么怎么使用呢?以下是给出的一个样例.
样例中做了非常多的安全性措施或者是排除性措施,代码会比較多,建议耐心点看完,关键部分都给出了凝视,大致流程分为几部分
- 推断当前调整值(adjustCount)是否在有效范围内(不超过数据源的最大值mCount,超过没意义)
- 推断adjustCount与上一次调整后的结果(mLastAdjustCount)是否同样,同样则不须要又一次设置
- 调用
RecycleViewUtil.setRecyclerViewStateItemCount()
方法对itemCount进行更新设置 - 依据方法返回值确定是否调用成功(负数为不成功)
- 返回调整后的结果(注意依据不同的情况可能返回mCount/mLastAdjustCount/adjustCount)
//首先,假设adapter定义了一个成员变量mLastAdjustCount来缓存上一次调整后的结果.
int mLastAdjustCount=0;
//实际设置adapter中的数据源提供的数据量
int mCount=0;
public int getItemCount() {
int adjustCount = mCount;
//第一次载入时,lastAdjustCount都必然更新为当前的itemCount
if (mLastAdjustCount == FIRST_LOAD_ITEM_COUNT) {
mLastAdjustCount = mCount;
}
IAdjustCountOption justOption = null;
adjustCount = justOption.getAdjustCount();
//当item数量在有效范围内才会进行调整
if (adjustCount < 0 || adjustCount > mCount) {
adjustCount = mCount;
mLastAdjustCount = adjustCount;
} else {
if (mLastAdjustCount != adjustCount) {
//当当前调整值与上一次调整值不同一时候再进行计算新的调整值
int originalCount = RecyclerViewUtil.setRecyclerViewStateItemCount(adjustCount, this.getOriginalItemCount(), mParentRecycle);
if (originalCount >= 0) {
//正常
//记录最后一次调整的item数量
mLastAdjustCount = adjustCount;
} else {
//返回负数,出错了,打出错误
switch (originalCount) {
case RecyclerViewUtil.STATE_RECYCLERVIEW_EXCEPTION:
//异常已经在方法中打印出
break;
case RecyclerViewUtil.STATE_RECYCLERVIEW_ILLEGAL_PARAMS:
//參数不合法不对,可能parentView为null或者是调整的adjustCount不对之类
break;
case RecyclerViewUtil.STATE_RECYCLERVIEW_STATE_UNINITIALIZED:
//parentView中的state对象还没有被初始化...这个,一般不可能,这里仅仅是以防万一
break;
default:
break;
}
//不论什么的出错都将调整值重置为初始值
mLastAdjustCount = mCount;
}
} else {
//当前调整数量与上次调整一样,直接使用缓存的值
}
}
return mLastAdjustCount;
}
关于改动后可能存在的问题
改动后事实上并非就全然解决这个问题了,首先我们须要明白一下:
adapter.getItemCount()
在正常情况下决定了item的数量,同一时候与之有关的也就是缓存的item数量及位置state.mItemCount
是实际能够进行布局的item数量,我们能够理解为是总的缓存(包含用来显示itemView)的总数量(关于RecycleView缓存view实际上不仅仅缓存了一次,一共同拥有两部分缓存,不细致讨论,我们就是指看到的部分的缓存);
因为RecycleView更新item时总是会从缓存中取出view进行更新,同一时候item的位置也view也是有关联并不断地变化,所以这里改动state.mItemCount
之后就可能导致一个问题.
样例说明
假设将state.mItemCount
数量改动少了,改动前为m,改动后为n(所以m>n);
- 对RecycleView来说,可用的缓存数量有n个(由
state.mItemCount
决定) - 对于
state
来说,实际可被复用的缓存view数量有m个(由本身的view缓存列表决定)
所以当RecycleView须要获取缓存view时state
可能会将n以外的view交给RecycleView,而此时该部分view的位置已经超过了state.mItemCount
总的缓存数n,这时就会报错了.
//报错大致内容
IndexOutOfBoundsException:Inconsistency detected. Invalid item position m (offset:n). state:n
//大致意思就是state仅仅有n个item,可是如今拿到的item位置超过了其同意的范围
//报错的代码部分(不至一个地方可能报错,但原因基本一致)
View getViewForPosition(int position, boolean dryRun) {
...
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ "position " + position + "(offset:" + offsetPosition + ")."
+ "state:" + mState.getItemCount());
}
...
}
关于报错的原因这部分仅仅是大概查看了一下源代码,实际可能会有很多其它的原因或者是其它的问题,因为没有深究下去,所以以上的推断主要是结合部分代码进行的推測.
触发条件与解决方式
既然有问题了,就应该确定他的触发条件与解决方式.
触发条件
触发条件是非常明显的,依据上面的分析,当将state.mItemCount
的值改为n时,有且仅仅有当须要获取n以上数量的item时才会触发该异常;而这样的情况通常发生在更新adapter时;解决方式
解决方式非常easy,确保在recycleView.setAdapter()
更新adapter数据的操作之前对该字段进行更改就可以.
关于该方法及使用的时机
首先该方法在RecycleViewUtil
中的签名及使用例如以下:
//签名
/**
* 改动RecycelView中的State对象中的itemCount,当动态改变显示的itemCount时,必须进行改动.
*
* @param itemCount 须要更新的itemCount,该值必须 >=0,小于RecycleView的itemList中的item数量.
* @param rv
* @return 返回值为改动前的itemCount, 当为负数时说明改动失败, 依据常量确定失败原因
*/
public static final int setRecyclerViewStateItemCount(intitemCount, @Nullable RecyclerView rv);
//用法
int originalCount = RecyclerViewUtil.setRecyclerViewStateItemCount(adjustCount, recycleView);
使用时机,因为这个是针对state.mItemCount
进行改动的,而state.mItemCount
是与adapter.getItemCount()
相关联,换言之,在不论什么对state.mItemCount
的操作之前必须是已经调用过adapter.getItemCount()
,所以该方法的运行时机就非常明显了:放在adapter.getItemCount()
中运行.
因为adatper.getItemCount()
会在非常多地方被调用(包含开发人员本身有时也会须要用到),而且对state.mItemCount
的改动也不是每一次都须要,在改动完之后没有不论什么再须要进行改动之前都不再须要调用该方法,所以怎样处理好调用的次数应该由开发人员解决.
当然(安利来了),HeaderRecycleAdapter已经非常细致地处理了这个问题了,所以推荐使用
HeaderRecycleAdapter
,一来降低了工作量,二来保证绝大部分情况下的安全可靠,毕竟是測试过非常多场景的使用.
使用场景
首先看个图,有没有看过这样的需求的?
从图中能够非常明显分析到这个需求的一些要点:
- 最后一个头像是很多其它,而且后面不再有其它头像
- 全部的头像(包含很多其它)须要显示在一行内,而且在屏幕内
- 因为设备的多样,无法保证实际在不同的设备上须要的头像的多少(能够大概预算数量范围,但准确数却无法确定)
- 头像须要均分显示出来
可能的解决方式
这里就能够使用这个动态设置item数量的方法了.往数量中填充超量的数量(比方几十甚至一百,这里仅仅是极端地举例),然后并不须要管数据量的问题,我们仅仅要动态设置我们须要显示的item数量就能够了.(关于均分界面的方法也包含在这个工具类中,请查看相关的另外的文章)
当然该需求有多种方式实现(使用gridView之类的…),但通过上述的方式有一个优点是,数据我能够任意地设置(上百甚至上千条数据都能够),而我仅仅要改变item的数量就能够做到显示指定数量的item了,同一时候数据并不会有不论什么改变.
当我须要显示很多其它数据时我再次更改item数量就可以,不须要对数据进行不论什么的加入或者移除的操作.
扩展的adapter及接口
对于动态改动显示item数量的功能,也集成到了HeaderRecycleAdapter
中了.并为了方便使用,提供了一个新的接口.原有的对数据源进行分组的接口没有不论什么变化,主要就是为了兼容上个版本号的用法.
IAdjustCountOption
接口
该接口仅仅提供与调整item数量相关的方法,并不涉及其它不论什么方面的,是独立存在的.
/**
* 调整界面显示的item数接口
*/
public interface IAdjustCountOption {
/**
* 无效的item长度,使用此值时不会改变原来的item长度
*/
public static final int NO_USE_ADJUST_COUNT = -1;
/**
* 设置调整后须要显示的itemCount.
*
* @param adjustCount count必须小于原始加入数据的量(否则多出的数据根本不可能找到填充的对象), 同一时候也必须大于1才有效.返回负数无效, 将使用原数据量.
* 默认值为负数{@link #NO_USE_ADJUST_COUNT}
*/
public void setAdjustCount(int adjustCount);
/**
* 返回调整后须要显示的itemCount.
*
* @return 返回的count必须小于原始加入数据的量(否则多出的数据根本不可能找到填充的对象), 同一时候也必须大于1才有效.返回负数无效, 将使用原数据量.
* 默认值为负数{@link #NO_USE_ADJUST_COUNT}
*/
public int getAdjustCount();
/**
* 每一次当itemView被创建的时候此方法会被回调,建议在这个地方依据parentView进行计算并设置须要调整的itemCount
*
* @param itemView
* @param parentView 此处为RecycleView
* @param adapter 适配器
* @param viewType
*/
public void onCreateViewEverytime(@NonNull View itemView, @NonNull ViewGroup parentView, @NonNull HeaderRecycleAdapter adapter, int viewType);
}
- 用法
对于原本使用HeaderRecycleAdapter
来说,须要使用调整item数量的功能仅仅须要让adapterOption实现此接口就可以;否则不须要作不论什么改动;
对于使用SimpleRecycleAdapter
简易版的adapter来说,SimpleAdapterOption
已经默认实现此方法了,仅仅要调接口的setAdjustCount(int)
就可以实现改动item数量的目的.
关于部分方法的使用
在接口中有一个方法须要注意一下onCreateViewEverytime
,此方法在每次adapter.onCreateViewHolder
创建新的view与holder时会被调用.
存在这种方法的主要原因是,有时一些对recycleView中item数量的动态计算能够在这里操作(当然一般仅仅须要计算一次而不是每次创建view时都计算),并在这里调整item的数量就可以;
关于此方法兴许会有其它重要用途,但临时不须要使用时能够搁置.
接口使用实例
以下举例说明接口的实现与使用;事实上实现该接口非常easy,仅仅须要让实现IHeaderAdapterOption
接口的实现类同一时候实现此接口就可以(内置的SimpleAdapterOption
已经默认实现了此接口).
//同一时候实现IHeaderAdapterOption与IAdjustCountOption
//原因以下会提及
public class AdapterOption<T> implements IHeaderAdapterOption<T, Object>, IAdjustCountOption{
//定义缓存的变量
private int mAdjustItemCount=-1;
//实现相应的方法
public int getAdjustCount() {
return mAdjustCount;
}
public void setAdjustCount(int adjustCount) {
mAdjustCount = adjustCount;
}
//此方法在不须要使用时空实现就可以
public void onCreateViewEverytime(@NonNull View itemView, @NonNull ViewGroup parentView, @NonNull HeaderRecycleAdapter adapter, int viewType) {
}
}
SimpleAdapterOption
已实现此接口,实现方式与上述样例同样,须要重写相应的方法时直接重写就可以;
- 同一时候与
IHeaderAdapterOption
实现的原因
这里要求IAdjustCountOption
能够生效须要与IHeaderAdapterOption
同一时候实现,原因是HeaderRecycleAdapter
在调整item数量时,全部的数量来源和操作是对IHeaderAdapterOption
的操作.
所以调整item的操作也是在检測到实现类同一时候实现了IAdjustCountOption
时才会去调整item数量;
此处仅仅针对使用HeaderRecycleAdapter
,假设是自己定义的adapter没有这个要求,而且调整item的操作须要自己使用上述工具类RecycleViewUtil
进行操作.
if(headerAdapterOption instanceof IAdjustCountOption){
//处理调整的操作.
}
//使用SimpleAdapterOption时不须要关注这个问题,因为本身已经实现了这个接口了.当然你全然能够重写方法去更新某些操作.
接口使用建议
一般来说,不太可能在同一时候须要分组(使用HeaderRecycleAdapter
)的同一时候,还须要调整item数量;所以一般使用的是SimpleRecycleAdapter
,而内置的SimpleAdapterOption
已经实现了这个接口,所以尽情使用就是了.
当然,实际上有没有可能同一时候须要用到分组及调整item数量并非能全然确定的,当须要使用时,记得让adapterOption实现此接口.
小结
感觉说了非常多但事实上仅仅是介绍了非常少的部分内容.对于这个功能实际上使用频率是不可能会非常高的,可是在某些场景下会非常实用.
强烈建议查看相关的还有一篇文章,动态计算并均分显示childView的部分,你会发现这个功能的实际用处.
GitHub地址
https://github.com/CrazyTaro/RecycleViewAdapter
假设认为实用,欢迎start,能够提供很多其它动力~谢谢~