IAdjustCountOption--动态设置recycleView的itemCount(不需要修改数据源)

Posted 疯狂小芋头

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IAdjustCountOption--动态设置recycleView的itemCount(不需要修改数据源)相关的知识,希望对你有一定的参考价值。

概述

RecycleViewUtil是新增的一个主要针对RecycleView的一个工具类.该工具类中提供了部分RecycleView可能会使用到的方法,其中也包括了一些用来增强HeaderRecycleAdapter功能的扩展方法.
通过该工具类也可以很容易在普通的adapter中实现对应的相关扩展功能.

可实现的功能有:

  • 随意调整RecycleViewAdapter中的item数量
  • 可根据RecycleView已确定的某一边长自动调整item数量以填充整个RecycleView(并不再有多余的item)
  • 可根据RecycleView已确定的某一边长随意指定需要填充整个RecycleView的item的数量

对于实现的功能如果觉得有点不理解,下面会有更详细的分析.


调整任意数量的item数量

使用过RecycleView的都知道数据部分都是由adapter进行负责的,而item的数量取决于数据量的大小,所以当需要更改item数量时,势必通过调整数据量的大小进行更改.

使用这个原理进行工作的包括给RecycleView添加header/footer等.

而这里调整的方式与之不同的是:

  • 数据完全不需要有任何的变化,而是支持对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时;

  • 解决方案
    解决方案很简单,确保在recycleView.setAdapter()更新adapter数据的操作之前对该字段进行更改即可.


关于该方法及使用的时机

首先该方法在RecycleViewUtil中的签名及使用如下:

//签名
/**
 * 修改RecycelView中的State对象中的itemCount,当动态改变显示的itemCount时,必须进行修改.
 *
 * @param itemCount 需要更新的itemCount,该值必须 &gt;=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的数量即可;
关于此方法后续会有其它重要用途,但暂时不需要使用时可以搁置.


接口使用实例

以下举例说明接口的实现与使用;其实实现该接口非常简单,只需要让实现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,可以提供更多动力~谢谢~


示例GIF

图片可能有点大,请耐心等待加载

回到目录

以上是关于IAdjustCountOption--动态设置recycleView的itemCount(不需要修改数据源)的主要内容,如果未能解决你的问题,请参考以下文章

IAdjustCountOption--动态设置recycleView的itemCount(不须要改动数据源)

R语言plotly包可视化线图(line plot)使用restyle参数自定义设置可视化结果中线条的颜色使用按钮动态切换线条的颜色(change line color with button)

如何使用代码动态的获取和设置ImageView的宽度和高度

R Shiny动态盒子高度

动态设置图像视图的边距

动态设置RecyclerView的高度