Android 掌握自定义LayoutManager(二) 实现流式布局
来源:互联网 发布:nginx lua redis 限流 编辑:程序博客网 时间:2024/06/06 01:03
转载请标明出处:
http://blog.csdn.net/zxt0601/article/details/52956504
本文出自:【张旭童的博客】
本系列文章相关代码传送门:
自定义LayoutManager实现的流式布局
欢迎star,pr,issue。
本系列文章目录:
掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,常用API。
掌握自定义LayoutManager(二) 实现流式布局
一 概述
在开始之前,我想说,如果需求是每个Item宽高一样,实现起来复杂度比每个Item宽高不一样的,要小10+倍。
然而我们今天要实现的流式布局,恰巧就是至少每个Item的宽度不一样,所以在计算坐标的时候算的我死去活来。先看一下效果图:
艾玛,换成妹子图后貌似好看了许多,我都不认识它了,好吧,项目里它一般长下面这样:
往常这种效果,我们一般使用自定义ViewGroup实现,我以前也写了一个。自定义VG实现流式布局
这不最近再研究自定义LayoutManager么,想来想去也没有好的创意,就先拿它开第一刀吧。
(后话:流式布局Item宽度不一,不知不觉给自己挖了个大坑,造成拓展一些功能难度倍增,观之网上的DEMO,99%Item的大小都是一样的,so,这个系列的下一篇我计划 实现一个Item大小一样 的酷炫LayoutManager。但是最终做成啥样的效果还没想好,有朋友看到酷炫的效果可以告诉我,我去高仿一个。)
自定义LayoutManager的步骤:
以本文的流式布局为例,需求是一个垂直滚动的布局,子View以流式排列。先总结一下步骤:
一 实现 generateDefaultLayoutParams()
二 实现 onLayoutChildren()
三 竖直滚动需要 重写canScrollVertically()和scrollVerticallyBy()
下面我们就一步一步来吧。
二 实现generateDefaultLayoutParams()
如果没有特殊需求,大部分情况下,我们只需要如下重写该方法即可。
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
RecyclerView.LayoutParams
是继承自Android.view.ViewGroup.MarginLayoutParams
的,所以可以方便的使用各种margin。
这个方法最终会在recycler.getViewForPosition(i)
时调用到,在该方法浩长源码的最下方:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
重写完这个方法就能编译通过了,只不过然并卵,界面上是一片空白,下面我们就走进onLayoutChildren()
方法 ,为界面添加Item。
注:99%用不到的情况:如果需要存储一些额外的东西在LayoutParams
里,这里返回你自定义的LayoutParams
即可。
当然,你自定义的LayoutParams
需要继承自RecyclerView.LayoutParams
。
三 onLayoutChildren()
该方法是LayoutManager的入口。它会在如下情况下被调用:
1 在RecyclerView初始化时,会被调用两次。
2 在调用adapter.notifyDataSetChanged()时,会被调用。
3 在调用setAdapter替换Adapter时,会被调用。
4 在RecyclerView执行动画时,它也会被调用。
即RecyclerView 初始化 、 数据源改变时 都会被调用。
(关于初始化时为什么会被调用两次,我在系列第一篇文章里已经分析过。)
在系列开篇我已经提到,它相当于ViewGroup的onLayout()方法,所以我们需要在里面layout当前屏幕可见的所有子View,千万不要layout出所有的子View。本文如下编写:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
这个fill(recycler, state);
方法将是你自定义LayoutManager之旅一生的敌人,简单的说它承担了以下任务:
在考虑滑动位移的情况下:
1 回收所有屏幕不可见的子View
2 layout所有可见的子View
在这一节,我们先看一下它的简单版本,不考虑滑动位移,不考虑滑动方向等,只考虑初始化时,从头至尾,layout所有可见的子View,在下一节我会配合滑动事件放出它的完整版.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
用到的一些工具函数(在系列开篇已介绍过):
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
如上编写一个超级简单的fill()
方法,运行,你的程序应该就能看到流式布局的效果出现了。
可是千万别开心,因为痛苦的计算远没到来。
如果这些都看不懂,那么我建议:
一,直接下载完整代码,配合后面的章节看,看到后面也许前面的就好理解了= =。
二,去学习一下自定义ViewGroup的知识。
此时虽然界面上已经展示了流式布局的效果,可是它并不能滑动,下一节我们让它动起来。
四,动起来
想让我们自定义的LayoutManager动起来,最简单的写法如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
offsetChildrenVertical(-realOffset);
这句话移动所有的childView.
返回值会被RecyclerView用来判断是否达到边界, 如果返回值!=传入的dy,则会有一个边缘的发光效果,表示到达了边界。而且返回值还会被RecyclerView用于计算fling效果。
写完编译,哇塞,真的跟随手指滑动了,只不过能动的总共就我们在上一节layout的那些Item,Item并没有回收,也没有新的Item出现。
好了,下面开始正经的写它吧,
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
这里用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.
(我个人认为这两个方案都不太好,希望有朋友能提出更好的方案。)
下面上码:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
思路已经在前面讲解过,代码里也配上了注释,计算坐标等都是数学问题,略饶人,需要用笔在纸上写一写,或者运行调试调试。没啥好办法。
值得一提的是,可以通过getChildCount()
和recycler.getScrapList().size()
查看当前屏幕上的Item数量 和 scrapCache缓存区域的Item数量,合格的LayoutManager,childCount数量不应大于屏幕上显示的Item数量,而scrapCache缓存区域的Item数量应该是0.
官方的LayoutManager都是达标的,本例也是达标的,网上大部分文章的Demo,都是不合格的。。
原因在系列开篇也提过,不再赘述。
至此我们的自定义LayoutManager已经可以用了,使用的效果就和文首的两张图一模一样。
下面再提及一些其他注意点和适配事项:
五 适配notifyDataSetChanged()
此时会回调onLayoutChildren()函数。因为我们流式布局的特殊性,每个Item的宽度不一致,所以化简处理,每次这里归零。
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
如果每个Item的大小都一样,逆序顺序layoutChild都比较好处理,则应该在此判断,getChildCount(),大于0说明是DatasetChanged()操作,(初始化的第二次也会childCount>0)。根据当前记录的position和位移信息去fill视图即可。
六 适配 Adapter的替换。
我根据24.2.1源码,发现网上的资料对这里的处理其实是不必要的。
一 资料中的做法如下:
当对RecyclerView设置一个新的Adapter时,onAdapterChanged()
方法会被回调,一般的做法是在这里remove掉所有的View。此时onLayoutChildren()
方法会被再次调用,一个新的轮回开始。
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
二 我的新观点:
通过查看源码+打断点跟踪分析,调用RecyclerView.setAdapter后,调用顺序依次为
1 Recycler.setAdapter():
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
那么我们查看setAdapterInternal()
方法:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
也就是说 更换Adapter一开始,还没有执行到LayoutManager.onAdapterChanged()
,界面上的View都已经被remove掉了,我们的操作属于多余的。
2 LayoutManager.onAdapterChanged()
空实现:也没必要实现了
- 1
- 2
- 1
- 2
3 Recycler.onAdapterChanged():
该方法先清空scapCache区域(貌似也是多余,一开始被清空过了),然后调用RecyclerViewPool.onAdapterChanged()
。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
4 RecyclerViewPool.onAdapterChanged()
如果没有别的Adapter在用这个RecyclerViewPool,会清空RecyclerViewPool的缓存。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
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实现的流式布局
欢迎star,pr,issue。
- 【Android】掌握自定义LayoutManager(二) 实现流式布局
- Android 掌握自定义LayoutManager(二) 实现流式布局
- 掌握自定义LayoutManager之实现流式布局
- RecyclerView自定义LayoutManager实现横向瀑布流
- android RecyclerView自定义 LayoutManager
- RecylerView 自定义 LayoutManager 基础二
- 自定义LayoutManager实现android-pile-layout滑动卡片堆叠效果
- 【Android】掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,常用API。
- Android 掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,常用API。
- RecyclerView自定义LayoutManager,打造不规则布局
- RecyclerView自定义LayoutManager,打造不规则布局
- Android 自定义ViewGroup 实现流式布局
- Android自定义ViewGroup实现流式布局
- android 自定义view实现流式布局
- android 自定义控件实现流式布局
- Android自定义View实现流式布局
- “傻瓜”式填充,自定义LayoutManager
- Android中的自定义View(二)之 流式布局实现
- POJ 3745 : Training little cats(矩阵快速幂加优化)
- 哈希表等概率情况下查找成功的平均查找长度
- Java中使用IO流实现大文件的分裂与合并
- 知识点:page visibility (页面可见性)
- AndroidStudio下载、配色以及初始化配置
- Android 掌握自定义LayoutManager(二) 实现流式布局
- java非访问修饰符
- 使用caffe框架利用faster-rcnn来训练自己的数据集
- win7下编译VLC
- Struts2标签
- c++中“箭头(->)”和“点号(.)”操作符的区别
- 不可错过的仿IOS删除应用的晃动动画&文本标签编辑的晃动动画小案例
- 如何正确理解商业智能(BI)?
- 驱动复习(mor8)