掌握自定义LayoutManager之实现流式布局

掌握自定义LayoutManager之实现流式布局

今日科技快讯

昨日,人民币兑美元汇率闹出巨大乌龙事件,短短两个小时,Google上查询的人民币兑美元的汇率从6.8 : 1变成了7.4 : 1 ,人民币资产瞬间缩水了约8%!一时间引起了极多人的恐慌。

后经查实,Google使用的是一家xe rate的网站数据,除了这家网站以外,其他网站的汇率数据都还保持在1 : 6.8左右,算是Google闹出了一个大的乌龙事件,也让很多人松了一口气。

作者简介

本篇是 张旭童 的第四篇投稿了,这是一个系列文章,我选取了实战部分分享给大家,如果对LayoutManager还不熟悉的朋友,可以去作者博客看看前置文章。

张旭童 的博客地址:

http://blog.csdn.net/zxt0601

概述

在开始之前,我想说,如果需求是每个Item宽高一样,实现起来复杂度比每个Item宽高不一样的,要小10+倍。

然而我们今天要实现的流式布局,恰巧就是至少每个Item的宽度不一样,所以在计算坐标的时候算的我死去活来。先看一下效果图:

掌握自定义LayoutManager之实现流式布局

艾玛,换成妹子图后貌似好看了许多,我都不认识它了,好吧,项目里它一般长下面这样:

掌握自定义LayoutManager之实现流式布局

往常这种效果,我们一般使用自定义ViewGroup实现,我以前也写了一个:

自定义VG实现流式布局 

http://blog.csdn.net/zxt0601/article/details/50533658

这不最近再研究自定义LayoutManager么,想来想去也没有好的创意,就先拿它开第一刀吧。

(后话:流式布局Item宽度不一,不知不觉给自己挖了个大坑,造成拓展一些功能难度倍增,观之网上的DEMO,99%Item的大小都是一样的,so,这个系列的下一篇我计划 实现一个Item大小一样 的酷炫LayoutManager。但是最终做成啥样的效果还没想好,有朋友看到酷炫的效果可以告诉我,我去高仿一个。)

自定义LayoutManager的步骤:

以本文的流式布局为例,需求是一个垂直滚动的布局,子View以流式排列。先总结一下步骤:

  • 实现 generateDefaultLayoutParams() 

  • 实现 onLayoutChildren() 

  • 竖直滚动需要 重写canScrollVertically()和scrollVerticallyBy()

下面我们就一步一步来吧。

实现generateDefaultLayoutParams

如果没有特殊需求,大部分情况下,我们只需要如下重写该方法即可。

掌握自定义LayoutManager之实现流式布局

RecyclerView.LayoutParams 是继承自 android.view.ViewGroup.MarginLayoutParams 的,所以可以方便的使用各种margin。

这个方法最终会在 recycler.getViewForPosition(i) 时调用到,在该方法浩长源码的最下方:

掌握自定义LayoutManager之实现流式布局

重写完这个方法就能编译通过了,只不过然并卵,界面上是一片空白,下面我们就走进onLayoutChildren()方法 ,为界面添加Item。

注:99%用不到的情况:如果需要存储一些额外的东西在LayoutParams里,这里返回你自定义的LayoutParams即可。

当然,你自定义的LayoutParams需要继承自 RecyclerView.LayoutParams。

onLayoutChildren

该方法是LayoutManager的入口。它会在如下情况下被调用:

  • 在RecyclerView初始化时,会被调用两次

  • 在调用adapter.notifyDataSetChanged()时,会被调用。

  • 在调用setAdapter替换Adapter时,会被调用。

  • 在RecyclerView执行动画时,它也会被调用。即RecyclerView 初始化 、 数据源改变时 都会被调用。

(关于初始化时为什么会被调用两次,我在系列第一篇文章里已经分析过。)

在系列开篇我已经提到,它相当于 ViewGroup 的 onLayout() 方法,所以我们需要在里面layout当前屏幕可见的所有子View,千万不要layout出所有的子View。本文如下编写:

掌握自定义LayoutManager之实现流式布局

这个 fill(recycler, state) 方法将是你自定义LayoutManager之旅一生的敌人,简单的说它承担了以下任务:

在考虑滑动位移的情况下:

  • 回收所有屏幕不可见的子View

  • layout所有可见的子View

在这一节,我们先看一下它的简单版本,不考虑滑动位移,不考虑滑动方向等,只考虑初始化时,从头至尾,layout所有可见的子View,在下一节我会配合滑动事件放出它的完整版.

掌握自定义LayoutManager之实现流式布局

用到的一些工具函数(在系列开篇已介绍过):

掌握自定义LayoutManager之实现流式布局

如上编写一个超级简单的fill()方法,运行,你的程序应该就能看到流式布局的效果出现了。

可是千万别开心,因为痛苦的计算远没到来。

如果这些都看不懂,那么我建议:

  • 直接下载完整代码,配合后面的章节看,看到后面也许前面的就好理解了= =。

  • 去学习一下自定义ViewGroup的知识。

此时虽然界面上已经展示了流式布局的效果,可是它并不能滑动,下一节我们让它动起来。

动起来

想让我们自定义的LayoutManager动起来,最简单的写法如下:

掌握自定义LayoutManager之实现流式布局

offsetChildrenVertical(-realOffset) 这句话移动所有的childView.

返回值会被RecyclerView用来判断是否达到边界, 如果返回值 != 传入的dy,则会有一个边缘的发光效果,表示到达了边界。而且返回值还会被RecyclerView用于计算fling效果。

写完编译,哇塞,真的跟随手指滑动了,只不过能动的总共就我们在上一节layout的那些Item,Item并没有回收,也没有新的Item出现。

好了,下面开始正经的写它吧:

掌握自定义LayoutManager之实现流式布局

这里用realOffset变量保存实际的位移,也是return 回去的值。大部分情况下它=dy。

在边界处,为了防止越界,做了一些处理,realOffset 可能不等于dy。

和别的文章不同的是,我参考了LinearLayoutManager的源码,先考虑滑动位移进行View的回收、填充( fill() 函数),然后再真正的位移这些子Item。

在fill()的过程中

流程:

一. 会先考虑到dy,回收界面上不可见的Item。

二. 填充布局子View

三. 判断是否将dy都消费掉了,如果消费不掉:例如滑动距离太多,屏幕上的View已经填充完了,仍有空白,那么就要修正dy给realOffset。

注意事项一:考虑滑动的方向

在填充布局子View的时候,还要考虑滑动的方向,即填充的顺序,是从头至尾填充,还是从尾至头部填充。

如果是向底部滑动,那么是顺序填充,显示底端position更大的Item。( dy>0)

如果是向顶部滑动,那么是逆序填充,显示顶端positon更小的Item。(dy<0)

注意事项二:流式布局 逆序布局子View的问题

再啰嗦最后一点,我们想象一下这个逆序填充的过程:

正序过程可以自上而下,自左向右layout 子View,每次layout之前判断当前这一行宽度+子View宽度,是否超过父控件宽度,如果超过了就另起一行。

逆序时,有两种方案:

1. 利用Rect保存子View边界

正序排列时,保存每个子View的Rect;

逆序时,直接拿出来,layout。

2. 逆序化

自右向左layout子View,每次layout之前判断当前这一行宽度+子View宽度,是否超过父控件宽度

如果超过了就另起一行。并且判断最后一个子View距离父控件左边的offset,平移这一行的所有子View,较复杂,采用方案1.

(我个人认为这两个方案都不太好,希望有朋友能提出更好的方案。)

下面上码:

掌握自定义LayoutManager之实现流式布局

掌握自定义LayoutManager之实现流式布局

思路已经在前面讲解过,代码里也配上了注释,计算坐标等都是数学问题,略饶人,需要用笔在纸上写一写,或者运行调试调试。没啥好办法。

值得一提的是,可以通过 getChildCount() 和 recycler.getScrapList().size() 查看 当前屏幕上的Item数量 和 scrapCache缓存区域的Item数量,合格的LayoutManager,childCount数量不应大于屏幕上显示的Item数量,而scrapCache缓存区域的Item数量应该是0. 

官方的LayoutManager都是达标的,本例也是达标的,网上大部分文章的Demo,都是不合格的。

原因在系列开篇也提过,不再赘述。

至此我们的自定义LayoutManager已经可以用了,使用的效果就和文首的两张图一模一样。

下面再提及一些其他注意点和适配事项:

适配notifyDataSetChanged

此时会回调onLayoutChildren()函数。因为我们流式布局的特殊性,每个Item的宽度不一致,所以化简处理,每次这里归零。

//初始化区域
mVerticalOffset = 0; mFirstVisiPos = 0; mLastVisiPos = getItemCount();

如果每个Item的大小都一样,逆序顺序layoutChild都比较好处理,则应该在此判断,getChildCount(),大于0说明是DatasetChanged()操作,(初始化的第二次也会childCount>0)。根据当前记录的position和位移信息去fill视图即可。

适配Adapter的替换

我根据24.2.1源码,发现网上的资料对这里的处理其实是不必要的。

资料中的做法如下:

当对 RecyclerView 设置一个 新的Adapter 时,onAdapterChanged()方法会被回调,一般的做法是在这里remove掉所有的View。此时onLayoutChildren()方法会被再次调用,一个新的轮回开始。

掌握自定义LayoutManager之实现流式布局

我的新观点:

通过查看源码+打断点跟踪分析,调用RecyclerView.setAdapter后,调用顺序依次为

1. Recycler.setAdapter():

掌握自定义LayoutManager之实现流式布局

那么我们查看 setAdapterInternal() 方法:

掌握自定义LayoutManager之实现流式布局

也就是说 更换Adapter一开始,还没有执行到LayoutManager.onAdapterChanged(),界面上的View都已经被remove掉了,我们的操作属于多余的

2. LayoutManager.onAdapterChanged()

空实现:也没必要实现了

public void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter) {}

3. Recycler.onAdapterChanged():

该方法先清空scapCache区域(貌似也是多余,一开始被清空过了),然后调用RecyclerViewPool.onAdapterChanged() 

掌握自定义LayoutManager之实现流式布局

4. RecyclerViewPool.onAdapterChanged()

如果没有别的Adapter在用这个RecyclerViewPool,会清空RecyclerViewPool的缓存。

掌握自定义LayoutManager之实现流式布局

5. LayoutManager.onLayoutChildren()

新的布局开始。

总结

引用一段话

They are also extremely complex, and hard to get right. For every amount of effort RecyclerView requires of you, it is doing 10x more behind the scenes.

本文Demo仍有很大完善空间,有些需要完善的细节非常复杂,需要经过多次试验才能得到正确的结果(这里我更加敬佩Google提供的三个LM)。每一个我们想要实现的需求,可能要花费比我们想象的时间*10倍的时间。

上篇也提及到的,不要过度优化,达成需求就好。

可以通过getChildCount()和recycler.getScrapList().size() 查看当前屏幕上的Item数量 和 scrapCache缓存区域的Item数量,合格的LayoutManager,childCount数量不应大于屏幕上显示的Item数量,而scrapCache缓存区域的Item数量应该是0.

官方的LayoutManager都是达标的,本例也是达标的,网上大部分文章的Demo,都是不合格的。

感兴趣的同学可以对网上的各个Demo打印他们onCreateViewHolder执行的次数,以及上述两个参数的值,和官方的LayoutManager比较,这三个参数先达标,才算是及格的LayoutManager,但后续优化之路仍很长。

本系列文章相关代码传送门:

自定义LayoutManager实现的流式布局

https://github.com/mcxtzhang/FlowLayoutManager

更多

每天学习累了,看些搞笑的段子放松一下吧。关注最具娱乐精神的公众号,每天都有好心情。

掌握自定义LayoutManager之实现流式布局

如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。

欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号:

掌握自定义LayoutManager之实现流式布局