Disobedient Container


preface

Before reading this article, let's review whether we often encounter the following problems in the development of fluent:

  • The Container has an invalid width height setting
  • Column overflow boundary, Row overflow boundary
  • When should I use ConstrainedBox and UnconstrainedBox

Whenever I encounter this kind of problem, I always keep trying, and it takes nine cattle and two tigers, The Widget finally obeyed (achieved the desired effect). After thinking hard, I finally began to resist (rise up, people who don't want to be slaves, sing the National Anthem ~). Why is the width and height of the Container invalid? Why does the Column overflow the boundary? With warm blood, I finally summoned up the courage to start with the Container source code and uncover its mystery one by one.

Layout rules

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

  • First, the upper Widget passes constraints to the lower Widget
  • Secondly, the lower Widget passes size information to the upper Widget
  • Finally, the upper Widget determines the location of the lower Widget

If we can't skillfully use these rules in development, we can't fully understand their principles in layout, so the sooner we master these rules, the better.

  • The Widget obtains its own constraints through its parents. Constraints are actually a collection of four floating-point types: maximum / minimum width, and maximum / minimum height.
  • Then the Widget will traverse its children list one by one, pass constraints to its children (the constraints may be different between children), and then ask each of its children for the size of the layout.
  • Then the Widget will layout 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 of exact size. In other words, its maximum / minimum width is the same, and its 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;

A relaxed constraint sets the maximum width / height, but allows its child Widget to obtain any size smaller than it. In other words, the minimum width / height of a relaxed 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 source code

First of all, I'll present some of the source code of the Container. Next, 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;

  // The alignment of the child element in the Container
  final AlignmentGeometry alignment;

  // Fill in margin
  final EdgeInsetsGeometry padding;

  // colour
  final Color color;

  // Background decoration
  final Decoration decoration;

  // Foreground decoration
  final Decoration foregroundDecoration;

  // Layout constraints
  final BoxConstraints constraints;

  // Margin 
  final EdgeInsetsGeometry margin;

  // The transformation matrix to apply before drawing the container
  final Matrix4 transform;

  // Clip behavior when decoration parameter has 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 1

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

The Container is used separately in Scaffold body, and the color of the Container is set to colors red.

Open DevTools to check the elements. We can find the structure of Widget Tree container - > coloredbox - > limitedbox - > constrainedbox. Finally, RenderConstrainedBox will be created, and the width and height cover the whole screen (except AppBar).

Then we can't help asking why. I didn't set the width and height of the Container, so let's go back to the above source code again. If the Container doesn't set the child parameter and satisfies constraints = = null |! constraints. Istight will return a LimitedBox element with maxWidth of 0 and maxHeight of 0, and the child of the LimitedBox is a constraints parameter, const boxconstraints The element of ConstrainedBox in expand(), so the Container will fill the whole 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 2

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

Modify based on Scene 1. At this time, set the width of the Container to 100, the height to 100, and the color to colors red.

Similarly, open DevTools to check the elements. We can find the structure of Widget Tree: container - > constrainedbox - > colordedbox. Finally, it will be created_ RenderColoredBox is a square with width and height of 100 and color of red.

Through the source code analysis, we can conclude that if the width and height are set in the Container and the constraints attribute 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 finally a ConstrainedBox will be nested to return.

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 3

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

Next, we continue to add alignment based on scenario 2: alignment Center properties.

At this point, we will find why it is not centered? 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.

At this time, we will change the code and add a sub Widget to the current Container, and finally achieve the desired centering effect.

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,
      ),
    ),
  ),

Scenario 4

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

Because the body element in Scaffold will fill the whole screen (except AppBar), the body tells the Center to fill the whole screen, and then the Center tells the Container that it can be of any size, but the Container is set to width 200, so the size of the Container is width 200 and the height is infinite.

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

Scene 5

Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Center(
      child: Row(
        children: <Widget>[
          Container(
            color: Colors.red,
            child: Text(
              'I am a long, long, long text',
              style: TextStyle(
                fontSize: 30,
              ),
            ),
          ),
          Container(
            color: Colors.red,
            child: Text(
              'I am a very short text',
            ),
          ),
        ],
      ),
    ),
  ),

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

Scene 6

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 property value of the Container to BoxConstraints(minHeight:300, maxHeight:400, minWidth:300, maxWidth:400), and set the width to 200. Therefore, when initializing parameters of the constructor, constraints = BoxConstraints(minHeight:300, maxHeight:400, minWidth:300, maxWidth:300) will be set. In the Container build function, such a Widget Tree structure (Container - > constrainedbox - > coloredbox - > limitedbox - > constrainedbox) will be returned.

At this time, the Center tells the Container that it can be of any size, but the constraints set by the Container are the minimum width of 300, the maximum width of 300, that is, the width of 300, the minimum height of 300 and the maximum height of 400. Therefore, setting the width of 200 in the Container is ineffective. At this time, you may ask, what is the height? The answer is 400. Because child is not set in the Container and meets the condition of child = = null & & (constraints = = null |! Constraints. Istight), 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()),
    );
  }

last

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

More attention, please pay attention to our official account of "100 bottles technology". There are occasional benefits!

Keywords: Flutter

Added by w1ww on Fri, 24 Dec 2021 14:41:41 +0200