在这篇文章 Flutter ListView 是如何滚动的?中引出了这个问题,那么今天就来具体分析下 item 到底是如何复用的。
下边两张动图都是代码写的,突然觉得画图软件没那么好用了。会敲代码,我想画什么就画什么,想怎么画就怎么画!
1. SliverConstraints 说明
说下重点。scrollOffset 为滑动偏移量。
2. 来看看 performLayout 方法吧
performLayout 方法将近300,虽然有很多注释,可是看起来也是十分头疼!逐行往下看,你可以试试,😩😩😩。
2.1 ListView 初始化布局过程
先上图吧,我觉得先上图,比先上代码说明直观易懂。
很简单,那我们结合代码具体分析下这个过程。
首先,我们要先插入,接着布局第一个 child。
class RenderSliverList extends RenderSliverMultiBoxAdaptor {
...
// Make sure we have at least one child to start from.
if (firstChild == null) {
if (!addInitialChild()) {
... // 在判断条件里插入的 firstChild
}
} ...
assert(earliestUsefulChild == firstChild);
// Make sure we've laid out at least one child.
if (leadingChildWithLayout == null) {
earliestUsefulChild!.layout(childConstraints, parentUsesSize: true);
...
}
...
}
下边这块代码负责插入 child 。代码好长,不想插代码了。。。
吐槽掘金插入代码块的组件,不能识别代码自己换行!!!
bool inLayoutRange = true;
RenderBox? child = earliestUsefulChild;
int index = indexOf(child!);
double endScrollOffset = childScrollOffset(child)! + paintExtentOf(child);
bool advance() { // returns true if we advanced, false if we have no more children
// This function is used in two different places below, to avoid code duplication.
assert(child != null);
if (child == trailingChildWithLayout)
inLayoutRange = false;
child = childAfter(child!);
if (child == null)
inLayoutRange = false;
index += 1;
if (!inLayoutRange) {
if (child == null || indexOf(child!) != index) {
// We are missing a child. Insert it (and lay it out) if possible. child = insertAndLayoutChild(childConstraints,
after: trailingChildWithLayout,
parentUsesSize: true,
);
if (child == null) {
// We have run out of children.
return false;
}
} else {
// Lay out the child.
child!.layout(childConstraints, parentUsesSize: true);
}
trailingChildWithLayout = child;
}
assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData = child!.parentData as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = endScrollOffset;
assert(childParentData.index == index);
endScrollOffset = childScrollOffset(child!)! + paintExtentOf(child!);
return true;
}
关键点来了,下面的判断决定了是否继续插入 child。
while (endScrollOffset < targetEndScrollOffset) {
if (!advance()) {
reachedEnd = true;
break;
}
}
这里有个问题,cacheExtent 是什么,为什么是 160?
cacheExtent 是视图缓存区,160 是我随便写的。😏😏😏
好吧,cacheExtent 是有系统默认值的。
abstract class RenderAbstractViewport extends RenderObject {
...
static const double defaultCacheExtent = 250.0;
}
250,你开心咯😛😛😛
初始化完了,该滚了。。。
等等,判断条件不说说?不慌,下边更精彩。
2.2 ListView 上滑解析
先来看一张图,一图胜千言。这么好的图,我都不想讲代码了。。。
属性都在图里,用代码简单描述下关系。
fakeScrollOffset; // 视图滑出屏幕的偏移
// 注意 cacheOrigin 是负数,最小为 -250
// scrollOffset 为视图滑出包括缓冲区在内的偏移,前 250 个像素单位为 0
scrollOffset = fakeScrollOffset + cacheOrigin;
// 主轴方向视图尺寸,上下缓冲区
fullCacheExtent = mainAxisExtent + 2 * _caculatedCacheExtent;
// 剩余可布局范围
remainCacheExtent = (mainAxisExtent + _caculatedCacheExtent + offset.pixels).clamp(0, fullCacheExtent);
// 可以理解为整个视图的尺寸
targetEndScrollOffset = scrollOffset + remainCacheExtent;
// 注意这里不是线性递增的!最后一个 child 的偏移 + child 的绘制范围
endScrollOffset = childScrollOffset(lastChild) + paintExtentOf(lastChild);
还有谁!(能比我讲的更清楚)
2.3 ListView 上滑,底部如何变化?
不想讲。
2.4 ListView 上滑,头部如何变化?
不想讲。
2.5 ListView 该下滑了
不想讲。
2.6 performLayout 方法简单说明
在“敲”上边那个动图时,我感觉我实现了一遍 performLayout。。。
只能说是简化版。下面具体看看 performLayout 剩余代码吧。
这块代码回收顶部 child。
if (childScrollOffset(firstChild!) == null) {
int leadingChildrenWithoutLayoutOffset = 0;
while (childScrollOffset(earliestUsefulChild!) == null) {
earliestUsefulChild = childAfter(firstChild!);
leadingChildrenWithoutLayoutOffset += 1;
}
// We should be able to destroy children with null layout offset safely,
// because they are likely outside of viewport
collectGarbage(leadingChildrenWithoutLayoutOffset, 0);
assert(firstChild != null);
}
这块代码主要决定下滑过程中是否需要在头部添加 child。以及下滑一些其它复杂情况的处理。
earliestUsefulChild = firstChild;
for (double earliestScrollOffset = childScrollOffset(earliestUsefulChild!)!;
earliestScrollOffset > scrollOffset;
earliestScrollOffset = childScrollOffset(earliestUsefulChild)!) {
// We have to add children before the earliestUsefulChild. earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
if (earliestUsefulChild == null) {
final SliverMultiBoxAdaptorParentData childParentData = firstChild!.parentData as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
if (scrollOffset == 0.0) {
// insertAndLayoutLeadingChild only lays out the children before
// firstChild. In this case, nothing has been laid out. We have
// to lay out firstChild manually.
firstChild!.layout(childConstraints, parentUsesSize: true);
earliestUsefulChild = firstChild;
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
break;
} else {
// We ran out of children before reaching the scroll offset.
// We must inform our parent that this sliver cannot fulfill
// its contract and that we need a scroll offset correction. geometry = SliverGeometry(
scrollOffsetCorrection: -scrollOffset,
);
return;
}
}
final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild!);
// firstChildScrollOffset may contain double precision error
if (firstChildScrollOffset < -precisionErrorTolerance) {
// Let's assume there is no child before the first child. We will
// correct it on the next layout if it is not. geometry = SliverGeometry(
scrollOffsetCorrection: -firstChildScrollOffset,
);
final SliverMultiBoxAdaptorParentData childParentData = firstChild!.parentData as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
return;
}
final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild.parentData as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = firstChildScrollOffset;
assert(earliestUsefulChild == firstChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
}
滑动到顶部的一些判断处理。
if (scrollOffset < precisionErrorTolerance) {
// We iterate from the firstChild in case the leading child has a 0 paint
// extent.
while (indexOf(firstChild!) > 0) {
final double earliestScrollOffset = childScrollOffset(firstChild!)!;
// We correct one child at a time. If there are more children before
// the earliestUsefulChild, we will correct it once the scroll offset
// reaches zero again.
earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
assert(earliestUsefulChild != null);
final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild!);
final SliverMultiBoxAdaptorParentData childParentData = firstChild!.parentData as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
// We only need to correct if the leading child actually has a
// paint extent.
if (firstChildScrollOffset < -precisionErrorTolerance) {
geometry = SliverGeometry(
scrollOffsetCorrection: -firstChildScrollOffset,
);
return;
}
}
}
其余代码基本只剩底部 child 回收,和计算 geometry 了。
3. 最后
不要执着陷入代码细节。知其意,忘其形,无招胜有招。
今天的文章Flutter ListView 是如何管理 item 的?分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/18470.html