Flutter | 佈局流程

語言: CN / TW / HK

theme: channing-cyan

淺談佈局過程

Layout(佈局)過程中是確定每一個元件的資訊(大小和位置),Flutter 中的佈局過程如下:

1,父節點向子節點傳遞約束資訊,限制子節點的最大和最小寬高。

2,子節點根據自己的約束資訊來確定自己的大小(Szie)。

3,父節點根據特定的規則(不同的元件會有不同的佈局演算法)確定每一個子節點在父節點空間中的位置,用偏移 offset表示。

4,遞迴整個過程,確定出每一個節點的位置和大小。

可以看到,元件的大小是由自身來決定的,而元件的位置是由父元件來決定的。

Flutter 中佈局類元件有很多,根據孩子數量可以分為單子元件和多子元件,下面我們分別定義一個單子元件和多子元件來深入理解一下 Fluuter 佈局過程。


單子元件佈局示例

我們自定義一個單子元件 CustomCenter。公告基本和 Center 一樣,通過這個示例我們演示一下佈局的主要流程。

為了展示原理,我們不採用組合的方式來實現元件,而是通過定製的 RenderObject 的方式來實現。因為居中元件需要包含一個子節點,所以我們繼承 SingleChildRenderObjectWidget。

```dart class CustomCenter extends SingleChildRenderObjectWidget { const CustomCenter({Key key, @required Widget child}) : super(key: key, child: child);

@override RenderObject createRenderObject(BuildContext context) { return RenderCustomCenter(); } } ```

接著實現 RenderCustomCenter:

```dart class RenderCustomCenter extends RenderShiftedBox { RenderCustomCenter({RenderBox child}) : super(child);

@override void performLayout() { //1,先對子元件進行 layout,隨後獲取他的 size child.layout(constraints.loosen(), //將約束傳遞給子節點 parentUsesSize: true //因為接下來要使用 child 的 size,所以不能為 false ); //2,根據子元件的大小確定自身的大小 size = constraints.constrain(Size( constraints.maxWidth == double.infinity ? child.size.width : double.infinity, constraints.maxHeight == double.infinity ? child.size.height : double.infinity));

//3,根據父節點大小,算出子節點在父節點中居中後的偏移,
//然後將這個偏移儲存在子節點的 parentData 中,在後續的繪製節點會用到
BoxParentData parentData = child.parentData as BoxParentData;
parentData.offset = ((size - child.size) as Offset) / 2;

} } ```

上面程式碼本來繼承 RenderObject 會更底層一點,但是這需要我們手動實現一些和佈局無關的東西,比如事件分發等邏輯。為了更聚焦佈局本身,我們選擇繼承 RenderShiftedBox,他會幫我們實現佈局之外的功能,這樣我們只需要重寫 performLayout。在改函式中實現居中演算法即可。

佈局過程如上註釋所示,在此之外還有三點需要說明:

  1. 在對子元件進行 Layout 的時候,constraints 是 CustomCenter 的父元件傳遞給自己的約束資訊,我們傳遞給位元組的的約束資訊是 constraints.loosen(),下面看一下 lossen 的實現:

dart BoxConstraints loosen() { assert(debugAssertIsValid()); return BoxConstraints( minWidth: 0.0, maxWidth: maxWidth, minHeight: 0.0, maxHeight: maxHeight, ); }

很明顯,CustomCenter 約束位元組的最大寬高不能超過自身的最大寬高

  1. 子節點在父節點(CustomCenter) 的約束下,確定自己的寬高。此時 CustomCenter 會根據子節點的寬高來確定自己的寬高。

上面的程式碼邏輯是,如果父節點的約束是無限大,他的寬高就是位元組的寬高,否則自己寬高為無限大。

需要注意的是,如果這個時候將 CustomCenter 的寬高也設定為無限大就會有問題,因為在一個無限大的範圍內自己的寬高也是無限大的話,那麼自己的父節點會懵逼的。螢幕的大小是固定的,這顯然很不合理。

如果CustomCenter 父節點傳遞的寬高不是無限大,那麼這個時候是可以設定自己的寬高為無限大,因為在一個有限的空間內,子節點設定無限大也就是父節點的大小。

簡而言之,CustomCenter 會盡可能讓自己填滿父元素的空間

  1. CustomCenter 確定了自己的大小和子節點的大小之後就可以確定子節點的位置了。根據居中演算法,將子節點的原點座標計算出來後儲存在子節點的 parentData 中,在後續的繪製階段會用到,具體如何使用我們看一下 RenderShiftedBox 中的預設實現:

dart @override void paint(PaintingContext context, Offset offset) { if (child != null) { final BoxParentData parentData = child.parentData as BoxParentData; //從 child。parentData 中取出子節點相對當前節點的偏移,加上當前節點在螢幕中的偏移 //便是子節點在螢幕中的偏移 context.paintChild(child, parentData.offset + offset); } }

PerformLayout

通過上面可以看到,佈局的邏輯是在 performLayout 方法中實現的,我們總結一下 performLayout 中具體做的事:

  1. 如果有子元件,則對子元件進行遞迴排序
  2. 確定當前元件大小(size),通知會依賴於子元件的大小
  3. 確定子元件在當前元件中的起始偏移

在Flutter 元件庫中,有很多常用的單子元件,如 Align,SizeBox,DecoratedBox 等,都可以開啟原始碼去看一下具體實現。


多子元件佈局示例

在實際開發中,我們經常會用到左右貼邊的佈局,現在我們就來實現一個 LeftRightBox 元件,來實現左右貼邊。

首先我們定義元件,與單子元件不同的是,多子元件需要繼承自 MultiChildRenderObjectWidget

```dart class LeftRightBox extends MultiChildRenderObjectWidget { LeftRightBox({Key key, @required List list}) : assert(list.length == 2, "只能傳兩個 child"), super(key: key, children: list);

@override RenderObject createRenderObject(BuildContext context) { return RenderLeftRight(); } } ```

接下來在 RenderLeftRight 中的 performLayout 實現左右佈局的演算法:

```dart class LeftRightParentData extends ContainerBoxParentData {}

class RenderLeftRight extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { /// 初始化每一個 child 的 parentData @override void setupParentData(covariant RenderObject child) { if (child.parentData is! LeftRightParentData) child.parentData = LeftRightParentData(); }

@override void performLayout() { //獲取當前約束(從父元件傳入的), final BoxConstraints constraints = this.constraints;

//獲取第一個元件,和他父元件傳的約束
RenderBox leftChild = firstChild;
LeftRightParentData childParentData =
    leftChild.parentData as LeftRightParentData;
//獲取下一個元件
//至於這裡為什麼可以獲取到下一個元件,是因為在 多子元件的 mount 中,遍歷建立所有的 child 然後將其插入到到 child 的 childParentData 中了
RenderBox rightChild = childParentData.nextSibling;

//限制右孩子寬度不超過總寬度的一半
rightChild.layout(constraints.copyWith(maxWidth: constraints.maxWidth / 2),
    parentUsesSize: true);

//設定右子節點的 offset
childParentData = rightChild.parentData as LeftRightParentData;
//位於最右邊
childParentData.offset =
    Offset(constraints.maxWidth - rightChild.size.width, 0);

//左子節點的 offset 預設為 (0,0),為了確保左子節點能始終顯示,我們不修改他的 offset
leftChild.layout(
    constraints.copyWith(
        //左側剩餘的最大寬度
        maxWidth: constraints.maxWidth - rightChild.size.width),
    parentUsesSize: true);

//設定 leftRight 自身的 size
size = Size(constraints.maxWidth,
    max(leftChild.size.height, rightChild.size.height));

}

double max(double height, double height2) { if (height > height2) return height; else return height2; }

@override void paint(PaintingContext context, Offset offset) { defaultPaint(context, offset); }

@override bool hitTestChildren(BoxHitTestResult result, {Offset position}) { return defaultHitTestChildren(result, position: position); } }

```

使用如下:

dart Container( child: LeftRightBox( list: [ Text("左"), Text("右"), ], ), ),

image-20211209155013117

我們對上面流程進行一個簡單分析:

1,獲取當前元件的約束資訊

2,獲取兩個子元件

3,對兩個子元件進行layout,並且右元件的寬度不能超過總寬度的一半,設定又元件的偏移為最右邊。接著對左元件進行佈局,左子元件的寬度為總寬度-右子元件的寬度,並且沒有設定偏移,預設偏移為0

4,設定當前元件自身的大小,高度為子元件的 max。

可以看到,實際佈局流程和單子元件沒太大區別,只不過多子元件需要對多個元件進行佈局。

l另外和 RenderCustomCenter 不同的是,RenderLeftRight 是直接繼承了 RenderBox,同時混入了 ContainerRenderObjectMixinRenderBoxContainerDefaultsMixin 兩個 mixin ,這兩個 mixin 中幫我們實現了磨人的繪製和事件處理的相關邏輯。

佈局更新

理論上,當某個元件的佈局發生變化之後,會影響到其他的元件佈局,所以當有元件佈局發生改變之後,最笨的辦法就是對整棵元件樹進行重新佈局。但是對所有的元件進行 reLayout 的成本還是比較大,所以我們需要探索一下降低 reLayout 成本的方案,事實上,在一些特定的場景下,元件發生變化之後只需要對特定的元件進行重新佈局即可,無需對整棵樹進行 reLayout

佈局邊界

image-20211214113606334

假如有一個頁面的元件樹結構如上所示:

假如 Text3 的文字長度發生變化,就會導致 Text4 的位置發生變化,相應的 Column2 的高度也會發生變化。又因為 SizedBox 的寬高已經固定。所以最終需要 reLayout 的元件是:Text3,Colum2,這裡需要注意的是:

  1. Text4 是不需要進行重新佈局的,因為 Text4 的大小沒有發生變化,只是位置發生了變化,而它的位置是在父元件 Colum2 佈局時確定的。
  2. 很容易發現:假如 Text3 和 Column2 之間還有其他元件,則這些元件也都是需要 reLayout 的。

在本例中,Column2 就是 Text3 的 relayoutBoundary(重新佈局的邊界點)。每個元件的 renderObject 中都有一個 _relayoutBoundary 屬性指向自身佈局,如果當前節點佈局發生變化後,自身到 _relayoutBoundary 路徑上的所有節點都需要 reLayout。

那麼一個元件的是否是 relayoutBoundary 的條件是什麼呢? 這裡有一個原則和四個場景,原則是 "元件自身的大小變化不會影響父元件",如果一個元件滿足下面四種情況之一,則它便是 relayoutBoundary:

  1. 當前元件的父元件大小不依賴當前元件大小時;這種情況下父元件在佈局時會呼叫子元件佈局函式時並會給子元件傳遞一個 parentUserSize 引數,該引數為 false 是表示父元件的佈局演算法不會依賴子元件的大小。
  2. 元件大小隻取決於父元件傳遞的約束,而不會依賴後代元件的大小。這樣的話後代元件的大小變化就不會影響到自身的大小了,這種情況元件的 sizedByParent 屬性必須為 true。
  3. 父元件傳遞給自身的約束是一個嚴格約束(固定寬高);這種情況下即使自身大小依賴後代元素,但也不會影響父元件。
  4. 元件Wie根元件;Fluuter 應用的根元件是 RenderView ,他的預設大小是當前裝置螢幕的大小。

對應實現的程式碼是:

dart if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) { _relayoutBoundary = this; } else { _relayoutBoundary = (parent! as RenderObject)._relayoutBoundary; }

程式碼中的 if 的判斷條件和上面的四條一一對應,其中除了第二個條件之外(sizeByParent 為 true),其他的都很直觀。第二個條件在後面會講到。

markNeedsLayout

當佈局發生變化的時候,他需要呼叫 markNeedsLayout 方法來更新佈局,它的主要功能有兩個:

1,將自身到其 relayoutBoundary 路徑上的所有節點標記為"需要佈局"

2,其請求新的 frame;在新的 frame 中會對標記為 "需要佈局" 的節點重新佈局

dart void markNeedsLayout() { //如果當前元件不是佈局邊界節點 if (_relayoutBoundary != this) { //遞迴標記將當前節點到佈局邊界節點 markParentNeedsLayout(); } else { //如果是佈局邊界節點 _needsLayout = true; if (owner != null) { //將佈局邊界節點加入到 piplineOwner._nodesNeedingLayout 列表中 owner!._nodesNeedingLayout.add(this); //改函式最終會請求新的 frame owner!.requestVisualUpdate(); } } }

flushLayout

markNeedsLayout 執行完成後,就會將其 relayoutBoundary 新增到 piplineOwner._nodesNeedingLayout 列表中,然後請求新的 frame。

當新的 frame 到來時,就會執行 piplineOwner.drawFrame 方法:

dart void drawFrame() { assert(renderView != null); pipelineOwner.flushLayout(); pipelineOwner.flushCompositingBits(); pipelineOwner.flushPaint(); .../// }

flushLayout 中會對之前新增到 _nodesNeedingLayout 中的節點進行重新佈局,如下:

dart void flushLayout() { while (_nodesNeedingLayout.isNotEmpty) { final List<RenderObject> dirtyNodes = _nodesNeedingLayout; _nodesNeedingLayout = <RenderObject>[]; //安裝節點在樹中的深度從小到大排序後在重新 layout for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) { if (node._needsLayout && node.owner == this) //重新佈局 node._layoutWithoutResize(); } } }

看一下 _layoutwithoutResize() 的實現

dart void _layoutWithoutResize() { try { //遞迴重新佈局 performLayout(); markNeedsSemanticsUpdate(); } catch (e, stack) { _debugReportException('performLayout', e, stack); } _needsLayout = false; //佈局更新後,更新UI markNeedsPaint(); }

到此佈局更新完成。

Layout流程

如果元件有子元件,則需要在 performLayout 中呼叫子元件的 layout 先對子元件進行佈局,如下:

```dart void layout(Constraints constraints, { bool parentUsesSize = false }) { RenderObject? relayoutBoundary;

//先確定當前佈局邊界
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) { relayoutBoundary = this; } else { relayoutBoundary = (parent! as RenderObject)._relayoutBoundary; } // _neessLayout 標記當前元件是否被標記為需要佈局 // _constraints 是上次佈局時父元件傳遞給當前元件的約束 // _relayoutBoundary 為上次佈局時當前元件的佈局邊界 // 所以,噹噹前元件沒有被標記為需要佈局,且父元件傳遞的約束沒有發生變化 // 和佈局邊界也沒有發生變化時則不需要重新佈局,直接返回即可
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) { ..../// return; } // 如果需要佈局,快取約束和佈局邊界
_constraints = constraints; _relayoutBoundary = relayoutBoundary; assert(!_debugMutationsLocked); assert(!_doingThisLayoutWithCallback); assert(() { _debugMutationsLocked = true; if (debugPrintLayouts) debugPrint('Laying out (${sizedByParent ? "with separate resize" : "with resize allowed"}) $this'); return true; }());

// 後面解釋 if (sizedByParent) { performResize(); }

// 執行佈局 performLayout();

//佈局結束後將 _needsLayotu 置位 false _needsLayout = false;

// 將當前元件標記為重繪,因為佈局發生變化後,需要重新繪製 markNeedsPaint(); } ```

簡單的講一下佈局的過程:

  1. 確定當前元件的佈局邊界
  2. 判斷是否需要重新佈局,如果沒有必要會直接返回,反之才需要重新佈局。不需要佈局時需要滿足三個條件
  3. 單籤元件沒有被標記為需要重新佈局。
  4. 父元件傳遞的約束沒有發生變化。
  5. 當前元件佈局邊界也沒有發生變化時。
  6. 呼叫 performLayout 進行佈局,因為 performLayout 中又會呼叫子元件的 layout 方法,所以這是一個遞迴的過程,遞迴結束後整個元件的佈局也就完成了。
  7. 請求重繪

sizedByParent

在 layout 方法中,有以下邏輯:

dart if (sizedByParent) { performResize(); }

上面我們說過,sizeByParent 為 true 是表示:當前元件的大小值取決於父元件傳遞的約束,而不會依賴後元件的大小。前面我們說過,performLayout 中確定當前元件大小時通常會依賴子元件的大小,如果 sizedByParent 為 true,則當前元件大小就不會依賴於子元件的大小。

為了清晰邏輯,Flutter 框架中約定,當 sizedByParent 為 true 時,確定當前元件大小的邏輯應該抽離到 performResize() 中,這種情況下 performLayout 主要任務便只有兩個:對子元件進行佈局和確定子元件在當前元件中的偏移。

下面通過一個 AccurateSizedBox 示例來演示一下 sizebyParent 為 true 時我們應該如何佈局:

AccurateSizeBox

Flutter 中的 SizeBox 會將其父元件的約束傳遞給其子元件,這也就意味著,如果父元件限制了最新的寬度為 100,即使我們通過 SizeBox 指定寬度為 50 也是沒有用的。

因為 SizeBox 中的實現會讓 SizedBox 的子元件先滿足 SizeBox 父元件的約束。例如:

dart AppBar( title: Text(title), actions: <Widget>[ SizedBox( // 使用SizedBox定製loading 寬高 width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 3, valueColor: AlwaysStoppedAnimation(Colors.white70), ), ) ], )

實際結果還是 progress 的高度為 appbar 的高度。

通過檢視 SizedBox 原始碼,如下所示:

dart @override void performLayout() { final BoxConstraints constraints = this.constraints; if (child != null) { child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true); size = child!.size; } else { size = _additionalConstraints.enforce(constraints).constrain(Size.zero); } } //返回尊重給定約束同時儘可能接近原始約束的新框約束 BoxConstraints enforce(BoxConstraints constraints) { return BoxConstraints( // clamp :根據數字返回一個介於低和高之間的值 minWidth: minWidth.clamp(constraints.minWidth, constraints.maxWidth), maxWidth: maxWidth.clamp(constraints.minWidth, constraints.maxWidth), minHeight: minHeight.clamp(constraints.minHeight, constraints.maxHeight), maxHeight: maxHeight.clamp(constraints.minHeight, constraints.maxHeight), ); }

可以發現,之所以不生效,是應為父元件限制了最小高度,SizeBox 中的子元件會先滿足父元件的約束。當然,我們也可以通過使用 UnconstrainedBox + SizedBox 來實現我們想要的效果,但是這裡我們希望使用一個佈局搞定,為此我們自定義一個 AccurateSizeBox 元件。

它和 SizedBox 主要的區別就是 AccurateSizedBox 自身會遵守其父元件傳遞的約束,而不是讓子元件去滿足 AccureateSizeBox 父元件的約束,具體:

  1. AccurateSizedBox 自身大小隻取決於父元件的約束和自身的寬高。
  2. AccurateSizedBox 確定自身大小後,限制其子元件的大小。

```dart class AccurateSizedBox extends SingleChildRenderObjectWidget { const AccurateSizedBox( {Key key, this.width = 0, this.height = 0, @required Widget child}) : super(key: key, child: child);

final double width; final double height;

@override RenderObject createRenderObject(BuildContext context) { return RenderAccurateSizeBox(width, height); }

@override void updateRenderObject( BuildContext context, covariant RenderAccurateSizeBox renderObject) { renderObject ..width = width ..height = height; } }

class RenderAccurateSizeBox extends RenderProxyBoxWithHitTestBehavior { RenderAccurateSizeBox(this.width, this.height);

double width; double height;

//當前元件的大小隻取決於父元件傳遞的約束 @override bool get sizedByParent => true;

// performResize 中會呼叫 @override Size computeDryLayout(BoxConstraints constraints) { //設定當前元素的寬高,遵守父元件的約束 return constraints.constrain(Size(width, height)); }

@override void performLayout() { child.layout( BoxConstraints.tight( Size(min(size.width, width), min(size.height, height))), //父容器是固定大小,子元素大小改變時不影響父元素 //parentUserSize 為 false時,子元件的佈局邊界會是他自身,子元件佈局發生變化後不會影響當前元件 parentUsesSize: false); } } ```

上面程式碼有三點需要注意:

  1. 我們的 RenderAccurateSizedBox 不在繼承自 RenderBox,而是繼承 RenderProxyBoxWithHitTestBehaviorRenderProxyBoxWithHitTestBehavior 是間接繼承自 RenderBox 的,它裡面包含了預設的命中測試和繪製相關邏輯,繼承它以後則不需要我們手動實現了。

  2. 我們將確定當前元件大小的邏輯挪到了 computeDryLayout 方法中,因為 RenderBox 的 performResize 方法會呼叫 computeDryLayout,並將返回結果作為當前元件大小。

按照 Flutter 框架約定,我們應該重寫 computeDryLayout 方法,而不是 performResize 方法。就行我們在佈局時應該重寫 performLayout 方法而不是 layout 方法;不過,這只是一個約定,並非強制,但我們應該儘可能遵守這個約定,除非你清楚的知道自己在幹什麼並且能確保之後維護你程式碼的人也清楚。

  1. RenderAccurateSizedBox 在呼叫子元件 layout 時,將 parentUserSize 置為 false,這樣的話子元件就會變成一個佈局邊界。

測試如下:

dart class AccurateSizedBoxRoute extends StatelessWidget { @override Widget build(BuildContext context) { final child = GestureDetector( onTap: () => print("tap"), child: Container(width: 300, height: 30, color: Colors.red), ); return Row( children: [ ConstrainedBox( //限制高度為 100x100 constraints: BoxConstraints.tight(Size(100, 100)), child: SizedBox( width: 50, height: 50, child: child, ), ), Padding( padding: const EdgeInsets.only(left: 8), child: ConstrainedBox( constraints: BoxConstraints.tight(Size(100, 100)), child: AccurateSizedBox(width: 50, height: 50, child: child), ), ) ], ); } }

image-20211215183103420

結果如上所示,當父元件寬高是 100 時,我們通過 SizedBox 指定 Container 大小是 50x50 是不能成功的。而通過 AccurateSizedBox 時成功了。

需要注意的是,如果一個元件的 sizeByParent 為 true,那它在佈局子元件的時候也是能將 parentUserSize 的,sizeByParent 為 true 表示自己是佈局邊界。

而將 parentUsesSize 置為 true 或者 false 決定的是子元件是否是佈局邊界,兩者並不相矛盾,這一點不能混淆。

另外,在 Flutter 自帶的 OverflowBox 元件中,他的 sizeByParent 為 true,在呼叫子元件 layout 時,parentUsesSize 也是 true,詳情可檢視 OverflowBox 的原始碼

Constraints

Constraints(約束)主要描述了最小和最大寬高的限制,理解元件在佈局過程中如何根據約束確定自身或子節點的大小對我們理解元件的佈局行為有很大的幫助。

我們通過一個 200*200 的 Container 的例子來說明,為了排除干擾,我們讓根節點(RenderView) 作為 Container 的父元件,程式碼如下:

dart Container(width: 200, height: 200, color: Colors.red)

執行之後,就會發現整個螢幕都為紅色,為什麼呢,我們看看 RenderView 的實現:

dart @override void performLayout() { //configurateion.sieze 為當前裝置的螢幕 _size = configuration.size; assert(_size.isFinite); if (child != null) child!.layout(BoxConstraints.tight(_size));//強制子元件和螢幕一樣大 }

這裡需要介紹一下兩種常用的約束:

  1. 寬鬆約束:不限制最小寬高(為 0),只限制最大寬高,可以通過 BoxConstraints.loose(Size size) 來快速建立。
  2. 嚴格約束:限制為固定大小,即最小寬度等於最大寬度,最小高度等於最大高度,可以通過 BoxConstraints.thght(Size) 來快速建立。

可以發現,RenderView 中給子元件傳遞的是一個嚴格的約束,即強制子元件等於螢幕大小,所以 Container 便撐滿了螢幕。

那麼我們如何才能讓指定的大小生效呢,答案就是 “引入一箇中間元件,讓中間元件遵守父元件的約束,然後對子元件傳遞新的約束”。對於這個例子來說,最簡單的辦法就是使用一個 Align 元件來包裹 Container:

dart @override Widget build(BuildContext context) { var container = Container(width: 200, height: 200, color: Colors.red); return Align( child: container, alignment: Alignment.topLeft, ); }

Align 會遵守 RenderView 的約束,讓自身撐滿螢幕,然後會給子元件一個寬鬆的約束(最小寬度為 0,最大寬度為 200),這樣 Container 就可以變成 200*200 了。

當然我們也可以使用其他元件來代替 Align,例如 UnconstrainedBox,但原理是相同的。具體可檢視原始碼進行驗證。

例如 Align 的佈局過程如下:

```dart void performLayout() { final BoxConstraints constraints = this.constraints; final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity; final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;

if (child != null) { //子元件採用寬鬆約束,並且設定子元件不是佈局邊界(表示子元件改變後當前元件也需要重新重新整理) child!.layout(constraints.loosen(), parentUsesSize: true); size = constraints.constrain(Size( shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity, shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity, )); alignChild(); } else { size = constraints.constrain(Size( shrinkWrapWidth ? 0.0 : double.infinity, shrinkWrapHeight ? 0.0 : double.infinity, )); } } ```

總結

到這裡我們已經對 flutter 佈局流程比較熟悉了,現在我們看一張官網的圖:

image-20220105112857377

在進行佈局的時候,Flutter 會以 DFS(深度優先遍歷) 的方式遍歷渲染樹,並限制自上而下的方式從父節點傳遞給子節點。子節點如果需要確定自身的大小,則必須遵守父節點傳遞的限制。子節點的響應方式是在父節點建立的約束內將大小以自上而下的方式傳遞給父節點。

是不是理解的更透徹了一些

推薦閱讀

參考資料

  • Flutter 中文網

如果本文有幫助到你的地方,不勝榮幸,如有文章中有錯誤和疑問,歡迎大家提出