头图


Preface

Before reading this article, let's review first in the Flutter development process, do you often encounter the following problems:

  • Container setting width and height is invalid
  • Column overflows the boundary, Row overflows the boundary
  • When should you use ConstrainedBox and UnconstrainedBox

Whenever I encounter this kind of problem, I keep trying, and after a lot of effort, Widget finally obediently (achieving the desired effect). After learning from it, I finally started to resist (get up, people who don’t want to be slaves, sing the national anthem~), why is the container width and height invalid? Why does Column overflow the boundary again? With full of enthusiasm, I finally plucked up the courage to start with the Container source code and uncover its mystery one by one.

Layout rules

Before talking about this article, we should first understand the following rules in Flutter layout:

  • First, the upper Widget transfers constraint conditions to the lower Widget
  • Secondly, the lower Widget transmits size information to the upper Widget
  • Finally, the upper Widget determines the position of the lower Widget

If we are not able to use these rules proficiently during development, we will not be able to fully understand the principles during layout, so the sooner we master these rules, the better.

  • The Widget will get its own constraints through its parent. Constraints are actually a collection of 4 floating-point types: maximum/minimum width, and maximum/minimum height.
  • Then the Widget will traverse its children list one by one, pass constraints to the children (the constraints between children may be different), and then ask each of its children for the size of the layout.
  • Then this Widget will lay out its children one by one.
  • Finally, the Widget will pass its size information up to the parent Widget (including its original constraints).

Tight vs. Loose

Strict constraint is the choice to obtain the exact size, in other words, its maximum/minimum width is the same, and the height is the same.

// flutter/lib/src/rendering/box.dart
BoxConstraints.tight(Size size)
    : minWidth = size.width,
      maxWidth = size.width,
      minHeight = size.height,
      maxHeight = size.height;

The loose constraint is to set the maximum width/height, but allow its child Widget to obtain any size smaller than it. In other words, the minimum width/height of the loose constraint is 0.

// flutter/lib/src/rendering/box.dart
BoxConstraints.loose(Size size)
    : minWidth = 0.0,
      maxWidth = size.width,
      minHeight = 0.0,
      maxHeight = size.height;

Container part of the source code

First of all, we will provide some source code of Container. Below we will analyze the source code one by one in combination with specific scenarios.

// flutter/lib/src/widgets/container.dart
class Container extends StatelessWidget {
  Container({
    Key key,
    this.alignment,
    this.padding,
    this.color,
    this.decoration,
    this.foregroundDecoration,
    double width,
    double height,
    BoxConstraints constraints,
    this.margin,
    this.transform,
    this.child,
    this.clipBehavior = Clip.none,
  })  : assert(margin == null || margin.isNonNegative),
        assert(padding == null || padding.isNonNegative),
        assert(decoration == null || decoration.debugAssertIsValid()),
        assert(constraints == null || constraints.debugAssertIsValid()),
        assert(clipBehavior != null),
        assert(
            color == null || decoration == null,
            'Cannot provide both a color and a decoration\n'
            'To provide both, use "decoration: BoxDecoration(color: color)".'),
        constraints = (width != null || height != null)
            ? constraints?.tighten(width: width, height: height) ??
                BoxConstraints.tightFor(width: width, height: height)
            : constraints,
        super(key: key);

  final Widget child;

  // child 元素在 Container 中的对齐方式
  final AlignmentGeometry alignment;

  // 填充内边距
  final EdgeInsetsGeometry padding;

  // 颜色
  final Color color;

  // 背景装饰
  final Decoration decoration;

  // 前景装饰
  final Decoration foregroundDecoration;

  // 布局约束
  final BoxConstraints constraints;

  // 外边距
  final EdgeInsetsGeometry margin;

  // 绘制容器之前要应用的变换矩阵
  final Matrix4 transform;

  // decoration 参数具有 clipPath 时的剪辑行为
  final Clip clipBehavior;

  EdgeInsetsGeometry get _paddingIncludingDecoration {
    if (decoration == null || decoration.padding == null) return padding;
    final EdgeInsetsGeometry decorationPadding = decoration.padding;
    if (padding == null) return decorationPadding;
    return padding.add(decorationPadding);
  }

  @override
  Widget build(BuildContext context) {
    Widget current = child;

    if (child == null && (constraints == null || !constraints.isTight)) {
      current = LimitedBox(
        maxWidth: 0.0,
        maxHeight: 0.0,
        child: ConstrainedBox(constraints: const BoxConstraints.expand()),
      );
    }

    if (alignment != null)
      current = Align(alignment: alignment, child: current);

    final EdgeInsetsGeometry effectivePadding = _paddingIncludingDecoration;
    if (effectivePadding != null)
      current = Padding(padding: effectivePadding, child: current);

    if (color != null) current = ColoredBox(color: color, child: current);

    if (decoration != null)
      current = DecoratedBox(decoration: decoration, child: current);

    if (foregroundDecoration != null) {
      current = DecoratedBox(
        decoration: foregroundDecoration,
        position: DecorationPosition.foreground,
        child: current,
      );
    }

    if (constraints != null)
      current = ConstrainedBox(constraints: constraints, child: current);

    if (margin != null) current = Padding(padding: margin, child: current);

    if (transform != null)
      current = Transform(transform: transform, child: current);

    if (clipBehavior != Clip.none) {
      current = ClipPath(
        clipper: _DecorationClipper(
            textDirection: Directionality.of(context), decoration: decoration),
        clipBehavior: clipBehavior,
        child: current,
      );
    }

    return current;
  }
}

Scene analysis

scene one

Scaffold(
  appBar: AppBar(
    title: Text('Flutter Container'),
  ),
  body: Container(
    color: Colors.red,
  ),
),

Use the Container alone in the Scaffold body, and set the color of the Container to Colors.red.

Open DevTools for element inspection, we can find the structure of Widget Tree Container -> ColoredBox -> LimitedBox -> ConstrainedBox, and finally a RenderConstrainedBox will be created with the width and height of the entire screen (except AppBar).

Then we can’t help asking, why is this happening? I didn’t set the width and height of the Container. Then we go back to the above source code again. If the Container does not set the child parameter and meets constraints == null || !constraints.isTight will return a LimitedBox with maxWidth of 0 and maxHeight of 0. And the child of const BoxConstraints.expand() is an element of ConstrainedBox with a constraint parameter of 061c98eb09f31f, so the Container will fill the entire screen (except AppBar).

// flutter/lib/src/widgets/container.dart

if (child == null && (constraints == null || !constraints.isTight)) {
    current = LimitedBox(
      maxWidth: 0.0,
      maxHeight: 0.0,
      child: ConstrainedBox(constraints: const BoxConstraints.expand()),
    );
  }
// flutter/lib/src/rendering/box.dart
const BoxConstraints.expand({
    double width,
    double height,
  }) : minWidth = width ?? double.infinity,
       maxWidth = width ?? double.infinity,
       minHeight = height ?? double.infinity,
       maxHeight = height ?? double.infinity;

Scene two

Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
  ),

Modify on the basis of scene one. At this time, set width to 100, height to 100, and color to Colors.red for Container.

Also open DevTools for element inspection, we can find the structure of Widget Tree Container -> ConstrainedBox -> ColorededBox, and finally a _RenderColoredBox will be created, the width and height are 100, and the color is a red square.

Through source code analysis, we can conclude that if the width and height are set in the Container and the constraints property is not set, the constraints will be assigned in the constructor first, so constraints = BoxConstraints.tightFor(width:100, height:100) , then a ColoredBox will be nested in the outer layer, and then another will be nested ConstrainedBox returns.

Container({
    Key key,
    this.alignment,
    this.padding,
    this.color,
    this.decoration,
    this.foregroundDecoration,
    double width,
    double height,
    BoxConstraints constraints,
    this.margin,
    this.transform,
    this.child,
    this.clipBehavior = Clip.none,
  }) : assert(margin == null || margin.isNonNegative),
       assert(padding == null || padding.isNonNegative),
       assert(decoration == null || decoration.debugAssertIsValid()),
       assert(constraints == null || constraints.debugAssertIsValid()),
       assert(clipBehavior != null),
       assert(color == null || decoration == null,
         'Cannot provide both a color and a decoration\n'
         'To provide both, use "decoration: BoxDecoration(color: color)".'
       ),
       constraints =
        (width != null || height != null)
          ? constraints?.tighten(width: width, height: height)
            ?? BoxConstraints.tightFor(width: width, height: height)
          : constraints,
       super(key: key);
if (decoration != null)
      current = DecoratedBox(decoration: decoration, child: current);

if (constraints != null)
      current = ConstrainedBox(constraints: constraints, child: current);

Scene three

Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Container(
      width: 100,
      height: 100,
      color: Colors.red,
      alignment: Alignment.center,
    ),
  ),

Next, we continue to add the alignment:Alignment.center attribute on the basis of the second scene.

At this point we will find why there is no center display? By looking at the Align source code, it is not difficult to find that it is to set the alignment between the child Widget and itself.

A widget that aligns its child within itself and optionally sizes itself based on the child's size.

So now let's change the code again, add sub Widget to the current Container, and finally achieve the centering effect we want.

Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Container(
      width: 100,
      height: 100,
      color: Colors.red,
      alignment: Alignment.center,
      child: Container(
        width: 10,
        height: 10,
        color: Colors.blue,
      ),
    ),
  ),

Scene four

Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Center(
      child: Container(
        color: Colors.red,
        width: 200,
      ),
    ),
  ),

Since the body element in Scaffold will fill the entire screen (except AppBar), the body tells Center to fill the entire screen, and then Center tells that the Container can be changed to any size, but the container is set to width 200, so the size of the Container is 200 width and height Unlimited.

The primary content of the scaffold.
Displayed below the [appBar], above the bottom of the ambient

Scene five

Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Center(
      child: Row(
        children: <Widget>[
          Container(
            color: Colors.red,
            child: Text(
              '我是一段很长很长很长的文字',
              style: TextStyle(
                fontSize: 30,
              ),
            ),
          ),
          Container(
            color: Colors.red,
            child: Text(
              '我是一段很短的文字',
            ),
          ),
        ],
      ),
    ),
  ),

Since Row does not impose any constraints on its children, its children are likely to be too large to exceed the width of Row. In this case, Row will display an overflow warning.

Scene six

Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Center(
      child: Container(
        constraints: BoxConstraints(
          maxHeight: 400,
          minHeight: 300,
          minWidth: 300,
          maxWidth: 400,
        ),
        color: Colors.red,
        width: 200,
      ),
    ),
  ),

Here we set the constraints attribute value of the Container to BoxConstraints(minHeight:300, maxHeight:400, minWidth:300, maxWidth:400), and set the width to 200. So when the constructor initializes the parameters, constraints = BoxConstraints(minHeight:300, maxHeight:400, minWidth:300, maxWidth:300) will be set, and a Widget Tree structure like this will be returned in the Container build function (Container -> ConstrainedBox -> ColoredBox -> LimitedBox -> ConstrainedBox).

At this time, Center tells that the Container can be changed to any size, but the constraints set by the Container are that the minimum width is 300, the maximum is 300, that is, the width is 300, the minimum height is 300, and the maximum height is 400, so the width set in the Container If it is 200, it is invalid. At this time, you may ask, what is the height? The answer is 400, because there is no child set in the Container, and the condition of child == null && (constraints == null || !constraints.isTight) met, so a ConstrainedBox(constraints: const BoxConstraints.expand() will be nested so the height will be the maximum height of 400.

// flutter/lib/src/rendering/box.dart
BoxConstraints tighten({ double width, double height }) {
  return BoxConstraints(
    minWidth: width == null ? minWidth : width.clamp(minWidth, maxWidth) as double,
    maxWidth: width == null ? maxWidth : width.clamp(minWidth, maxWidth) as double,
    minHeight: height == null ? minHeight : height.clamp(minHeight, maxHeight) as double,
    maxHeight: height == null ? maxHeight : height.clamp(minHeight, maxHeight) as double,
  );
}
// flutter/lib/src/rendering/box.dart
/// Whether there is exactly one width value that satisfies the constraints.
bool get hasTightWidth => minWidth >= maxWidth;

/// Whether there is exactly one height value that satisfies the constraints.
bool get hasTightHeight => minHeight >= maxHeight;

/// Whether there is exactly one size that satisfies the constraints.
@override
bool get isTight => hasTightWidth && hasTightHeight;
// flutter/lib/src/widgets/container.dart
if (child == null && (constraints == null || !constraints.isTight)) {
    current = LimitedBox(
      maxWidth: 0.0,
      maxHeight: 0.0,
      child: ConstrainedBox(constraints: const BoxConstraints.expand()),
    );
  }

At last

Through the above source code analysis and different scenarios, it is not difficult to find that Container is mainly composed of Widgets such as LimitedBox, ConstrainedBox, Align, Padding, ColoredBox, DecoratedBox, Transform, ClipPath, etc. by setting different parameters.

more exciting 161c98eb09f82e, please pay attention to our public "161c98eb09f831 One Hundred Bottle Technology ", there are irregular benefits!


百瓶技术
127 声望18 粉丝

「百瓶」App 技术团队官方账号。