【Flutter】熊孩子拆元件系列之拆ListView(七)—— SliverList的基礎結構

語言: CN / TW / HK

theme: condensed-night-purple

「這是我參與11月更文挑戰的第3天,活動詳情檢視:2021最後一次更文挑戰」。

前言

之前的所有內容,可以說是對ListView本身的操作可可見範圍之類的,做一個約束,並沒有涉及到展示內容;現在終於到了ListView本身部分的最後一步:SliverList,ListView的內容就是其展示出來的

SliverList也是不簡單的東西啊~

國際慣例,先看註釋

``` A sliver that places multiple box children in a linear array along the main axis.

Each child is forced to have the [SliverConstraints.crossAxisExtent] in the cross axis but determines its own main axis extent.

[SliverList] determines its scroll offset by "dead reckoning" because children outside the visible part of the sliver are not materialized, which means [SliverList] cannot learn their main axis extent. Instead, newly materialized children are placed adjacent to existing children.

{@youtube 560 315 http://www.youtube.com/watch?v=ORiTTaVY6mM}

If the children have a fixed extent in the main axis, consider using [SliverFixedExtentList] rather than [SliverList] because [SliverFixedExtentList] does not need to perform layout on its children to obtain their extent in the main axis and is therefore more efficient. ```

從字面意思上來看,SliverList本身是一個組合型Widget,提供的內容正是listView主要表現的那部分;

在這裡也提到了,SliverList本身無法獲取展示區域外的內容,說白了,只會計算包含可見範圍的一定區域內的東西,這也是官方沒提供諸如scrollerTo、animateToPostion之類功能的原因所在;

SliverList 的構成

SliverList 本身程式碼非常簡單,就寥寥不到20行的程式碼:

``` class SliverList extends SliverMultiBoxAdaptorWidget { /// Creates a sliver that places box children in a linear array. const SliverList({ Key? key, required SliverChildDelegate delegate, }) : super(key: key, delegate: delegate);

@override SliverMultiBoxAdaptorElement createElement() => SliverMultiBoxAdaptorElement(this, replaceMovedChildren: true);

@override RenderSliverList createRenderObject(BuildContext context) { final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement; return RenderSliverList(childManager: element); } } ``` 看來還是一樣,核心邏輯放到了RenderObject和Element這塊了;

先看看Element:

SliverMultiBoxAdaptorElement

還是先從註釋開始: ``` An element that lazily builds children for a [SliverMultiBoxAdaptorWidget].

Implements [RenderSliverBoxChildManager], which lets this element manage the children of subclasses of [RenderSliverMultiBoxAdaptor]. 從註釋上來看,這個Element的作用是通過實現RenderSliverBoxChildManager,來管理RenderSliverMultiBoxAdaptor```;

那麼按照註釋和程式碼的指引,來看一下這兩個類是什麼樣的:

RenderSliverBoxChildManager

首先是註釋: /// A delegate used by [RenderSliverMultiBoxAdaptor] to manage its children. /// /// [RenderSliverMultiBoxAdaptor] objects reify their children lazily to avoid /// spending resources on children that are not visible in the viewport. This /// delegate lets these objects create and remove children as well as estimate /// the total scroll offset extent occupied by the full child list. 跟前面說的部分沒啥太大差別……

由於其本身是個抽象類,那麼來看一下具體實現:

image.png

正好,其中就有之前出現的 SliverMultiBoxAdaptorElement

再看下具體結構:

image.png

看到一堆create、delete 之類的方法,大膽假設一波:

因為RenderObject樹會根據Element 樹做出相應改變;所以這個Element會被提供出去,供其他地方呼叫來通過create、delete之類的方法改變Element樹;

而在CreateRenderObject方法中,可以看到這個Element以childManager的形式被提供給了RenderObject;

看來RenderObject會持有這個childManager,並呼叫相應方法來完成手勢響應之類的操作效果;

RenderSliverMultiBoxAdaptor

其註釋是這樣的: ``` A sliver with multiple box children. [RenderSliverMultiBoxAdaptor] is a base class for slivers that have multiple box children. The children are managed by a [RenderSliverBoxChildManager], which lets subclasses create children lazily during layout. Typically subclasses will create only those children that are actually needed to fill the [SliverConstraints.remainingPaintExtent].

The contract for adding and removing children from this render object is more strict than for normal render objects:

  • Children can be removed except during a layout pass if they have already been laid out during that layout pass.
  • Children cannot be added except during a call to [childManager], and then only if there is no child corresponding to that index (or the child child corresponding to that index was first removed). ``` 簡單翻譯一下,意思就是說,RenderSliverMultiBoxAdaptor和其子類,其所持有的child都受RenderSliverBoxChildManager的嚴格管控,意思差不多這樣;

說白了,所有跟child相關的部分,都要先問下childManager;

這點也體現在了其中的各種方法中,舉個例子:

image.png

其中大部分方法,所做的事情的思路就是:

  • 如果keepAliveBucket這個map中有Item對應的index,那麼就從這個map中取,不用通過childManager來建立、銷燬之類的;

  • 如果沒有,那麼交給childManager來管理;

除了RenderSliverMultiBoxAdaptor本身,它的mixin也不少:

image.png

ContainerRenderObjectMixIn

image.png 首先,其註釋是這樣的:

``` /// Generic mixin for render objects with a list of children. /// /// Provides a child model for a render object subclass that has a doubly-linked /// list of children. /// /// The [ChildType] specifies the type of the children (extending [RenderObject]), /// e.g. [RenderBox]. /// /// [ParentDataType] stores parent container data on its child render objects. /// It must extend [ContainerParentDataMixin], which provides the interface /// for visiting children. This data is populated by /// [RenderObject.setupParentData] implemented by the class using this mixin. /// /// When using [RenderBox] as the child type, you will usually want to make use of /// [RenderBoxContainerDefaultsMixin] and extend [ContainerBoxParentData] for the /// parent data. /// /// Moreover, this is a required mixin for render objects returned to [MultiChildRenderObjectWidget].

``` 去掉一堆廢話之後,其作用其實就一句話:

提供一個child順序的雙向列表

通讀一下,其實核心邏輯也就兩個方法:_insertIntoChildList_removeFromChildListvoid _insertIntoChildList(ChildType child, { ChildType? after }) { final ParentDataType childParentData = child.parentData! as ParentDataType; assert(childParentData.nextSibling == null); assert(childParentData.previousSibling == null); _childCount += 1; assert(_childCount > 0); if (after == null) { // insert at the start (_firstChild) childParentData.nextSibling = _firstChild; if (_firstChild != null) { final ParentDataType _firstChildParentData = _firstChild!.parentData! as ParentDataType; _firstChildParentData.previousSibling = child; } _firstChild = child; _lastChild ??= child; } else { assert(_firstChild != null); assert(_lastChild != null); assert(_debugUltimatePreviousSiblingOf(after, equals: _firstChild)); assert(_debugUltimateNextSiblingOf(after, equals: _lastChild)); final ParentDataType afterParentData = after.parentData! as ParentDataType; if (afterParentData.nextSibling == null) { // insert at the end (_lastChild); we'll end up with two or more children assert(after == _lastChild); childParentData.previousSibling = after; afterParentData.nextSibling = child; _lastChild = child; } else { // insert in the middle; we'll end up with three or more children // set up links from child to siblings childParentData.nextSibling = afterParentData.nextSibling; childParentData.previousSibling = after; // set up links from siblings to child final ParentDataType childPreviousSiblingParentData = childParentData.previousSibling!.parentData! as ParentDataType; final ParentDataType childNextSiblingParentData = childParentData.nextSibling!.parentData! as ParentDataType; childPreviousSiblingParentData.nextSibling = child; childNextSiblingParentData.previousSibling = child; assert(afterParentData.nextSibling == child); } } }

void _removeFromChildList(ChildType child) { final ParentDataType childParentData = child.parentData! as ParentDataType; assert(_debugUltimatePreviousSiblingOf(child, equals: _firstChild)); assert(_debugUltimateNextSiblingOf(child, equals: _lastChild)); assert(_childCount >= 0); if (childParentData.previousSibling == null) { assert(_firstChild == child); _firstChild = childParentData.nextSibling; } else { final ParentDataType childPreviousSiblingParentData = childParentData.previousSibling!.parentData! as ParentDataType; childPreviousSiblingParentData.nextSibling = childParentData.nextSibling; } if (childParentData.nextSibling == null) { assert(_lastChild == child); _lastChild = childParentData.previousSibling; } else { final ParentDataType childNextSiblingParentData = childParentData.nextSibling!.parentData! as ParentDataType; childNextSiblingParentData.previousSibling = childParentData.previousSibling; } childParentData.previousSibling = null; childParentData.nextSibling = null; _childCount -= 1; }

作用其實也簡單,說白了,就是在連結串列中插入和刪除而已,維護child連結串列的順序,標明下一個child,上一個child是哪個;

實現方式也是通過獲取每個child 的 parentData,並修改更新其資料來實現的;

RenderSliverHelpers

``` /// Mixin for [RenderSliver] subclasses that provides some utility functions. mixin RenderSliverHelpers implements RenderSliver { bool _getRightWayUp(SliverConstraints constraints) { assert(constraints != null); assert(constraints.axisDirection != null); bool rightWayUp; switch (constraints.axisDirection) { case AxisDirection.up: case AxisDirection.left: rightWayUp = false; break; case AxisDirection.down: case AxisDirection.right: rightWayUp = true; break; } assert(constraints.growthDirection != null); switch (constraints.growthDirection) { case GrowthDirection.forward: break; case GrowthDirection.reverse: rightWayUp = !rightWayUp; break; } assert(rightWayUp != null); return rightWayUp; }

/// Utility function for [hitTestChildren] for use when the children are /// [RenderBox] widgets. /// /// This function takes care of converting the position from the sliver /// coordinate system to the Cartesian coordinate system used by [RenderBox]. /// /// This function relies on [childMainAxisPosition] to determine the position of /// child in question. /// /// Calling this for a child that is not visible is not valid. @protected bool hitTestBoxChild(BoxHitTestResult result, RenderBox child, { required double mainAxisPosition, required double crossAxisPosition }) { final bool rightWayUp = _getRightWayUp(constraints); double delta = childMainAxisPosition(child); final double crossAxisDelta = childCrossAxisPosition(child); double absolutePosition = mainAxisPosition - delta; final double absoluteCrossAxisPosition = crossAxisPosition - crossAxisDelta; Offset paintOffset, transformedPosition; assert(constraints.axis != null); switch (constraints.axis) { case Axis.horizontal: if (!rightWayUp) { absolutePosition = child.size.width - absolutePosition; delta = geometry!.paintExtent - child.size.width - delta; } paintOffset = Offset(delta, crossAxisDelta); transformedPosition = Offset(absolutePosition, absoluteCrossAxisPosition); break; case Axis.vertical: if (!rightWayUp) { absolutePosition = child.size.height - absolutePosition; delta = geometry!.paintExtent - child.size.height - delta; } paintOffset = Offset(crossAxisDelta, delta); transformedPosition = Offset(absoluteCrossAxisPosition, absolutePosition); break; } assert(paintOffset != null); assert(transformedPosition != null); return result.addWithOutOfBandPosition( paintOffset: paintOffset, hitTest: (BoxHitTestResult result) { return child.hitTest(result, position: transformedPosition); }, ); }

/// Utility function for [applyPaintTransform] for use when the children are /// [RenderBox] widgets. /// /// This function turns the value returned by [childMainAxisPosition] and /// [childCrossAxisPosition]for the child in question into a translation that /// it then applies to the given matrix. /// /// Calling this for a child that is not visible is not valid. @protected void applyPaintTransformForBoxChild(RenderBox child, Matrix4 transform) { final bool rightWayUp = _getRightWayUp(constraints); double delta = childMainAxisPosition(child); final double crossAxisDelta = childCrossAxisPosition(child); assert(constraints.axis != null); switch (constraints.axis) { case Axis.horizontal: if (!rightWayUp) delta = geometry!.paintExtent - child.size.width - delta; transform.translate(delta, crossAxisDelta); break; case Axis.vertical: if (!rightWayUp) delta = geometry!.paintExtent - child.size.height - delta; transform.translate(crossAxisDelta, delta); break; } } } ```

根據註釋來看,這塊的部分主要是一些方便使用的工具類,可以看到,是為了讓沒有KeepAlive的控制元件,能根據實際的位置來判斷,所以重寫了HitTest和paint方法;

RenderSliverWithKeepAliveMixin

/// This class exists to dissociate [KeepAlive] from [RenderSliverMultiBoxAdaptor]. /// /// [RenderSliverWithKeepAliveMixin.setupParentData] must be implemented to use /// a parentData class that uses the right mixin or whatever is appropriate. mixin RenderSliverWithKeepAliveMixin implements RenderSliver { /// Alerts the developer that the child's parentData needs to be of type /// [KeepAliveParentDataMixin]. @override void setupParentData(RenderObject child) { assert(child.parentData is KeepAliveParentDataMixin); } } 這個方法的作用也就一個,assert一下child的parentData是否合法;

RenderSliverList

現在來到了RenderSliverList這塊,從繼承關係這塊,可以看到RenderSliverList就是RenderSliverMultiBoxAdapter的子類:

image.png

而實際上,RenderSliverList所重寫的部分也就一處:

image.png

那麼很明顯了,驅動這一切執行的地方,就放在這個重寫的performLayout中;

總結

現在可以公開的情報:

SLiverList中,是由被當作ChildManager來使用的Element,和具體繪製體現child的RenderObject組合而成的;

Element的作用就是建立銷燬child,維護SliverList的Element 樹,進而觸發RenderObject繪製,在此過程中,它是一個懶載入的模式,只會載入可見範圍和附近的快取大小內的部分;

RenderObject 會在layout 方法中觸發 ChildManager的建立銷燬child的方法,修改Element樹和RenderObject樹;同時維護了一個child順序的雙向連結串列;

下面一片就來看下這個performLayout方法具體是如何呼叫childManager,觸發依據和條件是哪些

「其他文章」