Android无限循环RecyclerView的完美实现方案
背景
项目中要实现横向列表的无限循环滚动,自然而然想到了recyclerview,但我们常用的recyclerview是不支持无限循环滚动的,所以就需要一些办法让它能够无限循环。
方案选择
方案1 对adapter进行修改
网上大部分博客的解决方案都是这种方案,对adapter做修改。具体如下
首先,让 adapter 的 getitemcount() 方法返回 integer.max_value,使得position数据达到很大很大;
其次,在 onbindviewholder() 方法里对position参数取余运算,拿到position对应的真实数据索引,然后对itemview绑定数据
最后,在初始化recyclerview的时候,让其滑动到指定位置,如 integer.max_value/2,这样就不会滑动到边界了,如果用户一根筋,真的滑动到了边界位置,再加一个判断,如果当前索引是0,就重新动态调整到初始位置
这个方案是挺简单,但并不完美。一是对我们的数据和索引做了计算操作,二是如果滑动到边界,再动态调整到中间,会有一个不明显的卡顿操作,使得滑动不是很顺畅。所以,直接看方案二。
方案2 自定义layoutmanager,修改recyclerview的布局方式
这个算得上是一劳永逸的解决方案了,也是我今天要详细介绍的方案。我们都知道,recyclerview的数据绑定是通过adapter来处理的,而排版方式以及view的回收控制等,则是通过layoutmanager来实现的,因此我们直接修改itemview的排版方式就可以实现我们的目标,让recyclerview无限循环。
自定义layoutmanager
1.创建自定义layoutmanager
首先,自定义 looperlayoutmanager 继承自 recyclerview.layoutmanager,然后需要实现抽象方法 generatedefaultlayoutparams(),这个方法的作用是给 itemview 设置默认的layoutparams,直接返回如下就行。
public class looperlayoutmanager extends recyclerview.layoutmanager { @override public recyclerview.layoutparams generatedefaultlayoutparams() { return new recyclerview.layoutparams(viewgroup.layoutparams.wrap_content, viewgroup.layoutparams.wrap_content); } }
2.打开滚动开关
接着,对滚动方向做处理,重写canscrollhorizontally()方法,打开横向滚动开关。注意我们是实现横向无限循环滚动,所以实现此方法,如果要对垂直滚动做处理,则要实现canscrollvertically()方法。
@override public boolean canscrollhorizontally() { return true; }
3.对recyclerview进行初始化布局
好了,以上两部是基础工作,接下来,重写 onlayoutchildren() 方法,开始对itemview初始化布局。
@override public void onlayoutchildren(recyclerview.recycler recycler, recyclerview.state state) { if (getitemcount() <= 0) { return; } //标注1.如果当前时准备状态,直接返回 if (state.isprelayout()) { return; } //标注2.将视图分离放入scrap缓存中,以准备重新对view进行排版 detachandscrapattachedviews(recycler); int autualwidth = 0; for (int i = 0; i < getitemcount(); i++) { //标注3.初始化,将在屏幕内的view填充 view itemview = recycler.getviewforposition(i); addview(itemview); //标注4.测量itemview的宽高 measurechildwithmargins(itemview, 0, 0); int width = getdecoratedmeasuredwidth(itemview); int height = getdecoratedmeasuredheight(itemview); //标注5.根据itemview的宽高进行布局 layoutdecorated(itemview, autualwidth, 0, autualwidth + width, height); autualwidth += width; //标注6.如果当前布局过的itemview的宽度总和大于recyclerview的宽,则不再进行布局 if (autualwidth > getwidth()) { break; } } }
onlayoutchildren() 方法顾名思义,就是对所有的 itemview 进行布局,一般会在初始化和调用 adapter 的 notifydatasetchanged() 方法时调用。代码思路已经注释的很清楚了,其中有几个方法需要简单提下:
标注2处 detachandscrapattachedviews(recycler) 方法会将所有的 itemview 从view树中全部detach,然后放入scrap缓存中。了解过recyclerview的同学应该知道,recyclerview是有一个二级缓存的,一级缓存是 scrap 缓存,二级缓存是 recycler 缓存,其中从view树上detach的view会放入scrap缓存里,调用removeview()删除的view会放入recycler缓存中。
标注3处 recycler.getviewforposition(i) 方法会从缓存中拿到对应索引的 itemview,这个方法内部会先从 scrap 缓存中取 itemview,如果没有则从 recycler 缓存中取,如果还没有则调用 adapter 的 oncreateviewholder() 去创建 itemview。
标注5处 layoutdecorated() 方法会对 itemview 进行布局排版,这里可以看出来,我们是根据宽依次往父容器的右边排下去,直到下一个 itemview的顶点位置超过了recyclerview 的宽度。
4.对recyclerview进行滚动和回收itemview处理
对recyclerview的子item进行排版布局后,运行一下效果就会出现了,不过这时候我们滑动列表会发现滑动后变成空白了,所以就该对滑动操作进行处理了。
前面说过,我们打开了横向滚动的开关,所以对应的,我们要重写 scrollhorizontallyby()方法进行横向滑动操作。
@override public int scrollhorizontallyby(int dx, recyclerview.recycler recycler, recyclerview.state state) { //标注1.横向滑动的时候,对左右两边按顺序填充itemview int travl = fill(dx, recycler, state); if (travl == 0) { return 0; } //2.滑动 offsetchildrenhorizontal(-travl); //3.回收已经不可见的itemview recyclerhideview(dx, recycler, state); return travl; }
可以看到,滑动逻辑很简单,总结为三步:
- 横向滑动的时候,对左右两边按顺序填充itemview
- 滑动itemview
- 回收已经不可见的itemview
下面一步一步介绍:
首先第一步,滑动的时候调用自定义的 fill() 方法,对左右两边进行填充。还没忘了,我们是来实现循环滑动的,所以这一步尤其重要,先看代码:
/** * 左右滑动的时候,填充 */ private int fill(int dx, recyclerview.recycler recycler, recyclerview.state state) { if (dx > 0) { //标注1.向左滚动 view lastview = getchildat(getchildcount() - 1); if (lastview == null) { return 0; } int lastpos = getposition(lastview); //标注2.可见的最后一个itemview完全滑进来了,需要补充新的 if (lastview.getright() < getwidth()) { view scrap = null; //标注3.判断可见的最后一个itemview的索引, // 如果是最后一个,则将下一个itemview设置为第一个,否则设置为当前索引的下一个 if (lastpos == getitemcount() - 1) { if (looperenable) { scrap = recycler.getviewforposition(0); } else { dx = 0; } } else { scrap = recycler.getviewforposition(lastpos + 1); } if (scrap == null) { return dx; } //标注4.将新的itemviewadd进来并对其测量和布局 addview(scrap); measurechildwithmargins(scrap, 0, 0); int width = getdecoratedmeasuredwidth(scrap); int height = getdecoratedmeasuredheight(scrap); layoutdecorated(scrap,lastview.getright(), 0, lastview.getright() + width, height); return dx; } } else { //向右滚动 view firstview = getchildat(0); if (firstview == null) { return 0; } int firstpos = getposition(firstview); if (firstview.getleft() >= 0) { view scrap = null; if (firstpos == 0) { if (looperenable) { scrap = recycler.getviewforposition(getitemcount() - 1); } else { dx = 0; } } else { scrap = recycler.getviewforposition(firstpos - 1); } if (scrap == null) { return 0; } addview(scrap, 0); measurechildwithmargins(scrap,0,0); int width = getdecoratedmeasuredwidth(scrap); int height = getdecoratedmeasuredheight(scrap); layoutdecorated(scrap, firstview.getleft() - width, 0, firstview.getleft(), height); } } return dx; }
代码是有点长,不过逻辑很清晰。首先分为两部分,往左填充或是往右填充,dx为将要滑动的距离,如果 dx > 0,则是往左边滑动,则需要判断右边的边界,如果最后一个itemview完全显示出来后,在右边填充一个新的itemview。
看标注3,往右边填充的时候需要检测当前最后一个可见itemview的索引,如果索引是最后一个,则需要新填充的itemview为第0个,这样就可以实现往左边滑动时候无限循环了。然后将需要新填充的itemview进行测量布局操作,将填充进去了。
同理,往右滑动的逻辑跟往左滑动相似,就不一一再阐述了。
第二步:填充完新的itemview后,就开始进行滑动了,这里直接调用 layoutmanager 的 offsetchildrenhorizontal() 方法滑动-travl 距离,travl 是通过fill方法计算出来的,通常情况下都为 dx,只有当滑动到最后一个itemview,并且循环滚动开关没有打开的时候才为0,也就是不滚动了。
//2.滚动 offsetchildrenhorizontal(travl * -1);
第三步:回收已经不可见的itemview。只有对不可见的itemview进行回收,才能做到回收利用,防止内存爆增。
/** * 回收界面不可见的view */ private void recyclerhideview(int dx, recyclerview.recycler recycler, recyclerview.state state) { for (int i = 0; i < getchildcount(); i++) { view view = getchildat(i); if (view == null) { continue; } if (dx > 0) { //标注1.向左滚动,移除左边不在内容里的view if (view.getright() < 0) { removeandrecycleview(view, recycler); log.d(tag, "循环: 移除 一个view childcount=" + getchildcount()); } } else { //标注2.向右滚动,移除右边不在内容里的view if (view.getleft() > getwidth()) { removeandrecycleview(view, recycler); log.d(tag, "循环: 移除 一个view childcount=" + getchildcount()); } } } }
代码也很简单,遍历所有添加进 recyclerview 里的item,然后根据 itemview 的顶点位置进行判断,移除不可见的item。移除 itemview 调用 removeandrecycleview(view, recycler) 方法,会对移除的item进行回收,然后存入 recyclerview 的缓存里。
至此,一个可以实现左右无限循环的layoutmanager就实现了,调用方式跟通常我们用rrcyclerview没有任何区别,只需要给 recyclerview 设置 layoutmanager 时指定我们的layoutmanager,如下:
recyclerview.setadapter(new myadapter()); looperlayoutmanager layoutmanager = new looperlayoutmanager(); layoutmanager.setlooperenable(true); recyclerview.setlayoutmanager(layoutmanager);
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: 春茶有什么好处,给你介绍春茶的相关知识
下一篇: 深入理解Java注解的使用方法