*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
之前就想写这篇文章,奈何没有彻底弄懂自定义LayoutManager机制,导致写到一半时候就感觉无从下手了,就搁浅停笔了。而现在终于可以完成这篇文章了。
ok,开门见山不废话,直接分析实现一个跟系统LinearLayoutManager一样的自定义LayoutManager。
RecyclerView由于解耦的比较彻底,所以可定制性也非常的强,我们设置LinearLayoutManager的时候所有的控件就会水平或者垂直排列,设置GridLayoutManager时就会呈现网格排列,设置StaggeredGridLayoutManager就会瀑布流排列。
虽然系统提供的这些控件足以应对大多数产品99%的需求,但是这么神奇的东东还是值得去研究一下的。
我们都知道自定义ViewGroup的时候需要去继承ViewGroup,那么同样我们自定义LayoutManager的时候也需要去继承LayoutManager这个类。
随便起名一个CustomeLayoutManager类,继承这个类需要实现一个方法
这个方法是必须实现的,那么实现它有什么作用呢?简单的解释下,假设我们平时调用LayoutInflate.inflate(resource , null, false)第二个参数传的是null或者直接使用View.inflate()去填充View的时候那么填充的View是没有布局参数的,那么当我们的Recyclerview去addView()时就会进行判断,如果childView的布局参数为null就是调用这个方法去生成一个默认的布局参数。
下面是部分截取源码:
具体可以参考这篇文章:
http://blog.csdn.net/overseasandroid/article/details/51840819
自定义ViewGroup要重写并实现一个onLayout()方法,我们这里类似需要实现onLayoutChildren()这个方法
首先我们需要判断itemCount是否为空,state.isPreLayout()是判断之前布局时动画有没有处理结束,这里我们假设所有的itemView的宽和高都是相等的,创建第一个view并测量得到这个view的宽和高,注意这里我们调用的是getDecoratedMeasuredWidth方法而不是getMeasuredWidth
这个方法会得到view控件的宽、高和ItemDecoration的top,bottom,left,right进行叠加我们需要考虑添加ItemDecoration这种情况。
最后把这个view放入scap中,scap是轻量级缓存集合,通常被 detach 但会在同一布局重新使用的视图会临时储存在这里,它不需要进行重新绑定数据。
ok,拿到第一个控件的宽高之后,就可以大干一场了,创建一个集合这里用SparseArray就行了,因为我们的key都是index下标使用SparseArray效率比较高,
遍历数据集把每一个itemview的位置都计算出来然后放进集合里。
我这里把填充view单独抽取到一个方法里防止还要使用
这里就简单的填充下,注意网上大多数甚至基本我看到的所有教程、博客基本都是统一这么写的,
一开始我也就这么实现,但是当我好奇把itemCount加到几万,甚至几十万条数据时。。。你懂的,爆炸了~~~
所以说国内现象是很多人不喜欢思考,ctrl+c、v 实现了完事。。。。
我这里改进了下,开始就填充,屏幕可见数量,在onBindViewHolder中打印下log可以看到确实是合格的,而官方的LayoutManager也是默认填充这么多滴!
可以说我实现的这个达到官方要求啦~~啊哈小小的自恋一下。
ok,布局已经全部填充完毕,滑动一下,咦!没有任何反应,别着急,我们还需要实现一些方法才可以让它动起来,
当我们需要垂直滚动时就重写scrollVerticallyBy这个方法,它会把垂直滚动的偏移量传递给到dy里。
这里进行下逻辑判断
dy>0:向上滚动
dy<0:向下滚动
这里处理一下边界越界情况,最后将实际位移距离应用给子视图,注意这里返回的偏移量不能算错了,因为返回值被用来决定什么时候取消 flings,如果返回错误的值会让你失去对 content fling 的控制,并且正确的返回值还可以正确拥有边缘发光效果。
好了我们实现这个方法后已经可以动了,看下我录制的gif图
最后我们还需要完成最后一件事,也是最重要的一件事,recyclerview最神奇的地方就在于它的item控件是可以复用的,这样大大的节省了内存所占用空间并提高了加载控件的效率,当初使用它的时候就被这一神奇特性惊叹到,ok现在我们自己去实现这个特性。
这里我重新封装一个fillViews()方法用于回收复用view
注意这里跟上面填充的方法是不同的(参数数量不同),看下我是怎么实现的
这里判断当childCount不为0的时候遍历recyclerview的每一个child然后拿到当前每一个child的位置跟屏幕显示大小进行比较,这里屏幕的范围为0~屏幕的高,如果不在这个范围内就回收掉它等待复用。
还记得一开始我们那个存储了所有view位置的集合吗,现在有它用武之地了,遍历所有的view跟当前偏移的屏幕显示大小进行比较,在这个范围内的,我们获取到view后并进行位置的填充,注意!这里屏幕显示大小的范围不再是0~屏幕的高了而是我们滚动时记录的 位置偏移量~位置偏移量+屏幕的高。
可能这里你会有疑问,回收的view我们是怎么拿到的呢?就是这个方法getViewForPosition(), recyclerview有多级缓存,它会去recyclerview的所有缓存中去找,比如最先会在scap中找,如果找到会比较这个当前position跟scap缓存中viewHolder的position是否一致,如果一致直接返回这个viewholder并且不需要进行rebinding数据,如果找不到或者position不一致再去caches缓存中去找,如果所有的缓存中都找不到就会调用mAdapter.onCreateViewHolder()方法去创建一个全新的viewholder。
具体细节可以看我这篇关于recyclerview回收分析:
http://blog.csdn.net/boboyuwu/article/details/77148302
到此为止,一个简单的自定义LinearLayoutManager就诞生了,并且特性和效果跟官方的基本一致,当然我们自然没有官方提供控件考虑的那么周到并且测试的也没有那么全面,毕竟官方的控件还是提供了非常多的功能,代码逻辑要更之复杂,但是基本的功能已经完全实现了,我们看下效果吧
在onCreateViewHolder方法里并创建一个累加值打印下log观看下viewholder创建情况
可以看到从第16条数据开始,完全复用的是前面回收的view,之后一直都是在binding数据而没有重新创建viewholder了,这个gif效果图跟官方LinearLayoutManager是一摸一样的感兴趣的可以自己试验下,这里就不再截图了。
最后还有个中间小插曲,就是一开始在一定滑动速度下,这个view的回收复用都是正常的,但是手指以超速fling状态下惯性滑动复用就失效了,这里录了个效果图,可以对比看下
这是不惯性滑动,一切都是正常的
惯性滑动后尼玛就疯了~~~有多少数据创建多少view
纠结了半天去排查原因,最后发现原来是惯性滑动时第一次dy这个值传的就非常大,可能会直接超出一个view高度以上
而问题就出现在这里,假如第一个事件的值就特别大,recyclerview的第一个child和第二个child全都偏移出了屏幕可见范围,在这里遍历child时第一个child由于不可见被移除并回收,那么之前的position位置1此时会变成0,而我们的i此时却是1,此时getChildAt(1)获取到的view就是回收之前的position 2所在的view,所以我们就漏回收了position 1这个位置的view导致回收逻辑出现问题,解决的方法就是把i–注释去掉即可。
好了,到这里已经系统的学习了下自定义layoutmanager过程,了解了这些过程我相信做出其他的效果也只是时间的问题~~