【Flutter】熊孩子拆元件系列之拆ListView(七)—— SliverList的基礎結構
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.
跟前面說的部分沒啥太大差別……
由於其本身是個抽象類,那麼來看一下具體實現:
正好,其中就有之前出現的 SliverMultiBoxAdaptorElement
再看下具體結構:
看到一堆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;
這點也體現在了其中的各種方法中,舉個例子:
其中大部分方法,所做的事情的思路就是:
-
如果keepAliveBucket這個map中有Item對應的index,那麼就從這個map中取,不用通過childManager來建立、銷燬之類的;
-
如果沒有,那麼交給childManager來管理;
除了RenderSliverMultiBoxAdaptor本身,它的mixin也不少:
ContainerRenderObjectMixIn
首先,其註釋是這樣的:
``` /// 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
和 _removeFromChildList
;
void _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的子類:
而實際上,RenderSliverList所重寫的部分也就一處:
那麼很明顯了,驅動這一切執行的地方,就放在這個重寫的performLayout中;
總結
現在可以公開的情報:
SLiverList中,是由被當作ChildManager來使用的Element,和具體繪製體現child的RenderObject組合而成的;
Element的作用就是建立銷燬child,維護SliverList的Element 樹,進而觸發RenderObject繪製,在此過程中,它是一個懶載入的模式,只會載入可見範圍和附近的快取大小內的部分;
RenderObject 會在layout 方法中觸發 ChildManager的建立銷燬child的方法,修改Element樹和RenderObject樹;同時維護了一個child順序的雙向連結串列;
下面一片就來看下這個performLayout方法具體是如何呼叫childManager,觸發依據和條件是哪些
- 【Flutter】小說閱讀器改版 —— 翻頁動畫(三)
- 【Flutter】小說閱讀器改版 (六)—— 在動畫播放中攔截手勢
- 【Flutter】小說閱讀器改版 (五)—— 整合ScrollActivity
- 【Flutter】小說閱讀器改版 (四)—— 讓ScrollActivity追蹤手勢最新位置
- 【Flutter】小說閱讀器改版 (三)—— 實現支援 Drag 的ScrollActivity
- 【Flutter】小說閱讀器改版 (二)—— 改進一下模擬翻頁的效果
- 【Flutter】小說閱讀器改版 (一)—— 模擬翻頁的思路優化
- 【Flutter】自定義ListView開發記錄(五)—— 提供手勢等資訊
- 【Flutter】自定義ListView開發記錄(四)—— 關於ParentData的設想和分析與簡單實踐
- 【Flutter】自定義ListView開發記錄(三)—— 處理HitTest手勢事件
- 【Flutter】自定義ListView開發記錄(二)——設計LayoutManager
- 【Flutter】自定義ListView開發記錄(一)——設計滑動效果的處理方式
- 【Flutter】熊孩子拆元件系列之拆ListView(十)—— 按自己的方式組裝修改ListView
- 【Flutter】熊孩子拆元件系列之拆ListView(九)—— AutomaticKeepAlive和KeepAlive
- 【Flutter】熊孩子拆元件系列之拆ListView(八)—— SliverList的運作機制
- 【Flutter】熊孩子拆元件系列之拆ListView(七)—— SliverList的基礎結構
- 【Flutter】熊孩子拆元件系列之拆ListView(六)—— SliverPadding
- 【Flutter】熊孩子拆元件系列之拆ListView(五)—— ViewPort
- 【Flutter】熊孩子拆元件系列之拆ListView(四)—— _ScrollableScope
- 【Flutter】熊孩子拆元件系列之拆ListView(三)—— GlowingOverscrollIndicator