即RecyclerView 初始化 、 数据源改变时 都会被调用。
(关于初始化时为什么会被调用两次,我在系列第一篇文章里已经分析过。)
在系列开篇我已经提到,它相当于ViewGroup的onLayout()方法,所以我们需要在里面layout当前屏幕可见的所有子View,千万不要layout出所有的子View。本文如下编写:
private int mVerticalOffset;//竖直偏移量 每次换行时,要根据这个offset判断
private int mFirstVisiPos;//屏幕可见的第一个View的Position
private int mLastVisiPos;//屏幕可见的最后一个View的Position
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {//没有Item,界面空着吧
detachAndScrapAttachedViews(recycler);
return;
}
if (getChildCount() == 0 && state.isPreLayout()) {//state.isPreLayout()是支持动画的
return;
}
//onLayoutChildren方法在RecyclerView 初始化时 会执行两遍
detachAndScrapAttachedViews(recycler);
//初始化
mVerticalOffset = 0;
mFirstVisiPos = 0;
mLastVisiPos = getItemCount();
//初始化时调用 填充childView
fill(recycler, state);
}
这个fill(recycler, state);
方法将是你自定义LayoutManager之旅一生的敌人,简单的说它承担了以下任务:
在考虑滑动位移的情况下:
1 回收所有屏幕不可见的子View
2 layout所有可见的子View
在这一节,我们先看一下它的简单版本,不考虑滑动位移,不考虑滑动方向等,只考虑初始化时,从头至尾,layout所有可见的子View,在下一节我会配合滑动事件放出它的完整版.
int topOffset = getPaddingTop();//布局时的上偏移
int leftOffset = getPaddingLeft();//布局时的左偏移
int lineMaxHeight = 0;//每一行最大的高度
int minPos = mFirstVisiPos;//初始化时,我们不清楚究竟要layout多少个子View,所以就假设从0~itemcount-1
mLastVisiPos = getItemCount() - 1;
//顺序addChildView
for (int i = minPos; i <= mLastVisiPos; i++) {
//找recycler要一个childItemView,我们不管它是从scrap里取,还是从RecyclerViewPool里取,亦或是onCreateViewHolder里拿。
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
//计算宽度 包括margin
if (leftOffset + getDecoratedMeasurementHorizontal(child) <= getHorizontalSpace()) {//当前行还排列的下
layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));
//改变 left lineHeight
leftOffset += getDecoratedMeasurementHorizontal(child);
lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));
} else {//当前行排列不下
//改变top left lineHeight
leftOffset = getPaddingLeft();
topOffset += lineMaxHeight;
lineMaxHeight = 0;
//新起一行的时候要判断一下边界
if (topOffset - dy > getHeight() - getPaddingBottom()) {
//越界了 就回收
removeAndRecycleView(child, recycler);
mLastVisiPos = i - 1;
} else {
layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));
//改变 left lineHeight
leftOffset += getDecoratedMeasurementHorizontal(child);
lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));
}
}
}
用到的一些工具函数(在系列开篇已介绍过):
//模仿LLM Horizontal 源码
/**
-
获取某个childView在水平方向所占的空间
-
@param view
-
@return
*/
public int getDecoratedMeasurementHorizontal(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredWidth(view) + params.leftMargin
- params.rightMargin;
}
/**
-
获取某个childView在竖直方向所占的空间
-
@param view
-
@return
*/
public int getDecoratedMeasurementVertical(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return getDecoratedMeasuredHeight(view) + params.topMargin
- params.bottomMargin;
}
public int getVerticalSpace() {
return getHeight() - getPaddingTop() - getPaddingBottom();
}
public int getHorizontalSpace() {
return getWidth() - getPaddingLeft() - getPaddingRight();
}
如上编写一个超级简单的fill()
方法,运行,你的程序应该就能看到流式布局的效果出现了。
可是千万别开心,因为痛苦的计算远没到来。
如果这些都看不懂,那么我建议:
一,直接下载完整代码,配合后面的章节看,看到后面也许前面的就好理解了= =。
二,去学习一下自定义ViewGroup的知识。
此时虽然界面上已经展示了流式布局的效果,可是它并不能滑动,下一节我们让它动起来。
四,动起来
=====
想让我们自定义的LayoutManager动起来,最简单的写法如下:
@Override
public boolean canScrollVertically() {
return true;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
int realOffset = dy;//实际滑动的距离, 可能会在边界处被修复
offsetChildrenVertical(-realOffset);
return realOffset;
}
offsetChildrenVertical(-realOffset);
这句话移动所有的childView.
返回值会被RecyclerView用来判断是否达到边界, 如果返回值!=传入的dy,则会有一个边缘的发光效果,表示到达了边界。而且返回值还会被RecyclerView用于计算fling效果。
写完编译,哇塞,真的跟随手指滑动了,只不过能动的总共就我们在上一节layout的那些Item,Item并没有回收,也没有新的Item出现。
好了,下面开始正经的写它吧,
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
//位移0、没有子View 当然不移动
if (dy == 0 || getChildCount() == 0) {
return 0;
}
int realOffset = dy;//实际滑动的距离, 可能会在边界处被修复
//边界修复代码
if (mVerticalOffset + realOffset < 0) {//上边界
realOffset = -mVerticalOffset;
} else if (realOffset > 0) {//下边界
//利用最后一个子View比较修正
View lastChild = getChildAt(getChildCount() - 1);
if (getPosition(lastChild) == getItemCount() - 1) {
int gap = getHeight() - getPaddingBottom() - getDecoratedBottom(lastChild);
if (gap > 0) {
realOffset = -gap;
} else if (gap == 0) {
realOffset = 0;
} else {
realOffset = Math.min(realOffset, -gap);
}
}
}
realOffset = fill(recycler, state, realOffset);//先填充,再位移。
mVerticalOffset += realOffset;//累加实际滑动距离
offsetChildrenVertical(-realOffset);//滑动
return realOffset;
}
这里用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.
(我个人认为这两个方案都不太好,希望有朋友能提出更好的方案。)
下面上码:
private SparseArray
/**
-
填充childView的核心方法,应该先填充,再移动。
-
在填充时,预先计算dy的在内,如果View越界,回收掉。
-
一般情况是返回dy,如果出现View数量不足,则返回修正后的dy.
-
@param recycler
-
@param state
-
@param dy RecyclerView给我们的位移量,+,显示底端, -,显示头部
-
@return 修正以后真正的dy(可能剩余空间不够移动那么多了 所以return <|dy|)
*/
private int fill(RecyclerView.Recycler recycler, RecyclerView.State state, int dy) {
int topOffset = getPaddingTop();
//回收越界子View
if (getChildCount() > 0) {//滑动时进来的
for (int i = getChildCount() - 1; i >= 0; i–) {
View child = getChildAt(i);
if (dy > 0) {//需要回收当前屏幕,上越界的View
if (getDecoratedBottom(child) - dy < topOffset) {
removeAndRecycleView(child, recycler);
mFirstVisiPos++;
continue;
}
} else if (dy < 0) {//回收当前屏幕,下越界的View
if (getDecoratedTop(child) - dy > getHeight() - getPaddingBottom()) {
removeAndRecycleView(child, recycler);
mLastVisiPos–;
continue;
}
}
}
//detachAndScrapAttachedViews(recycler);
}
int leftOffset = getPaddingLeft();
int lineMaxHeight = 0;
//布局子View阶段
if (dy >= 0) {
int minPos = mFirstVisiPos;
mLastVisiPos = getItemCount() - 1;
if (getChildCount() > 0) {
View lastView = getChildAt(getChildCount() - 1);
minPos = getPosition(lastView) + 1;//从最后一个View+1开始吧
topOffset = getDecoratedTop(lastView);
leftOffset = getDecoratedRight(lastView);
lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(lastView));
}
//顺序addChildView
for (int i = minPos; i <= mLastVisiPos; i++) {
//找recycler要一个childItemView,我们不管它是从scrap里取,还是从RecyclerViewPool里取,亦或是onCreateViewHolder里拿。
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
//计算宽度 包括margin
if (leftOffset + getDecoratedMeasurementHorizontal(child) <= getHorizontalSpace()) {//当前行还排列的下
layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));
//保存Rect供逆序layout用
Rect rect = new Rect(leftOffset, topOffset + mVerticalOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child) + mVerticalOffset);
mItemRects.put(i, rect);
//改变 left lineHeight
leftOffset += getDecoratedMeasurementHorizontal(child);
lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));
} else {//当前行排列不下
//改变top left lineHeight
leftOffset = getPaddingLeft();
topOffset += lineMaxHeight;
lineMaxHeight = 0;
//新起一行的时候要判断一下边界
if (topOffset - dy > getHeight() - getPaddingBottom()) {
//越界了 就回收
removeAndRecycleView(child, recycler);
mLastVisiPos = i - 1;
} else {
layoutDecoratedWithMargins(child, leftOffset, topOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child));
//保存Rect供逆序layout用
Rect rect = new Rect(leftOffset, topOffset + mVerticalOffset, leftOffset + getDecoratedMeasurementHorizontal(child), topOffset + getDecoratedMeasurementVertical(child) + mVerticalOffset);
mItemRects.put(i, rect);
//改变 left lineHeight
leftOffset += getDecoratedMeasurementHorizontal(child);
lineMaxHeight = Math.max(lineMaxHeight, getDecoratedMeasurementVertical(child));
}
}
}
//添加完后,判断是否已经没有更多的ItemView,并且此时屏幕仍有空白,则需要修正dy
View lastChild = getChildAt(getChildCount() - 1);
if (getPosition(lastChild) == getItemCount() - 1) {
int gap = getHeight() - getPaddingBottom() - getDecoratedBottom(lastChild);
if (gap > 0) {
dy -= gap;
}
}
} else {
/**
正序排列时,保存每个子View的Rect,逆序时,直接拿出来layout。
*/
int maxPos = getItemCount() - 1;
mFirstVisiPos = 0;
if (getChildCount() > 0) {
View firstView = getChildAt(0);
maxPos = getPosition(firstView) - 1;
}
for (int i = maxPos; i >= mFirstVisiPos; i–) {
Rect rect = mItemRects.get(i);
if (rect.bottom - mVerticalOffset - dy < getPaddingTop()) {
mFirstVisiPos = i + 1;
break;
} else {
View child = recycler.getViewForPosition(i);
addView(child, 0);//将View添加至RecyclerView中,childIndex为1,但是View的位置还是由layout的位置决定
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left, rect.top - mVerticalOffset, rect.right, rect.bottom - mVerticalOffset);
}
}
}
Log.d(“TAG”, “count= [” + getChildCount() + “]” + “,[recycler.getScrapList().size():” + recycler.getScrapList().size() + “, dy:” + dy + “, mVerticalOffset” + mVerticalOffset+", ");
return dy;
}
思路已经在前面讲解过,代码里也配上了注释,计算坐标等都是数学问题,略饶人,需要用笔在纸上写一写,或者运行调试调试。没啥好办法。
值得一提的是,可以通过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源码,发现网上的资料对这里的处理其实是不必要的。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
最后这里放上我这段时间复习的资料,这个资料也是偶然一位朋友分享给我的,里面包含了腾讯、字节跳动、阿里、百度2020-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。
还有 高级架构技术进阶脑图、高级进阶架构资料 帮助大家学习提升进阶,这里我也免费分享给大家也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
一起互勉~
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
7)]
[外链图片转存中…(img-ZxGssdRr-1712683266277)]
[外链图片转存中…(img-sblMBJlk-1712683266278)]
[外链图片转存中…(img-jvFzMhQo-1712683266278)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
最后这里放上我这段时间复习的资料,这个资料也是偶然一位朋友分享给我的,里面包含了腾讯、字节跳动、阿里、百度2020-2021面试真题解析,并且把每个技术点整理成了视频和PDF(知识脉络 + 诸多细节)。
还有 高级架构技术进阶脑图、高级进阶架构资料 帮助大家学习提升进阶,这里我也免费分享给大家也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
[外链图片转存中…(img-tqZWriLR-1712683266278)]
[外链图片转存中…(img-XT9kzKO7-1712683266279)]
一起互勉~
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!