一「表」走天下,Flutter瀑布流及通用列表解决方案
Flutter中的列表视图介绍
▐ Scrollable
Scrollable是一个StatefulWidget, 职责是监听用户的手势输入。其State的build方法会返回一个含有Listener和RawGestureDetector的Viewport。ScrollPosition用于描述其位置信息,并在其内部定义了 onStart, onUpdate, onEnd等回调。Scrollable中的每一次滑动的开始到结束都对应于一个Darg对象,并且会发送滑动的通知。而Viewport则负责对通知进行监听。
▐ Sliver
Flutter有两种布局体系 Box, Sliver。在layout的过程中,每个Sliver 都接收 SliverConstraints 计算返回一个 SliverGeometry,可以类比于RenderBox 接收 BoxConstraints 返回一个 Size。Sliver由Viewport统一来负责进行管理。
▐ Viewport
A widget that is bigger on the inside.
Viewport持有一个或多个Sliver。Scrollable将offset传递给Viewport, 由Viewport决定哪些Sliver应该是Visible。Viewport本质上是一个MultiChildRenderObjectWidget,也就是整个滚动视图的主要渲染逻辑都在Viewport中完成。
而在performLayout中,_attemptLayout会以center为中心,先布局leading方向的child,再布局trailing方向的child。其中只有dirty的child会被布局。
do {correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);if (correction != 0.0) {offset.correctBy(correction);} else {if (offset.applyContentDimensions(math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),))break;}count += 1;} while (count < _maxLayoutCycles);
如果attemptLayout返回了一个非0的correction, 就会打断当前布局的过程,需要对offset进行调整后重新开始布局,最多只能连续打断10次(maxLayoutCycles)。
void _paintWithContext(PaintingContext context, Offset offset) {// 重新布局就不需要调整offset了.if (_needsLayout)return;_needsPaint = false;paint(context, offset);}
WatetfallFlow的布局过程中需要指定Child的Offset,然后对其进行布局。所以需要继承SliverMultiBoxAtaptor,依赖于其将SliverConstraints转换为BoxConstraints的能力。我们也可以使用其SliverBoxChildManager, 方便控制Child的懒加载过程。
▐ 核心逻辑
-
在滑动的过程中找到其边缘最近的child,在其后(前)进行添加child,并对child进行layout。 -
在child离开一定距离后进行GC。 -
保证layout方法被尽可能少的调用. 上文有提过layout会调用performLayout而不能直接进行paint。
for (int index = firstIndex; index <= targetLastIndex; ++index) {final SliverGeometry gridGeometry = layout.getGeometryForChildIndex(index);final BoxConstraints childConstraints = gridGeometry.getBoxConstraints(constraints);RenderBox child = childAfter(trailingChildWithLayout);if (child == null || indexOf(child) != index) {// 重新获取Child.child = _createAndLayoutChildIfNeeded(childConstraints, after: trailingChildWithLayout);if (child != null && indexOf(child) == index) {_layoutedChilds.add(index);}else if (child == null) {// Child已经用尽.break;}} else {if (!_layoutedChilds.contains(index)) {_layoutChildIfNeeded(child, parentUsesSize: true);_layoutedChilds.add(index);}}trailingChildWithLayout = child;}
if (firstChild != null) {// 上一次的最先最末Child.final int oldFirstIndex = indexOf(firstChild);final int oldLastIndex = indexOf(lastChild);// 前后需要GC的child数量final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount);final int trailingGarbage = targetLastIndex == null? 0 : (oldLastIndex - targetLastIndex).clamp(0, childCount);// GCcollectGarbage(leadingGarbage, trailingGarbage);_layoutedChilds.sort();_layoutedChilds.removeRange(0, leadingGarbage);_layoutedChilds.removeRange(layoutedChilds.length - 1 - trailingGarbage,layoutedChilds.length - 1);} else {collectGarbage(0, 0);}
