5、layout 流程分析

1、Flutter 之圖像繪製原理html

2、Widget、Element、RenderObjectnode

3、Flutter UI 更新流程api

4、build 流程分析bash

6、Paint 繪製(1)函數

7、Paint 繪製(2)佈局

8、composite 流程分析post

9、Flutter 小實踐ui

一、layout 流程

執行 Build 流程以後,接下來則是佈局流程,即完成相關節點的大小,位置測量this

(1) drawFrameurl

void drawFrame() {
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
  renderView.compositeFrame(); // this sends the bits to the GPU
  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
複製代碼

(2)flushLayout

void flushLayout() {
    while (_nodesNeedingLayout.isNotEmpty) {
      final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
      _nodesNeedingLayout = <RenderObject>[];
      for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
        if (node._needsLayout && node.owner == this)
          node._layoutWithoutResize();
      }
    }
}
複製代碼

這個方法中,實際上是遍歷 _nodesNeedingLayout (須要從新佈局的節點)集合, 分別調用 _layoutWithoutResize 方法 那 _nodesNeedingLayout 從何而來? 繼續追蹤,會發如今 markNeedsLayout 中 會有相關處理邏輯

(3)markNeedsLayout

void markNeedsLayout() {
  if (_relayoutBoundary != this) {
   // 繪製邊界不是吱聲,則向上遍歷,直到找到最近 relayoutBoundary 爲 true 的父元素
    markParentNeedsLayout();
  } else {
    _needsLayout = true;
    if (owner != null) {
      // 繪製邊界是自身,則將該節點加入待從新 layout 節點集合中
      owner._nodesNeedingLayout.add(this);
    }
  }
}
複製代碼

markNeedsLayout 什麼時候調用呢? 答案是: updateRenderObject

以 RenderImage 爲例, 當 image、width、height 屬性變化時都會觸發調用markNeedsLayout函數,以標記該 renderObject 須要從新 layout

// 設置圖像url 
set image(ui.Image value) {
  if (_width == null || _height == null)
    markNeedsLayout();
}

// 設置寬度
set width(double value) {
  if (value == _width)
    return;
  _width = value;
  markNeedsLayout();
}

// 設置高度
set height(double value) {
  if (value == _height)
    return;
  _height = value;
  markNeedsLayout();
}

(4)_layoutWithoutResize
void _layoutWithoutResize() {
    performLayout()
}
複製代碼

(5)performLayout

這個方法在 RenderObject 中只是聲明瞭,具體實現是由具體子類去重寫, 例如以 根節點 RenderView 爲例, 其中的 performlayout 就調用了子類的layout 方法, 即該方法是爲了完成子節點的佈局

void performLayout() {
  _size = configuration.size;
  if (child != null)
    child.layout(BoxConstraints.tight(_size));
}
複製代碼

(6)layout

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) {
    performResize();
  }
  RenderObject debugPreviousActiveLayout;
    performLayout();
    markNeedsSemanticsUpdate();
    markNeedsPaint();
}
複製代碼

layout 方法中,除了完成自身的佈局外,還須要調用 performLayout 完成子節點的佈局, 所以最終的調用棧爲:layout() > performResize()/performLayout() > child.layout(),如此遞歸完成整個UI的佈局。

一個節點在界面上的顯示須要兩個條件: 大小 size 位置 offset 在 flutter 中,renderObject 節點的佈局經歷了一個 自上而下自下而上 的一個遞歸過程,在 performLayout 中,父前節點會調用子節點的 layout 方法,同時將約束參數 constraints 傳遞給子節點,子節點佈局完以後,能計算出自身的 size, 而後 自下而上分別將 size 傳回的父節點

(1) constraint

用以控制子節點的最大和最小寬高,由父節點傳遞在佈局時傳遞給子節點,子節點必須遵照父節點給定的限制條件,maxWidth, maxHeight, minWidth, minHeight

(2) sizedByParent

若是這個參數值是 true, 則意味着該節點的大小僅僅經過 parent 傳給它的 constraints 就能夠肯定了,與其自身的屬性和子節點無關, 即其大小在 performResize() 中就肯定了,在後面的 performLayout() 方法中將不會再被修改了

(3) relayoutBoundary

佈局邊界,一個節點大小改變時,可能會影響父節點,這個參數是用來控制當子節大小點發生改變時,是否須要更新父節點的佈局,若是這個屬性值指向自身 , 則不須要通知父節點改變。 由layout 源碼能夠看到,這個值由如下幾個參數決定 parentUsesSize 爲 false sizedByParent 該值爲true時,其節點大小由父節點所傳的 constraint 決定 constraints.isTight parent 不是 RenderObject 以上的分析,咱們大體瞭解節點佈局的大體流程、節點大小的設置原理、若是佈局的話,還須要節點的位置,那位置信息保存在哪裏呢?答案是: ParentData

(4) parentData

這個參數主要是用來保存節點的相關位置信息,例如位移 offset, TableCellParentData 中 x(所在的列)、y(所在的行)等,TextParentData 中 文字的縮放比例 scale 等。

ParentData 有不一樣的實現類,具體可查看

api.flutter-io.cn/flutter/ren…

佈局過程當中,存儲位置相關信息又是如何更新的呢? 以 RenderListBody 爲例:

void performLayout() {
  double mainAxisExtent = 0.0;  // 主軸初始位移
  RenderBox child = firstChild;
  switch (axisDirection) {
  case AxisDirection.right:
    final BoxConstraints innerConstraints = BoxConstraints.tightFor(height: constraints.maxHeight);
    while (child != null) {
      // 註釋1: 子節點佈局
      child.layout(innerConstraints, parentUsesSize: true);
      final ListBodyParentData childParentData = child.parentData;
      // 註釋2: 更新parentData 中 offset 值
      childParentData.offset = Offset(mainAxisExtent, 0.0);
      mainAxisExtent += child.size.width;
      child = childParentData.nextSibling;
    }
    size = constraints.constrain(Size(mainAxisExtent, constraints.maxHeight));
    break;
  case AxisDirection.left:
    ...
  case AxisDirection.down:
    ...
    break;
  case AxisDirection.up:
    ...
    break;
  }
}
複製代碼

在以上代碼中,註釋1:在performLayout 函數中,先調用 childLayout 完成子節點的佈局,完成子節點佈局以後,各個節點的size 已經肯定,經過遍歷設置各個節點 parentData 的 offset 值 即可以完成各個節點的位置信息。

二、總結

如上圖中藍色部分,在 build 過程會涉及到 renderObject建立、更新、這兩部分會觸發 markNeedsLayout 函數 記錄須要更新佈局的 renderObject 節點,當 build 流程完成,調用 flushLayout 時,則會遍歷這些節點,完成各個節點的大小計算,以及相關位置信息的更新

相關文章
相關標籤/搜索