五、layout 流程分析

2,048 阅读4分钟

一、Flutter 之图像绘制原理

二、Widget、Element、RenderObject

三、Flutter UI 更新流程

四、build 流程分析

六、Paint 绘制(1)

七、Paint 绘制(2)

八、composite 流程分析

九、Flutter 小实践

1、layout 流程

执行 Build 流程之后,接下来则是布局流程,即完成相关节点的大小,位置测量

(1) drawFrame

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 传递给子节点,并设置子节点的relayoutBoundary, 子节点布局完之后,能计算出自身的 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 值 便可以完成各个节点的位置信息。

2、总结

如上图中蓝色部分,在 build 过程会涉及到 renderObject创建、更新、这两部分会触发 markNeedsLayout 函数 记录需要更新布局的 renderObject 节点,当 build 流程完成,调用 flushLayout 时,则会遍历这些节点,完成各个节点的大小计算,以及相关位置信息的更新