Discussion on layout Process

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

1. The parent node transmits constraint information to the child node to limit the maximum and minimum width and height of the child node.

2. The child node determines its size according to its constraint information (Szie).

3. The parent node determines the position of each child node in the parent node space according to specific rules (different components will have different layout algorithms), which is represented by offset.

4. Recurse the whole process to determine the position and size of each node.

As you can see, the size of the component is determined by itself, and the location of the component is determined by the parent component.

Flutter has many layout class components, which can be divided into monad components and subcomponents according to the number of children. Let’s define a monad component and subcomponent respectively to understand the Fluuter layout process.


Example monad component layout

We customize a monad component, CustomCenter. Announcements are basically the same as Center, and this example demonstrates the main flow of the layout.

To demonstrate the principle, we do not implement components as a composition, but as a custom RenderObject. Because the middle component needs to include a child node, so we inherit SingleChildRenderObjectWidget.

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

  @override
  RenderObject createRenderObject(BuildContext context) {
    returnRenderCustomCenter(); }}Copy the code

Then implement RenderCustomCenter:

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

  @override
  void performLayout() {
    //1, perform layout on the child component, then obtain its size
    child.layout(constraints.loosen(), // Pass the constraint to the child node
        parentUsesSize: true // Cannot be false because the size of the child is to be used next
        );
    //2, determine its own size based on the size of the child component
    size = constraints.constrain(Size(
        constraints.maxWidth == double.infinity
            ? child.size.width
            : double.infinity,
        constraints.maxHeight == double.infinity
            ? child.size.height
            : double.infinity));

    //3, according to the size of the parent node, calculate the offset of the child node after centering in the parent node,
    // Then store the offset in the parentData of the child node, which will be used later in the drawing node
    BoxParentData parentData = child.parentData as BoxParentData;
    parentData.offset = ((size - child.size) as Offset) / 2; }}Copy the code

The above code would have inherited RenderObject a little more low-level, but that would have required us to manually implement some layout-related logic, such as event distribution. To focus more on the layout itself, we chose to inherit RenderShiftedBox, which will help us implement functions outside of the layout, so we only need to rewrite performLayout. In the function to achieve the center algorithm.

The layout process is illustrated in the comments above, but there are three additional points to note:

  1. Constraints are the constraints that the parent of the CustomCenter component passes to it when performing a Layout on the child component. The constraint we pass to the byte is constraints.loosen(). Here’s what lossen does:

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

    Obviously, the maximum width and height of the CustomCenter constraint byte cannot exceed its own maximum width and height

  2. The child node determines its width and height under the constraints of the parent node (CustomCenter). In this case, CustomCenter determines the width and height of the child node.

    The code logic above is that if the parent node’s constraint is infinite, its width is the width of bytes, otherwise its width is infinite.

    Note that it would be a problem to set the CustomCenter width and height to infinity at this time, because if the CustomCenter width and height are also infinite in an infinite range, then the parent node would be overwhelmed. The screen size is fixed, which makes no sense.

    If the CustomCenter parent does not pass the width and height to infinity, then you can set your own width and height to infinity, because in a limited space, the child is set to infinity, which is the size of the parent.

    In short, CustomCenter fills as much space as possible for its parent element

  3. After CustomCenter has determined its size and the size of the child nodes, you can determine the location of the child nodes. According to the centering algorithm, the origin coordinates of the child node are calculated and saved in the parentData of the child node, which will be used in the subsequent drawing stage. For details, let’s look at the default implementation of RenderShiftedBox:

      @override
      void paint(PaintingContext context, Offset offset) {
        if(child ! =null) {
          final BoxParentData parentData = child.parentData as BoxParentData;
          / / from the child. ParentData retrieves the offset of the child node relative to the current node, plus the on-screen offset of the current node
          // Is the offset of the child node in the screencontext.paintChild(child, parentData.offset + offset); }}Copy the code

PerformLayout

As you can see from the above, the layout logic is implemented in the performLayout method, we summarize performLayout in the specific things to do:

  1. If there are child components, they are sorted recursively
  2. Determines the current component size (size). Notifications depend on the size of the child components
  3. Determines the starting offset of a child component within the current component

There are many common Flutter components in the Flutter library, such as Align, SizeBox, DecoratedBox, etc., that you can open source to see the implementation.


Example multi-sub-component layout

In the actual development, we often use the layout of the left and right edge, now let’s implement a LeftRightBox component, to achieve the left and right edge.

First we define components, components, and list many children component needs to inherit from MultiChildRenderObjectWidget:

class LeftRightBox extends MultiChildRenderObjectWidget {
  LeftRightBox({Key key, @required List<Widget> list})
      : assert(list.length == 2."Can only pass two children"),
        super(key: key, children: list);

  @override
  RenderObject createRenderObject(BuildContext context) {
    returnRenderLeftRight(); }}Copy the code

RenderLeftRight performLayout implements the left/right layout algorithm:

class LeftRightParentData extends ContainerBoxParentData<RenderBox> {}

class RenderLeftRight extends RenderBox
    with
        ContainerRenderObjectMixin<RenderBox.LeftRightParentData>,
        RenderBoxContainerDefaultsMixin<RenderBox.LeftRightParentData> {
  /// Initialize parentData of each child
  @override
  void setupParentData(covariant RenderObject child) {
    if (child.parentData is! LeftRightParentData)
      child.parentData = LeftRightParentData();
  }

  @override
  void performLayout() {
    // Get the current constraint (passed in from the parent component),
    final BoxConstraints constraints = this.constraints;

    // Get the constraints passed by the first component and its parent
    RenderBox leftChild = firstChild;
    LeftRightParentData childParentData =
        leftChild.parentData as LeftRightParentData;
    // Get the next component
    // The next component is available because the mount of multiple child components creates all the children and inserts them into the child's childParentData
    RenderBox rightChild = childParentData.nextSibling;

    // Limit the width of the right child to no more than half of the total width
    rightChild.layout(constraints.copyWith(maxWidth: constraints.maxWidth / 2),
        parentUsesSize: true);

    // Set offset for the right child node
    childParentData = rightChild.parentData as LeftRightParentData;
    // at the far right
    childParentData.offset =
        Offset(constraints.maxWidth - rightChild.size.width, 0);

    // the default value of the left child's offset is (0,0). To ensure that the left child is always displayed, we do not change its offset
    leftChild.layout(
        constraints.copyWith(
            // The maximum remaining width on the left
            maxWidth: constraints.maxWidth - rightChild.size.width),
        parentUsesSize: true);

    // Set the size of leftRight itself
    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}) {
    returndefaultHitTestChildren(result, position: position); }}Copy the code

Use as follows:

 Container(
  child: LeftRightBox(
    list: [
      Text("Left"),
      Text("Right"),,),),Copy the code

Let’s do a simple analysis of the above process:

1. Obtain constraint information for the current component

2. Get two child components

3, Layout the two sub-components, and the width of the right component should not exceed half of the total width, and set the offset of the components to the right. The left component is then laid out, the width of the left child is the total width – the width of the right child, and no offset is set. The default offset is 0

4. Set the size of the current component to the Max height of the child component.

As you can see, the actual layout process is not much different from a monad component, except that multiple sub-components need to be laid out for multiple components.

L Unlike RenderCustomCenter, RenderLeftRight directly inherits RenderBox, At the same time with the two ContainerRenderObjectMixin and RenderBoxContainerDefaultsMixin mixins, which help us to achieve the grinding of two mixin’s drawing and event processing logic.

Layout update

In theory, when the layout of one component changes, it affects the layout of the other components, so when the layout of one component changes, the dumbest solution is to rearrange the entire component tree. However, the cost of reLayout for all components is still relatively large, so we need to explore ways to reduce the cost of reLayout. In fact, in some specific scenarios, after the components change, we only need to rearrange specific components, without reLayout for the whole tree.

Layout of the boundary

Suppose you have a page with a component tree structure that looks like this:

If Text3’s text length changes, Text4’s position will change, and Column2’s height will change accordingly. And because the width and height of the SizedBox are fixed. So the final components that need reLayout are: Text3, Colum2, note here:

  1. Text4 does not need to be rearranged because the size of Text4 has not changed, only its position, which was determined when the parent component Colum2 was laid out.
  2. It’s easy to see: if there are other components between Text3 and Column2, those components also need reLayout.

In this case Column2 is the relayoutBoundary of Text3. Each component’s renderObject has a _relayoutBoundary property pointing to its own layout. If the current node layout changes, all nodes on its path to _relayoutBoundary will need to reLayout.

What is the condition of whether a component is relayoutBoundary or not? There is a rule and four scenarios. The rule is that “the size change of the component itself does not affect the parent component”. A component is a relayoutBoundary if it satisfies one of the following four conditions:

  1. The parent size of the current component does not depend on the current component size; In this case, the parent component calls the child component layout function and passes a parentUserSize parameter to the child component. If this parameter is false, the parent component’s layout algorithm does not depend on the size of the child component.
  2. Component size depends only on constraints passed by the parent component, not on the size of descendant components. In this case, the component’s sizedByParent property must be true.
  3. The constraint that the parent passes to itself is a strict constraint (fixed width and height); In this case, the parent component is not affected even though its size depends on the descendant element.
  4. Component Wie root component; The root component of the Fluuter application is RenderView, whose default size is the size of the current device screen.

The corresponding implementation code is:

if(! parentUsesSize || sizedByParent || constraints.isTight || parentis! RenderObject) {
  _relayoutBoundary = this;
} else {
  _relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
}
Copy the code

The if criteria in the code correspond to the four criteria above, and all of them are straightforward except for the second one (sizeByParent is true). The second condition will be discussed later.

markNeedsLayout

When the layout changes, he needs to call the markNeedsLayout method to update the layout, which has two main functions:

1. Mark all nodes on its relayoutBoundary path as “need layout”

2. It requests a new frame. Nodes marked “Need layout” are rearranged in the new frame

void markNeedsLayout() {
  // If the current component is not a layout boundary node
  if(_relayoutBoundary ! =this) {
    // The recursive tag takes the current node to the layout boundary node
    markParentNeedsLayout();
  } else {
    // If it is a layout boundary node
    _needsLayout = true;
    if(owner ! =null) {
      // Add the layout boundary node to the piplineowner. _nodesNeedingLayout listowner! ._nodesNeedingLayout.add(this);
      // The function will eventually request a new frameowner! .requestVisualUpdate(); }}}Copy the code

flushLayout

Once markNeedsLayout is executed, its relayoutBoundary is added to the piplineowner._nodesNeedingLayout list and a new frame is requested.

When a new frame arrives, the piplineowner. drawFrame method is executed:

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

FlushLayout rearranges the objects that were previously added to _nodesNeedingLayout as follows:

void flushLayout() {
    while (_nodesNeedingLayout.isNotEmpty) {
      final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
      _nodesNeedingLayout = <RenderObject>[];
      // Install nodes in the tree depth from small to large after a new layout
      for (final RenderObject node indirtyNodes.. sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {if (node._needsLayout && node.owner == this)
          // Rearrange the layoutnode._layoutWithoutResize(); }}}Copy the code

Take a look at the implementation of _layoutwithoutResize()

void _layoutWithoutResize() {
  try {
    // Recursively rearrange
    performLayout();
    markNeedsSemanticsUpdate();
  } catch (e, stack) {
    _debugReportException('performLayout', e, stack);
  }
  _needsLayout = false;
  // Update the UI after the layout is updated
  markNeedsPaint();
}
Copy the code

Until this layout update is complete.

Layout process

If the component has a child component, you need to call the child component’s layout in performLayout to layout the child component, as follows:

void layout(Constraints constraints, { bool parentUsesSize = false }) {
  RenderObject? relayoutBoundary;
  
  // Determine the current layout boundary
  if(! parentUsesSize || sizedByParent || constraints.isTight || parentis! RenderObject) {
    relayoutBoundary = this;
  } else {
    relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
  }
  // _neessLayout marks whether the current component is marked as needing a layout
  // _constraints is the constraint passed to the current component by the parent during the last layout
  // _relayoutBoundary Specifies the layout boundary of the current component from the last layout
  // So, when the current component is not marked for layout and the constraints passed by the parent component have not changed
  // If there is no change to the boundary of the layout, you do not need to rearrange the layout
  if(! _needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) { ....///
    return;
  }
  // Cache constraints and layout boundaries if layout is required
  _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; } ());// Explain later
  if (sizedByParent) {
     performResize();
  }

  // Execute the layout
  performLayout();

   // Set _needsLayotu to false after the layout is complete
  _needsLayout = false;
  
  // Redraw the current component marker because the layout changes and needs to be redrawn
  markNeedsPaint();
}
Copy the code

Briefly talk about the layout process:

  1. Determines the layout boundaries of the current component
  2. Determine whether the layout needs to be rearranged. If it is not necessary, the system will return directly. If it is not necessary, it needs to be rearranged. When no layout is required, three conditions must be met
    • The checkered component is not marked as needing to be rearranged.
    • The constraints passed by the parent component have not changed.
    • The current component layout boundary has not changed.
  3. Call performLayout for layout, because performLayout will call the layout method of the child component, so this is a recursive process, recursive completion of the layout of the entire component is completed.
  4. Request to redraw

sizedByParent

In the Layout method, there is the following logic:

  if (sizedByParent) {
     performResize();
  }
Copy the code

As we said, sizeByParent true means that the size of the current component depends on the constraints passed by the parent component, not the size of the later component. As we said earlier, the size of the current component in performLayout is usually dependent on the size of the child component. If sizedByParent is true, the size of the current component is not dependent on the size of the child component.

To make the logic clear, the Flutter framework specifies that when sizedByParent is true, the logic that determines the current component size should be removed to performResize(). In this case, performLayout has only two main tasks: Layout and determine the offsets of the child components within the current component.

Here’s an example with AccurateSizedBox to show how we should layout when sizebyParent is true:

AccurateSizeBox

The SizeBox in a Flutter will pass the constraints of its parent to its children. This means that if the parent limits its latest width to 100, specifying a width of 50 via SizeBox is useless.

Because the implementation in SizeBox makes the children of SizedBox satisfy the constraints of the parent of SizeBox first. Such as:

 AppBar(
    title: Text(title),
    actions: <Widget>[
      SizedBox( // Use SizedBox to customize loading width and height
        width: 20, 
        height: 20,
        child: CircularProgressIndicator(
          strokeWidth: 3,
          valueColor: AlwaysStoppedAnimation(Colors.white70),
        ),
      )
    ],
 )
Copy the code

The actual result is that progress is the height of the AppBar.

Check the SizedBox source code as follows:

@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); }}// Returns a new box constraint that respects the given constraint while being as close to the original constraint as possible
BoxConstraints enforce(BoxConstraints constraints) {
    return BoxConstraints(
        // clamp: Returns a value between low and high based on the number
        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),
    );
}
Copy the code

You can see that this does not take effect because the parent component limits the minimum height, and the child components in SizeBox satisfy the parent’s constraints first. Of course, we can also achieve the desired effect by using UnconstrainedBox + SizedBox, but here we want to use a layout, for which we define a custom AccurateSizeBox component.

The main difference between the AccurateSizedBox and SizedBox is that the AccurateSizedBox itself complies with the constraints passed by its parent component, rather than allowing its child components to meet the constraints of the parent component of the AccureateSizeBox. Specifically,

  1. The AccurateSizedBox itself depends only on the constraints of the parent component and its width and height.
  2. The AccurateSizedBox limits the size of its sub-components after determining its own size.
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, covariantRenderAccurateSizeBox renderObject) { renderObject .. width = width .. height = height; }}class RenderAccurateSizeBox extends RenderProxyBoxWithHitTestBehavior {
  RenderAccurateSizeBox(this.width, this.height);

  double width;
  double height;

  // The size of the current component depends only on the constraints passed by the parent component
  @override
  bool get sizedByParent => true;
    
  // performResize is called
  @override
  Size computeDryLayout(BoxConstraints constraints) {
    // Set the width and height of the current element, obeying the constraints of the parent component
    return constraints.constrain(Size(width, height));
  }  

  @override
  void performLayout() {
    child.layout(
        BoxConstraints.tight(
            Size(min(size.width, width), min(size.height, height))),
        // The parent container is of a fixed size, and changes in the child size do not affect the parent element
        // When parentUserSize is false, the layout boundary of the child component is itself. Changes in the layout of the child component do not affect the current component
        parentUsesSize: false); }}Copy the code

There are three things to note about the code above:

  1. Our RenderAccurateSizedBox not inherit from RenderBox, but inherit RenderProxyBoxWithHitTestBehavior, RenderProxyBoxWithHitTestBehavior is inherited from the RenderBox indirectly, it contains the default hit test and draw relevant logic, then don’t need to inherit it we implemented manually.

  2. We move the logic to determine the current component size into the computeDryLayout method because RenderBox’s performResize method calls the computeDryLayout and returns the result as the current component size.

    According to the Flutter framework convention, we should rewrite the computeDryLayout method instead of the performResize method. We should override the performLayout method instead of the Layout method; However, this is a convention, not a mandatory one, but it should be followed as much as possible, unless you know exactly what you’re doing and can make sure that whoever maintains your code later does too.

  3. RenderAccurateSizedBox sets parentUserSize to false when calling the child layout, so that the child becomes a layout boundary.

The tests are as follows:

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(
          // Limit height to 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), ), ) ], ); }}Copy the code

As shown above, when the parent component width and height is 100, we cannot use SizedBox to specify the Container size to be 50×50. He succeeded with AccurateSizedBox.

Note that if a component’s sizeByParent is true, it can also put parentUserSize in the layout of its child components, and sizeByParent is true to indicate that it is the boundary of the layout.

Setting parentUsesSize to true or false determines whether or not a child component is a layout boundary, and the two are not in conflict. This should not be confused.

In addition, the sizeByParent of the OverflowBox component of Flutter is true. ParentUsesSize is also true when the subcomponent layout is called. See the OverflowBox source for details

Constraints

Constraints mainly describes the minimum and maximum width and height Constraints. Understanding how components determine the size of themselves or their children during layout is helpful in understanding the layout behavior of components.

The RenderView is the parent component of the Container. The RenderView is the parent component of the Container. The RenderView is the parent component of the Container.

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

When you run it, you’ll notice that the entire screen is red. Why? Let’s look at RenderView implementation:

@override
void performLayout() {
  // Configurateion. Sieze is the screen for the current device
  _size = configuration.size;
  assert(_size.isFinite);
  if(child ! =null) child! .layout(BoxConstraints.tight(_size));// Force child components to be the same size as the screen
}
Copy the code

There are two common constraints that need to be introduced:

  1. Loose constraint: not limit the minimum width height (0), only limit the maximum width height, can passBoxConstraints.loose(Size size)To create it quickly.
  2. Strict constraints: Restrictions to fixed size, i.e. minimum width equals maximum width, minimum height equals maximum height, can be passedBoxConstraints.thght(Size)To create it quickly.

As you can see, RenderView imposes a strict constraint on the child components to be equal to the screen size, so the Container fills the screen.

So how do we make the specified size work? The answer is “introduce an intermediate component, make it obey the constraints of the parent, and then pass the new constraints to the children.” For this example, the easiest way to wrap the Container is to use an Align component:

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

Align adheres to the RenderView constraint, fills the screen with itself, and then gives its children a loose constraint (minimum width 0, maximum width 200) so that the Container can become 200 by 200.

Of course, we could use other components instead of Align, such as UnconstrainedBox, but the principle is the same. Check the source code for verification.

For example, the layout process of Align is as follows:

void performLayout() {
  final BoxConstraints constraints = this.constraints;
  final boolshrinkWrapWidth = _widthFactor ! =null || constraints.maxWidth == double.infinity;
  final boolshrinkWrapHeight = _heightFactor ! =null || constraints.maxHeight == double.infinity;

  if(child ! =null) {
    // The subcomponent has loose constraints, and the subcomponent is not set to layout boundaries (indicating that the current component needs to be refreshed if the subcomponent changes)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, )); }}Copy the code

conclusion

Now that we are familiar with the flutter layout process, let’s look at a diagram from the website:

Flutter traverses the render tree in DFS(depth-first traversal) and restricts top-down passing from parent to child nodes as it layouts. If a child node needs to determine its own size, it must comply with the parent node’s passing restrictions. The child responds by passing the size top-down to the parent within the constraints established by the parent.

Does that make sense to you a little bit

Recommended reading

  • This article learns about BuildContext
  • The principle and use of Key
  • Construction process analysis of Flutter three trees
  • # Start, render, setState process

The resources

  • Because Chinese website

If this article is helpful to you, we are honored, if there are mistakes and questions in the article, welcome to put forward