Flutter 探索系列:盒約束布局(三)

上一篇文章中,我們分析了 Flutter 布局和渲染的大致實(shí)現(xiàn),這篇文章繼續(xù)介紹 Flutter 的布局過程。

介紹

App 在渲染視圖時(shí),需要在坐標(biāo)系中指定區(qū)域進(jìn)行繪制。Flutter 的坐標(biāo)系是二維空間,這和iOS和安卓的原生坐標(biāo)系是一樣的,屏幕的左上角是坐標(biāo)原點(diǎn)(0, 0),向屏幕右下方延伸,對(duì)應(yīng)寬高兩個(gè)軸。

因此我們需要獲取視圖的位置和大小,才能在坐標(biāo)系中布局和繪制,F(xiàn)lutter 布局的過程即是計(jì)算視圖位置和大小的過程。

原理分析

我們從幾個(gè)問題開始,對(duì) Flutter 布局實(shí)現(xiàn)進(jìn)行分析。

1,布局流程是怎樣的

我們知道,當(dāng)一個(gè)組件的位置和大小都確定了,即可在頁(yè)面中布局。布局流程,便是確定頁(yè)面中各組件的大小和位置,如果某個(gè)組件含有子組件,那么先確定子組件的大小,再確定子組件在其父組件中的位置,以此類推完成整個(gè) UI 的布局。

Flutter 使用盒約束布局,組件按照指定限制條件來決定自身如何占用布局空間,這有點(diǎn)類似前端的 Flex 彈性布局,row、column、flex-start、flex\end、margin、padding等在 Flutter 中都有對(duì)應(yīng)的布局屬性。盒約束布局首先計(jì)算這些限制條件,得出子組件在父組件中的位置和大小,再對(duì)組件進(jìn)行布局和繪制。

Flutter 的布局流程如下圖所示

image

這是一個(gè)組件渲染樹,每個(gè)節(jié)點(diǎn)對(duì)應(yīng)一個(gè)組件,每個(gè)節(jié)點(diǎn)都是一個(gè) RenderObject 對(duì)象。從根節(jié)點(diǎn)開始,父節(jié)點(diǎn)向子節(jié)點(diǎn)傳遞 Constraints 約束,Constraints 對(duì)象可以限制子節(jié)點(diǎn)的最大和最小寬高,子節(jié)點(diǎn)遵守這個(gè)約束。子節(jié)點(diǎn)確定了自身的寬高之后,傳給父節(jié)點(diǎn),父節(jié)點(diǎn)再根據(jù)子節(jié)點(diǎn)的大小計(jì)算自身的寬高,在這個(gè)步驟中父節(jié)點(diǎn)會(huì)計(jì)算子節(jié)點(diǎn)相對(duì)于父節(jié)點(diǎn)的位置偏移量,即子節(jié)點(diǎn)的位置。以此類推完成整個(gè)頁(yè)面的布局。

2,如何計(jì)算組件寬高

從根節(jié)點(diǎn)開始,向子節(jié)點(diǎn)傳遞 Constraints 約束,Constraints 有四個(gè)屬性,最小寬度minWidth,最大寬度maxWidth,最小高度minHeight,最大高度maxHeight,他們決定了子節(jié)點(diǎn)的大小。如果子節(jié)點(diǎn)有額外的約束條件,則進(jìn)行比對(duì)添加,然后再傳給下一級(jí)。

RenderBox 是真正布局的類,它繼承自 RenderObject,它有兩個(gè)方法 performResize() 和 performLayout() 實(shí)現(xiàn)組件自身的布局邏輯。performLayout 先執(zhí)行子組件的布局,子組件布局完成后得到子組件的大小,再執(zhí)行自身的布局,同時(shí)還要遵守上一級(jí)傳過來的大小限制,比較得到一個(gè)合適的大小。

我們以 Center 為例,說明一下這個(gè)過程。Center 組件繼承自 Align,它對(duì)應(yīng)的 RenderObject 是 RenderPositionedBox,而 RenderPositionedBox 是 RenderBox 的子類,所以 Center 組件的布局方法是在 RenderPositionedBox。我們看一下 RenderPositionedBox 的 performLayout 方法

@override
  void performLayout() {
    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));
    }

如果子組件為空,Center 的寬高也為0。如果子組件存在,先布局子組件,并把約束傳進(jìn)去,這里的 constraints.loosen() 表示最小寬度和最小高度都為0,最大寬度和最大高度都為最大值。

如果需要縮放,則根據(jù)_widthFactor是否為null來進(jìn)行縮放,shrinkWrapWidth 和 shrinkWrapHeight 是縮放后的寬高。

alignChild 方法計(jì)算子組件的位置,入?yún)⒕褪歉附M件的大小減去子節(jié)點(diǎn)的大小,也就是父組件剩余的空間,然后分別對(duì)剩余長(zhǎng)寬除以2得到中值,即是子組件的位置。這里的x 和 y 是表示垂直居中,所以 x 和 y 都是0

    void alignChild() {
    final BoxParentData childParentData = child.parentData;
    childParentData.offset = _resolvedAlignment.alongOffset(size - child.size);
  }

    Offset alongOffset(Offset other) {
    final double centerX = other.dx / 2.0;
    final double centerY = other.dy / 2.0;
    return Offset(centerX + x * centerX, centerY + y * centerY);
  }

3,如何對(duì)布局進(jìn)行性能優(yōu)化

布局更新中,父子節(jié)點(diǎn)會(huì)相互作用,子節(jié)點(diǎn)更新時(shí),會(huì)通知父節(jié)點(diǎn)更新。Flutter 采取了許多措施,盡可能減少需要重新布局的節(jié)點(diǎn),縮小布局范圍。

Flutter 有個(gè)渲染邊界(relayoutBoundary)的概念。處于渲染邊界上的組件大小是固定的,不會(huì)因子組件的變化而變化,邊界內(nèi)的組件更新,更不會(huì)影響到邊界外面的組件。滿足以下任一條件,則該組件是渲染邊界(relayoutBoundary)

parentUsesSize,父節(jié)點(diǎn)傳子節(jié)點(diǎn) Constraints 約束時(shí),同時(shí)將這個(gè)屬性傳傳遞給子組件。它為true時(shí)表示父節(jié)點(diǎn)的布局依賴子節(jié)點(diǎn),子節(jié)點(diǎn)更新時(shí),父節(jié)點(diǎn)也要相應(yīng)更新。為false,表示父組件不依賴子基點(diǎn)。

sizedByParent,這個(gè)是RenderObject的屬性,表示這個(gè)組件完全由父組件控制,不受子組件的影響。

constraints.isTight,表示組件是嚴(yán)格約束,不受子組件的影響。

parent is! RenderObject,父組件不是RenderObject,一般是根組件。

同時(shí)滿足以下三個(gè)條件,則說明該組件不需要重新布局,進(jìn)入下一步。

_needsLayout屬性為false:這說明該組件沒有被自己或孩子標(biāo)記為臟。
constraints == _constraints :父級(jí)的限制沒有發(fā)生變化。
relayoutBoundary沒有發(fā)生變化:說明該組件所屬的渲染邊界沒有發(fā)生變化。

代碼如下

void layout(Constraints constraints, { bool parentUsesSize = false }) {
    RenderObject relayoutBoundary;
    if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
      relayoutBoundary = this;
    } else {
      final RenderObject parent = this.parent;
      relayoutBoundary = parent._relayoutBoundary;
    }

    if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
      return;
    }
    _constraints = constraints;
    _relayoutBoundary = relayoutBoundary;

    if (sizedByParent) {
      try {
        performResize();
      } catch (e, stack) {
        ...
      }
    }
    try {
      performLayout();
     
    } catch (e, stack) {
      ...
    }
    _needsLayout = false;
    markNeedsPaint();
  }

參考資料:[Flutter框架分析(六)-- 布局]https://juejin.im/post/5cda66b5e51d453ce55feab8#heading-0

[RenderObject和RenderBox]https://book.flutterchina.club/chapter14/render_object.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。