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,直接返回如下就行。
publicclassLooperLayoutManagerextendsRecyclerView.LayoutManager{ @Override publicRecyclerView.LayoutParamsgenerateDefaultLayoutParams(){ returnnewRecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } }
2.打开滚动开关
接着,对滚动方向做处理,重写canScrollHorizontally()方法,打开横向滚动开关。注意我们是实现横向无限循环滚动,所以实现此方法,如果要对垂直滚动做处理,则要实现canScrollVertically()方法。
@Override publicbooleancanScrollHorizontally(){ returntrue; }
3.对RecyclerView进行初始化布局
好了,以上两部是基础工作,接下来,重写onLayoutChildren()方法,开始对itemView初始化布局。
@Override publicvoidonLayoutChildren(RecyclerView.Recyclerrecycler,RecyclerView.Statestate){ if(getItemCount()<=0){ return; } //标注1.如果当前时准备状态,直接返回 if(state.isPreLayout()){ return; } //标注2.将视图分离放入scrap缓存中,以准备重新对view进行排版 detachAndScrapAttachedViews(recycler); intautualWidth=0; for(inti=0;igetWidth()){ 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 publicintscrollHorizontallyBy(intdx,RecyclerView.Recyclerrecycler,RecyclerView.Statestate){ //标注1.横向滑动的时候,对左右两边按顺序填充itemView inttravl=fill(dx,recycler,state); if(travl==0){ return0; } //2.滑动 offsetChildrenHorizontal(-travl); //3.回收已经不可见的itemView recyclerHideView(dx,recycler,state); returntravl; }
可以看到,滑动逻辑很简单,总结为三步:
- 横向滑动的时候,对左右两边按顺序填充itemView
- 滑动itemView
- 回收已经不可见的itemView
下面一步一步介绍:
首先第一步,滑动的时候调用自定义的fill()方法,对左右两边进行填充。还没忘了,我们是来实现循环滑动的,所以这一步尤其重要,先看代码:
/** *左右滑动的时候,填充 */ privateintfill(intdx,RecyclerView.Recyclerrecycler,RecyclerView.Statestate){ if(dx>0){ //标注1.向左滚动 ViewlastView=getChildAt(getChildCount()-1); if(lastView==null){ return0; } intlastPos=getPosition(lastView); //标注2.可见的最后一个itemView完全滑进来了,需要补充新的 if(lastView.getRight()=0){ Viewscrap=null; if(firstPos==0){ if(looperEnable){ scrap=recycler.getViewForPosition(getItemCount()-1); }else{ dx=0; } }else{ scrap=recycler.getViewForPosition(firstPos-1); } if(scrap==null){ return0; } addView(scrap,0); measureChildWithMargins(scrap,0,0); intwidth=getDecoratedMeasuredWidth(scrap); intheight=getDecoratedMeasuredHeight(scrap); layoutDecorated(scrap,firstView.getLeft()-width,0, firstView.getLeft(),height); } } returndx; }
代码是有点长,不过逻辑很清晰。首先分为两部分,往左填充或是往右填充,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 */ privatevoidrecyclerHideView(intdx,RecyclerView.Recyclerrecycler,RecyclerView.Statestate){ for(inti=0;i0){ //标注1.向左滚动,移除左边不在内容里的view if(view.getRight()<0){ removeAndRecycleView(view,recycler); Log.d(TAG,"循环:移除一个viewchildCount="+getChildCount()); } }else{ //标注2.向右滚动,移除右边不在内容里的view if(view.getLeft()>getWidth()){ removeAndRecycleView(view,recycler); Log.d(TAG,"循环:移除一个viewchildCount="+getChildCount()); } } } }
代码也很简单,遍历所有添加进RecyclerView里的item,然后根据itemView的顶点位置进行判断,移除不可见的item。移除itemView调用removeAndRecycleView(view,recycler)方法,会对移除的item进行回收,然后存入RecyclerView的缓存里。
至此,一个可以实现左右无限循环的LayoutManager就实现了,调用方式跟通常我们用RrcyclerView没有任何区别,只需要给RecyclerView设置LayoutManager时指定我们的LayoutManager,如下:
recyclerView.setAdapter(newMyAdapter()); LooperLayoutManagerlayoutManager=newLooperLayoutManager(); layoutManager.setLooperEnable(true); recyclerView.setLayoutManager(layoutManager);
访问源码请点我
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。