Flutter Notes | Flutter Core Principles (3) Layout Process

layout process

The Layout process is mainly to determine the layout information (size and position) of each component. The layout process of Flutter is as follows:

  1. The parent node transmits constraints (constraints) information to the child node, limiting the maximum and minimum width and height of the child node.
  2. The child node determines its own size (size) according to the constraint information.
  3. The parent node determines the position of each child node in the layout space of the parent node according to specific layout rules (different layout components will have different layout algorithms), represented by offset.
  4. Recurse the whole process to determine the size and position of each node.

It can be seen that the size of the component is determined by itself, and the position of the component is determined by the parent component.

The following is a picture of the official website, which describes the essence of Flutter's layout process in three sentences:

insert image description here

There are many layout components in Flutter, which can be divided into single-subcomponents and multi-subcomponents according to the number of children. Let’s first understand the layout process of Flutter intuitively by customizing a single-subcomponent and multi-subcomponents respectively, and then introduce the layout update. Processes and Constraints in Flutter.

Example of a monadic component layout

We implement a monadic component CustomCenterwhose functions are basically Centeraligned with the component. Through this example, we demonstrate the main flow of the layout.

First, we define components. In order to introduce the principle of layout, we do not use combination to implement components, but directly RenderObjectimplement them through customization. Because the centered component needs to contain a child node, we inherit directly SingleChildRenderObjectWidget.

class CustomCenter extends SingleChildRenderObjectWidget {
    
    
  const CustomCenter2({
    
    Key? key, required Widget child})
      : super(key: key, child: child);

  
  RenderObject createRenderObject(BuildContext context) {
    
    
    return RenderCustomCenter();
  }
}

Then implement RenderCustomCenter. Direct inheritance here RenderObjectwill be closer to the bottom layer, but this requires us to manually implement some things that have nothing to do with layout, such as event distribution and other logic. In order to focus more on the layout itself, we choose to inherit from RenderShiftedBox, which is RenderBoxa subclass of ( RenderBoxinherited from RenderObject), it will help us implement some functions other than layout, so we only need to rewrite performLayout, and implement the child node centering algorithm in this function. Can.

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

  
  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;
  }
}

Please refer to the comments for the layout process. There are 3 additional points to be explained here:

  1. When laying out child nodes, constraintsit is CustomCenterthe constraint information passed to itself by the parent component. The constraint information we pass to the child nodes is constraints.loosen(), let's take a look at loosenthe implementation source code:
BoxConstraints loosen() {
    
    
  return BoxConstraints(
    minWidth: 0.0,
    maxWidth: maxWidth,
    minHeight: 0.0,
    maxHeight: maxHeight,
  );
}

Obviously, CustomCenterthe maximum width and height of a constrained child node does not exceed its own maximum width and height.

  1. The child node CustomCenterdetermines its own width and height under the constraints of the parent node ( ); at this time, CustomCenterit will determine its own width and height according to the width and height of the child node. The logic of the above code is that if CustomCenterthe parent node passes it the maximum width and height constraint is infinite When larger, its width and height will be set to the width and height of its child nodes. Note that if you CustomCenterset the width and height to infinite at this time, there will be problems, because if your own width and height are also infinite in an infinite range, then what is the actual width and height, and its parent node Will be confused! The size of the screen is fixed, which is obviously unreasonable. If CustomCenterthe maximum width and height constraints passed to it by the parent node are not infinite, then you can specify your own width and height as infinite, because in a limited space, if the child node says that it is infinite, then the largest is the parent node the size of. So, in short, CustomCenterit fills the space of the parent element with itself as much as possible.

  2. CustomCenterAfter determining your own size and the size of the child node, you can determine the position of the child node. According to the centering algorithm, the origin coordinates of the child node are calculated and stored in the child node. It will be used in the subsequent drawing stage. How to use it specifically parentData, Let's take a look at the RenderShiftedBoxdefault paintimplementation in :


void paint(PaintingContext context, Offset offset) {
    
    
  if (child != null) {
    
    
    final BoxParentData childParentData = child!.parentData! as BoxParentData;
    //从child.parentData中取出子节点相对当前节点的偏移,加上当前节点在屏幕中的偏移,
    //便是子节点在屏幕中的偏移。
    context.paintChild(child!, childParentData.offset + offset);
  }
}

performLayout process

As you can see, the layout logic is performLayoutimplemented in the method. Let's sort out performLayoutwhat we do in detail:

  1. If there are subcomponents, the subcomponents are laid out recursively.
  2. Determine the size of the current component (size), usually depends on the size of subcomponents.
  3. Determines the starting offset of the child component within the current component.

In the Flutter component library, there are some commonly used monadic components such as Align、SizedBox、DecoratedBoxetc. You can open the source code to see its implementation.

Let's look at an example of a multi-child component.

Multi-child component layout example

In actual development, we often use the left-right layout of the border. Now we will implement a LeftRightBoxcomponent to realize the left-right layout, because LeftRightBoxthere are two children, and a Widget array is used to save the child components.

First, we define components. Unlike single-child components, multi-child components need to inherit from MultiChildRenderObjectWidget:

lass LeftRightBox extends MultiChildRenderObjectWidget {
    
    
  LeftRightBox({
    
    
    Key? key,
    required List<Widget> children,
  })  : assert(children.length == 2, "只能传两个children"),
        super(key: key, children: children);

  
  RenderObject createRenderObject(BuildContext context) {
    
    
    return RenderLeftRight();
  }
}

Next comes the implementation RenderLeftRight, performLayoutwhere we implement the left-right layout algorithm:

class LeftRightParentData extends ContainerBoxParentData<RenderBox> {
    
    }

class RenderLeftRight extends RenderBox
    with
        ContainerRenderObjectMixin<RenderBox, LeftRightParentData>,
        RenderBoxContainerDefaultsMixin<RenderBox, LeftRightParentData> {
    
    
 
  // 初始化每一个child的parentData        
  
  void setupParentData(RenderBox child) {
    
    
    if (child.parentData is! LeftRightParentData)
      child.parentData = LeftRightParentData();
  }

  
  void performLayout() {
    
    
    final BoxConstraints constraints = this.constraints;
    RenderBox leftChild = firstChild!;
    
    LeftRightParentData childParentData =
        leftChild.parentData! as LeftRightParentData;
    
    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,
    );

    // layout left child
    // 左子节点的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),
    );
  }

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

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

It can be seen that the actual layout process is not much different from that of single-child nodes, except that multi-child components need to layout multiple child nodes at the same time. In addition RenderCustomCenter, the difference from is that RenderLeftRightit is directly inherited from RenderBoxand mixed ContainerRenderObjectMixinwith RenderBoxContainerDefaultsMixintwo mixins, which mixinimplement general drawing and event processing related logic.

About ParentData

RenderObjectIn the above two examples, we used parentDatathe object of the child node (save the information of the child node ) when implementing the corresponding . offsetYou can see parentDatathat although it belongs childto the property, it is in the parent node from setting (including initialization) to use , which is why it is named " parentData". In fact, in the Flutter framework, parentDatathis attribute is mainly designed to save component layout information during the layout phase.

It should be noted: "parentData is used to save the layout information of the node" is just a convention. When we define a component, we can save the layout information of the child nodes in any place, and we can also save non-layout information. However, it is strongly recommended that you follow the Flutter specification, so that our code will be easier for others to understand and maintain.

Layout process analysis

Layout starts as a method drawFrame()in code flushLayout. Let's recall:

void drawFrame() {
    
    
  buildOwner!.buildScope(renderViewElement!); // 1.重新构建widget树
  //下面是 展开 super.drawFrame() 方法
  pipelineOwner.flushLayout(); // 2.更新布局
  pipelineOwner.flushCompositingBits(); //3.更新“层合成”信息
  pipelineOwner.flushPaint(); // 4.重绘
  if (sendFramesToEngine) {
    
    
    renderView.compositeFrame(); // 5. 上屏,会将绘制出的bit数据发送给GPU
    ...
  }
}

It should be noted that Flutter does not Render Treeexecute every node in during the rendering of a frame Layout. BuildLike the process, Flutter will mark the required Layoutnodes RenderObjectand proceed Layout.

Flutter's Build, Layout and other follow-up processes all use the mark-sweep (Mark-Sweep) idea in the GC algorithm. Its essence is to exchange space for time to achieve partial UI update and efficient generation of rendering data. In Flutter, the marking phase is called Mark and the clearing phase is called Flush . Therefore, the following will analyze the source code according to these two stages.

Mark stage

RenderObjectThe markNeedsLayoutmethod will mark the current node as needed Layout, but Buildunlike the process, Layoutthe mark entry is very discrete, and the method is usually caused by markNeedsBuildthe developer's active call, so the call point is very clear.setState

LayoutThe process is relative Render Treebecause relevant information RenderObjectis stored in it . LayoutAlthough it is impossible to enumerate all markNeedsLayoutcall sites, it can be speculated. For example, when a representation image RenderObjectchanges size, it must Layout:

double? get width => _width;
double? _width;
set width(double? value) {
    
    
  if (value == _width) return; // 宽度未改变,即使内容改变了,也无须Layout
  _width = value;
  markNeedsLayout();
}

The above logic is the logic when the RenderImageattribute widthis updated, it will call its own markNeedsLayoutmethod.

markNeedsLayout

When the layout of the component changes, it needs to call markNeedsLayoutthe method to update the layout. It has two main functions:

  1. Marks all nodes on its path from itself relayoutBoundaryas "needs layout".
  2. Request a new one ; nodes marked "needs layout" will be re-layouted in framethe new .frame

Let's look at its core source code:

void markNeedsLayout() {
    
     // RenderObject
  if (_needsLayout) {
    
     return; } // 已经标记过,直接返回
  if (_relayoutBoundary != this) {
    
     // 如果当前节点不是布局边界节点:父节点受此影响,也需要被标记
    markParentNeedsLayout(); // 递归调用 
  } else {
    
     // 如果当前节点是布局边界节点:仅标记到当前节点,父节点不受影响
    _needsLayout = true;
    if (owner != null) {
    
      
      // 将布局边界节点加入到 pipelineOwner._nodesNeedingLayout 列表中
      owner!._nodesNeedingLayout.add(this);
      owner!.requestVisualUpdate(); // 请求刷新
    }
  }
}

// 递归调用前节点到其布局边界节点路径上所有节点的markNeedsLayout方法 

void markParentNeedsLayout() {
    
    
  _needsLayout = true;
  final RenderObject parent = this.parent! as RenderObject;
  if (!_doingThisLayoutWithCallback) {
    
     // 通常会进入本分支
    parent.markNeedsLayout(); // 继续标记
  } else {
    
     // 无须处理
    assert(parent._debugDoingThisLayout);
  }
}

The above logic first judges _needsLayoutwhether the field is true, if it is true, it means that it has been marked, and returns directly, otherwise it will judge RenderObjectwhether it is currently Layouta boundary node, each RenderObjecthas a field, indicating the nearest " layout_relayoutBoundary " among ancestor nodes including itself border ". The so-called layout boundary means that the node Layoutwill not have an impact on the parent node, so Layoutthe side effects of the node and its child nodes will not continue to be passed on to the ancestor nodes . Flutter achieves localization by recording, storing, and updating boundary nodes Layoutto minimize redundant and invalid Layoutcalculations.

insert image description here

Take Figure 5-12 as an example. For the above logic, if the current node is not a layout boundary, the method of the ancestor node will be called markNeedsLayoutuntil the current node is a layout boundary. At this point, the node is added to the PipelineOwnerobject _nodesNeedingLayoutlist, and requestVisualUpdatethe frame rendering is requested through the method, but the latter is generally completed at the stage Buildof the process . The above process will mark the attributes markof each node passed as ._needsLayouttrue

Typically, markNeedsLayoutthe method is triggered after the arrival of the VsyncBuild signal, the stage of the process Flush, and the accompanying update.Render Tree

Flush stage

The following enters the Flush stage of the Layout process , that is, the method. The specific logic is as follows:flushLayout

void flushLayout() {
    
     // 由 drawFrame() 触发
  if (!kReleaseMode) {
    
     Timeline.startSync('Layout', arguments: ......); } 
  // Layout阶段开始
  try {
    
    
    while (_nodesNeedingLayout.isNotEmpty) {
    
     // 存在需要更新Layout信息的节点
      final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
      _nodesNeedingLayout = <RenderObject>[];
      // 先按照节点在树中的深度从小到大进行排序,优先处理祖先节点
      dirtyNodes.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
      for (final RenderObject node in dirtyNodes) {
    
    
        if (node._needsLayout && node.owner == this)
          node._layoutWithoutResize(); // 真正的Layout逻辑
      }
    }
  } finally {
    
    
    if (!kReleaseMode) {
    
     Timeline.finishSync(); } // Layout阶段结束
  }
}

The above logic sorts the nodes _nodesNeedingLayoutthat need to be re-layouted RenderObjectaccording to the depth, and prioritizes the layout of the nodes with smaller depths. These nodes are usually ancestor nodes. Then the method RenderObjectof these nodes that need to execute Layout will be triggered _layoutWithoutResize, because each added node is a border node, so the method name here WithoutResizeends with . For non-layout boundary nodes, if a RenderObjectnode changes size during Layout, its Layout information relative to the parent node will change.

Thinking question: Why is it necessary to dirtyNodessort the data from small to large according to the depth in the tree? Can't you go from big to small?

  • The reason for sorting according to the depth is mainly because the ancestor node layout process is to traverse the tree according to the depth first. After sorting, the smaller depth is the ancestor node. However, if it is sorted from large to small, it will give priority to the depth If a large child node is used for layout, then when the layout of the ancestor node is executed, the child node may need to re-layout due to the influence of the ancestor node, causing the previous layout to become an invalid calculation.

Let's take a look at _layoutWithoutResizethe implementation of the method:

void _layoutWithoutResize() {
    
     // RenderObject
  try {
    
    
    performLayout(); // 重新布局;会递归布局后代节点
    markNeedsSemanticsUpdate();
  } catch (e, stack) {
    
     ...... }
  _needsLayout = false;
  markNeedsPaint(); //布局更新后,UI也是需要更新的
}

void performLayout(); // 由RenderObject的子类负责实现

The above logic will execute performLayoutthe method of the current node to start the specific Layout logic, and then mark the current node as requiring the Paint process.

Layout is a relatively important process for developers in the rendering pipeline. Most of our work in UI development is actually to layout internal nodes through Widgets. Most of the Widgets used actually RenderObjectencapsulate different layouts, such Row、Colum、Stack、Centeras .

To put it simply, the essence of the Layout process is depth-first traversal starting from the layout boundary node . When entering a node, it carries the information of the parent node (obtained through the set field ). For the Box layout model, after the completion of the child node, it returns the size Information and information representing a location (by setting fields and fields).constraintsRenderObject_constraintsperformLayoutSizeOffset_sizeoffset

Layout instance analysis

RenderViewThe following performLayoutmethod is used as the entry point for analysis, and the code is as follows:

 // flutter/packages/flutter/lib/src/rendering/view.dart
 // RenderView
void performLayout() {
    
    
  assert(_rootTransform != null);
  _size = configuration.size; // Embedder负责配置size信息
  assert(_size.isFinite); // 必须是有限大小
  if (child != null) child!.layout(BoxConstraints.tight(_size)); 
}

The above logic first updates _sizethe field, indicating the size provided by Embedder FlutterView, generally the size of the screen, and then calls layoutthe method of the child node:

// flutter/packages/flutter/lib/src/rendering/object.dart
void layout(Constraints constraints, {
    
     bool parentUsesSize = false }) {
    
    
  RenderObject? relayoutBoundary; // 更新布局边界
  if (!parentUsesSize || sizedByParent || constraints.isTight ||
        parent is! RenderObject) {
    
     // 第1步,当前RenderObject是布局边界
    relayoutBoundary = this;
  } else {
    
     // 否则使用父类的布局边界
    relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
  }
  if (!_needsLayout && constraints == _constraints &&
        relayoutBoundary == _relayoutBoundary) {
    
     // 第2步,无须重新Layout
    return;
  }
  _constraints = constraints; // 第3步,更新约束信息
  if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
    
    
    visitChildren(_cleanChildRelayoutBoundary); // 后面分析
  }
  _relayoutBoundary = relayoutBoundary; // 更新布局边界
  if (sizedByParent) {
    
    // 第4步,子节点大小完全取决于父节点
     performResize(); // 重新确定组件大小
  }
  performLayout(); // 第5步,子节点自身实现布局逻辑
  markNeedsSemanticsUpdate();
  _needsLayout = false; // 当前节点的Layout流程完成
  markNeedsPaint(); // 标记当前节点需要重绘 
}

The above logic is mainly divided into 5 steps.

layout boundary (relayoutBoundary)

The code corresponding to step 1, let's look at it separately:

RenderObject? relayoutBoundary; // 更新布局边界
// parent is! RenderObject 为 true 时则表示当前组件是根组件,因为只有根组件没有父组件。
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
    
    
  relayoutBoundary = this; // 当前RenderObject是布局边界
} else {
    
     // 否则使用父类的布局边界
  relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
}

The main purpose here is to judge whether the current RenderObjectnode is a layout boundary node. relayoutBoundaryA component is a layout boundary node if it satisfies one of the following four conditions :

  1. !parentUsesSize: When the size of the parent component does not depend on the size of the current component , that is to say, the parent node does not use its own sizeinformation. In this case, the parent component will pass a parameter to the child component when calling the layout function of the child component parentUsesSize = false, indicating the layout algorithm of the parent component Will not depend on the size of the child components. For example, RenderViewthe default parameter is used when calling the layout method, so it is a layout boundary node.

  2. sizedByParent: The size of the component only depends on the constraints passed by the parent component , that is to say, the size of the current RenderObjectnode is completely controlled by the parent node and not affected by the child nodes. In this way, the size change of the descendant components will not affect its own size. A case component sizedByParent = true. For example RenderAndroidView, RenderOffstage, since the Layout of the child node stops at this node, it is also a layout boundary node.

  3. constraints.isTight: The constraint passed by the parent component to itself is a strict constraint (fixed width and height) . In this case, even if its own size depends on descendant elements, it will not affect the parent component. For example SliverConstraints, isTightthe field is always false, BoxConstraintswhen the minimum width and height are greater than or equal to the maximum width and height true. This indicator truealso shows that RenderObjectthe size of the current node is fixed, so no matter how its child nodes are laid out, its side effects will not exceed the current node.

  4. parent is! RenderObject: The component is the root node ; the root component of the Flutter application is RenderView , and its default size is the current device screen size .

The essence of the above four conditions is that the side effects generated by the layout of the child nodes will not diverge from the current node, so the current node can be considered as a boundary node . Otherwise, the current node inherits the layout bounds of the parent node .


Regarding the layout boundary, let's look at another example. If there is a component tree structure of a page as shown in the figure:

insert image description here

If Text3the text length of changes, Text4the position and Column2size of will also change; and because Column2the parent component of SizedBoxhas limited size, SizedBoxthe size and position of will not change. So in the end, relayoutthe components we need to carry out are: Text3, Column2, here we need to pay attention to:

  1. Text4There is no need to re-layout, because Text4the size of has not changed, but its position has changed, and its position is Column2determined when the parent component is laid out.

  2. It is easy to find: if there are other components between Text3and Column2, then these components are also required relayout.

In this case, Column2that's Text3( relayoutBoundaryrelayouted border nodes). Each component renderObjecthas a _relayoutBoundaryproperty pointing to its own layout boundary node . If the layout of the current node changes, all nodes on the path from itself to its layout boundary node need itrelayout .


Let's go back layoutto the source code of the method to continue the analysis:

Step 2. Determine whether it can be returned directly. The conditions are: the current _needsLayoutcomponent truehas not been marked as needing to be re-layouted, the layout constraints passed by the parent node ( Constraints) have not changed as before, and the layout boundary nodes have not changed as before. , these three conditions can ensure that the current layout will not change relative to the previous frame.

Step 3, update RenderObjectthe constraint information of the current node. And, if the layout boundary changes, the layout boundary information of the child nodes should be cleared.

Step 4, if sizedByParentthe attribute is true, then call performResizethe method, the specific implementation depends on RenderObjectthe subclass, generally such RenderObjectnodes will not implement performLayoutthe method.

Step 5, execute performLayoutthe method, and finally _needsLayoutmark the field as trueand mark the current node as requiring the Paint process.

Let's take a look at _cleanRelayoutBoundarythe implementation of the method:

// flutter/packages/flutter/lib/src/rendering/object.dart
void _cleanRelayoutBoundary() {
    
    
  if (_relayoutBoundary != this) {
    
     // 自身不是布局边界,则需要清理
    _relayoutBoundary = null;
    _needsLayout = true; // 需要重新布局
    visitChildren(_cleanChildRelayoutBoundary); // 遍历并处理所有子节点
  }
}
static void _cleanChildRelayoutBoundary(RenderObject child) {
    
    
  child._cleanRelayoutBoundary();
}

The above logic will clear the information RenderObjectof the node _relayoutBoundaryand mark the current node as requiring Layout. Note that since the ancestor node of this node is already in the Layout process, it will be Layout, so this node does not need to be added to the queue to be Layout. In addition, when the child node itself is a layout boundary node, there is no need to continue to clean up its child nodes. The layout boundary node is like a barrier , which isolates the interaction between ancestor nodes and child nodes, thus making local layout possible.

At this point, we can briefly summarize the Layout process:

insert image description here

sizedByParent

In layoutthe method, there is the following logic:

if (sizedByParent) {
    
    
  performResize(); //重新确定组件大小
}

When we said above, sizedByParentit truemeans: the size of the current component only depends on the constraints passed by the parent component, and does not depend on the size of the descendant components. As we said earlier, performLayoutwhen determining the size of the current component, it usually depends on the size of the subcomponent. If sizedByParentis true, the size of the current component does not depend on the size of the subcomponent. For the sake of logic clarity, the Flutter framework agrees that when sizedByParentistrue , determine The logic of the size of the current component should be extracted into performResize(). In this case, performLayoutthere are only two main tasks: layout the subcomponents and determine the offset of the layout start position of the subcomponents in the current component.

Let's use an AccurateSizedBoxexample to demonstrate how we should layout when sizedByParentfor :true

AccurateSizedBox

The component in Flutter SizedBoxwill pass the constraints of its parent component to its child components, which means that if the parent component limits the minimum width to 100, even if we SizedBoxspecify a width of 50, it is useless, because SizedBoxthe implementation of Let SizedBoxthe subcomponents of the first satisfy SizedBoxthe constraints of the parent component .

Remember the earlier example where we wanted AppBarto limit loadingthe size of the component in the :

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

The actual result is shown in the figure:
insert image description here

The reason why it doesn’t work is because the parent component limits the minimum height. Of course, we can also use UnconstrainedBox + SizedBoxto achieve the effect we want, but here we hope that it can be done with one component. For this reason, we customize a component, AccurateSizedBoxwhich is the same SizedBoxas the main The difference is that AccurateSizedBoxit will abide by the constraints passed by its parent component, instead of letting its child components satisfy AccurateSizedBoxthe constraints of the parent component , specifically:

  1. AccurateSizedBoxIts own size depends only on the constraints of the parent component and the width and height specified by the user. ( sizedByParent = true)
  2. AccurateSizedBoxAfter determining its own size, limit the size of its subcomponents.

AccurateSizedBoxThe implementation code is as follows:

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;

  
  RenderObject createRenderObject(BuildContext context) {
    
    
    return RenderAccurateSizedBox(width, height);
  }

  
  void updateRenderObject(context, RenderAccurateSizedBox renderObject) {
    
    
    renderObject
      ..width = width
      ..height = height;
  }
}

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

  double width;
  double height;

  // 当前组件的大小只取决于父组件传递的约束
  
  bool get sizedByParent => true;


  // performResize 中会调用
  
  Size computeDryLayout(BoxConstraints constraints) {
    
    
    //设置当前元素宽高,遵守父组件的约束
    return constraints.constrain(Size(width, height));
  }

  // @override
  // void performResize() {
    
    
  //   // default behavior for subclasses that have sizedByParent = true
  //   size = computeDryLayout(constraints);
  //   assert(size.isFinite);
  // }

  
  void performLayout() {
    
    
    child!.layout(
      BoxConstraints.tight(
          Size(min(size.width, width), min(size.height, height))), // 限制child大小
      // 父容器是固定大小,子元素大小改变时不影响父元素
      // parentUseSize为false时,子组件的布局边界会是它自身,子组件布局发生变化后不会影响当前组件
      parentUsesSize: false,
    );
  }
}

There are three things to note about the above code:

  1. Our is RenderAccurateSizedBoxno longer directly inherited from RenderBox, but inherited from RenderProxyBoxWithHitTestBehavior, RenderProxyBoxWithHitTestBehaviorindirectly inherited from RenderBox, which contains the default hit test and drawing related logic, and we don't need to implement it manually after inheriting from it.

  2. We moved the logic of determining the size of the current component to computeDryLayoutthe method, because the method RenderBoxof the performResizewill be called computeDryLayout, and the returned result will be the size of the current component. According to the Flutter framework convention, we should rewrite computeDryLayoutthe method instead of the method, just like we should rewrite the method instead of the method performResizewhen laying out ; however, this is just a convention, not mandatory, but we should try to follow this convention as much as possible, unless you know clearly The best people who know what they are doing and can ensure that the people who maintain your code in the future do too.performLayoutlayout

  3. RenderAccurateSizedBoxlayoutWhen calling the method of the child component , parentUsesSizeset to false, so that the child component will become a layout boundary .

Let's test it out:

class AccurateSizedBoxRoute extends StatelessWidget {
    
    
  const AccurateSizedBoxRoute({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    final child = GestureDetector(
      onTap: () => print("tap"),
      child: Container(width: 300, height: 300, color: Colors.red),
    );
    return Row(
      children: [
        ConstrainedBox(
          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,
            ),
          ),
        ),
      ],
    );
  }
}

running result:

insert image description here
It can be seen that when the parent component constrains the size, width, and height of the child component 100x100, we cannot succeed by SizedBoxspecifying the size of the child component Containeras , but succeed through .50×50AccurateSized

It needs to be emphasized here: if a component's is sizedByParent, then it can also be set to truewhen laying out sub-components , which means that it is a layout boundary, and setting or determines whether the sub-component is a layout boundary, both It's not contradictory, don't confuse this. For example, in the implementation of the component that comes with Flutter , its function is to pass the method when calling the method of the subcomponent . For details, you can view the implementation source code of the component.parentUsesSizetruesizedByParenttrueparentUsesSizetruefalseOverflowBoxsizedByParenttruelayoutparentUsesSizetrueOverflowBox

AfterLayout

We have introduced custom AfterLayoutcomponents before, now let's take a look at its implementation principle.

AfterLayoutRenderAfterLayoutYou can get the proxy rendering object ( ) of the subcomponent after the layout is completed , RenderAfterLayoutand the object will proxy the subcomponent rendering object. Therefore, through RenderAfterLayoutthe object, you can also get the properties of the subcomponent rendering object, such as the size and position of the subcomponent.

AfterLayoutThe implementation code is as follows:

class AfterLayout extends SingleChildRenderObjectWidget {
    
    
  AfterLayout({
    
    
    Key? key,
    required this.callback,
    Widget? child,
  }) : super(key: key, child: child);

  
  RenderObject createRenderObject(BuildContext context) {
    
    
    return RenderAfterLayout(callback);
  }

  
  void updateRenderObject(
      BuildContext context, RenderAfterLayout renderObject) {
    
    
    renderObject..callback = callback;
  }
  ///组件树布局结束后会被触发,注意,并不是当前组件布局结束后触发
  final ValueSetter<RenderAfterLayout> callback;
}

class RenderAfterLayout extends RenderProxyBox {
    
    
  RenderAfterLayout(this.callback);

  ValueSetter<RenderAfterLayout> callback;

  
  void performLayout() {
    
    
    super.performLayout();
    //  不能直接回调callback,原因是当前组件布局完成后可能还有其他组件未完成布局
    //  如果callback中又触发了UI更新(比如调用了 setState)则会报错。因此,我们在 frame 结束的时候再去触发回调。
    SchedulerBinding.instance
        .addPostFrameCallback((timeStamp) => callback(this));
  }

  /// 组件在屏幕坐标中的起始点坐标(偏移)
  Offset get offset => localToGlobal(Offset.zero);
  /// 组件在屏幕上占有的矩形空间区域
  Rect get rect => offset & size;
}

There are three things to note about the above code:

  1. callbackThe timing of the call is not to call immediately after the layout of the subcomponent is completed, because there may be other components that have not completed the layout after the layout of the subcomponent is completed. If it is called at this time, callbackonce callbackthere is a code that triggers the update (such as calling setState) in , an error will be reported. So we frametrigger the callback at the end.

  2. RenderAfterLayoutThe method of the performLayoutparent class is called directly :RenderProxyBoxperformLayout

void performLayout() {
    
    
  if (child != null) {
    
    
    child!.layout(constraints, parentUsesSize: true);
    size = child!.size;
  } else {
    
    
    size = computeSizeForNoChild(constraints);
  }
}

It can be seen that the constraints passed by the parent component to itself are passed directly to the child component, and the size of the child component is set to its own size. That is RenderAfterLayout, the size of the is the same as the size of its subcomponents

  1. We have defined offsetand recttwo properties, which are the component's position offset relative to the screen and the rectangular space it occupies. But in actual combat, what we often need to obtain is the coordinates and rectangular space range of a sub-component relative to a parent component. At this time, we can call the method. For example, the following code shows that a sub-component RenderObjectobtains localToGlobalthe Stackrelative Stackrectangle Space range:
...
Widget build(context){
    
    
  return Stack(
    alignment: AlignmentDirectional.topCenter,
    children: [
      AfterLayout(
        callback: (renderAfterLayout){
    
    
         //我们需要获取的是AfterLayout子组件相对于Stack的Rect
         _rect = renderAfterLayout.localToGlobal(
            Offset.zero,
            //找到 Stack 对应的 RenderObject 对象
            ancestor: context.findRenderObject(),
          ) & renderAfterLayout.size;
        },
        child: Text('Flutter@wendux'),
      ),
    ]
  );
}

Constraints

Constraints(Constraints) mainly describes the minimum and maximum width and height restrictions. Understanding how the component determines the size of itself or its child nodes according to the constraints during the layout process is very helpful for us to understand the layout behavior of the component. Now we will implement it through a 200x200red Containerexample to illustrate. In order to eliminate interference, we let the root node ( RenderView) be Containerthe parent component of , our code is:

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

But after actually running it, you will find that the entire screen turns red! why? Let's look at RenderViewthe layout implementation of :


void performLayout() {
    
    
  // configuration.size 为当前设备屏幕
  _size = configuration.size; 
  if (child != null)
    child!.layout(BoxConstraints.tight(_size)); // 强制子组件和屏幕一样大
}

It can be found that RenderViewthe subcomponent is passed a strict constraint, that is, the size of the subcomponent is forced to be the size of the screen, so it Containerfills the screen .

Here we need to introduce two commonly used constraints:

  • Loose constraints : No minimum width and height (0), only maximum width and height , which can be BoxConstraints.loose(Size size)quickly created by .
  • Strict constraints : limited to a fixed size; that is, the minimum width is equal to the maximum width, and the minimum height is equal to the maximum height , which can be BoxConstraints.tight(Size size)quickly created by .

So how can we make the specified size take effect? The standard answer is to introduce an intermediate component, let this intermediate component obey the constraints of the parent component, and then pass new constraints to the child component .

For this example, the easiest way is to Alignwrap it with a component Container:


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

AlignIt will abide RenderViewby the constraints of , let itself fill the screen, and then pass a loose constraint to the subcomponent (the minimum width and height is 0, the maximum width and height is 200), so Containerthat can become 200 x 200.

Of course, we can also use other components instead Align, for example UnconstrainedBox, but the principle is the same, you can check the source code verification.

Summarize

insert image description here

Now let's take a look at the official website's explanation of the Flutter layout:

  • "During layout, Flutter traverses the rendering tree in a DFS (depth-first traversal) manner, and passes constraints from parent nodes to child nodes in a top-down manner. To determine their own size, a child node must Follow the constraints passed by the parent node. The child node responds by passing the size to the parent node in a bottom-up fashion within the constraints established by the parent node."

Do you understand it more thoroughly!


reference:

Guess you like

Origin blog.csdn.net/lyabc123456/article/details/130945854