3

不听话的 Container

 2 years ago
source link: https://segmentfault.com/a/1190000041172745
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

不听话的 Container

在阅读本文之前我们先来回顾下在 Flutter 开发过程中,是不是经常会遇到以下问题:

  • Container 设置了宽高无效
  • Column 溢出边界,Row 溢出边界
  • 什么时候该使用 ConstrainedBox 和 UnconstrainedBox

每当遇到这种问题,我总是不断地尝试,费了九牛二虎之力,Widget 终于乖乖就范(达到理想效果)。痛定思过,我终于开始反抗(起来,不愿做奴隶的人们,国歌唱起来~),为什么 Container 设置宽高又无效了?Column 为什么又溢出边界了?怀揣着满腔热血,我终于鼓起勇气首先从 Container 源码入手,逐一揭开它的神秘面纱。

在讲本文之前,我们首先应该了解 Flutter 布局中的以下规则:

  • 首先,上层 Widget 向下层 Widget 传递约束条件
  • 其次,下层 Widget 向上层 Widget 传递大小信息
  • 最后,上层 Widget 决定下层 Widget 的位置

如果我们在开发时无法熟练运用这些规则,在布局时就不能完全理解其原理,所以越早掌握这些规则越好。

  • Widget 会通过它的父级获得自身约束。约束实际上就是 4 个浮点类型的集合:最大/最小宽度,以及最大/最小高度。
  • 然后这个 Widget 将会逐个遍历它的 children 列表,向子级传递约束(子级之间的约束可能会有不同),然后询问它的每一个子级需要用于布局的大小。
  • 然后这个 Widget 将会对它子级 children 逐个进行布局。
  • 最后,Widget 将会把它的大小信息向上传递至父 Widget(包括其原始约束条件)。

严格约束(Tight)vs. 宽松约束(Loose)

严格约束就是获得确切大小的选择,换句话来说,它的最大/最小宽度是一致的,高度也是一样。

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

宽松约束就是设置了最大宽度/高度,但是允许其子 Widget 获得比它更小的任意大小,换句话说就是宽松约束的最小宽度/高度为 0。

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

Container 部分源码

首先奉上 Container 部分源码,下面我们会结合具体场景对源码进行逐一分析。

// 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;
  }
}
Scaffold(
  appBar: AppBar(
    title: Text('Flutter Container'),
  ),
  body: Container(
    color: Colors.red,
  ),
),

在 Scaffold body 中单独使用 Container,并且 Container 设置 color 为 Colors.red。

打开 DevTools 进行元素检查我们可以发现 Widget Tree 的结构 Container -> ColoredBox -> LimitedBox -> ConstrainedBox,最后会创建 RenderConstrainedBox,宽度和高度撑满整个屏幕(除了 AppBar)。

那我们不禁会问,为什么会这样,我并没有设置 Container 的宽度和高度,那么我们再次回到上面的源码,如果 Container 没有设置 child 参数并且满足 constraints == null || !constraints.isTight 会返回一个 maxWidth 为 0,maxHeight 为 0 的 LimitedBox 的元素,并且 LimitedBox 的 child 是一个 constraints 参数为 const BoxConstraints.expand() 的 ConstrainedBox 的元素,所以 Container 会撑满整个屏幕(除了 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;
Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
  ),

在场景一的基础上进行修改,此时给 Container 设置 width 为 100,height 为 100,color 为 Colors.red。

同样打开 DevTools 进行元素检查我们可以发现 Widget Tree 的结构 Container -> ConstrainedBox -> ColorededBox,最后会创建 _RenderColoredBox,宽度和高度均为 100,颜色为红色的正方形。

通过源码分析我们可以得出,如果 Container 中设置了 width、height 并且没有设置 constraints 属性,首先会在构造函数中对 constraints 进行赋值,所以 constraints = BoxConstraints.tightFor(width:100, height:100),然后会在外层嵌套一个 ColoredBox,最后再嵌套一个 ConstrainedBox 返回。

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);
Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Container(
      width: 100,
      height: 100,
      color: Colors.red,
      alignment: Alignment.center,
    ),
  ),

接下来,我们在场景二的基础上继续添加 alignment:Alignment.center 属性。

此时我们会发现为什么没有居中显示呢?通过查看 Align 源码不难发现,它是设置子 Widget 与自身的对齐方式。

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

那么此时我们再来改变代码,给当前 Container 添加子 Widget,终于达到了我们想要的居中效果。

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,
      ),
    ),
  ),
Scaffold(
    appBar: AppBar(
      title: Text('Flutter Container'),
    ),
    body: Center(
      child: Container(
        color: Colors.red,
        width: 200,
      ),
    ),
  ),

由于 Scaffold 中的 body 元素会撑满整个屏幕(除了 AppBar),body 告诉 Center 占满整个屏幕,然后 Center 告诉 Container 可以变成任意大小,但是 Container 设置 width 为 200,所以 Container 的大小为宽度 200, 高度无限大。

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

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(
              '我是一段很短的文字',
            ),
          ),
        ],
      ),
    ),
  ),

由于 Row 不会对其子元素施加任何约束,因此它的 children 很有可能太大而超出 Row 的宽度,在这种情况下,Row 就会显示出溢出警告了。

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

这里我们设置了 Container 的 constraints 属性值为 BoxConstraints(minHeight:300, maxHeight:400, minWidth:300, maxWidth:400), 并且设置了 width 为 200。所以在构造函数初始化参数时,会进行设置 constraints = BoxConstraints(minHeight:300, maxHeight:400, minWidth:300, maxWidth:300) , 在 Container build 函数中会返回一个这样的 Widget Tree 的结构(Container -> ConstrainedBox -> ColoredBox -> LimitedBox -> ConstrainedBox)。

此时 Center 告诉 Container 可以变成任意大小,但是 Container 设置 constraints 约束条件为宽度最小为 300,最大为 300,也就是宽度为 300, 最小高度为 300, 最大高度为 400,所以在 Container 中设置的 width 为 200 也就无效了,这个时候你也许会问,那高度到底是多少?答案是 400,因为 Container 中没有设置 child ,满足 child == null && (constraints == null || !constraints.isTight) 条件,所以会嵌套一个 ConstrainedBox(constraints: const BoxConstraints.expand() 所以高度会为最大高度 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()),
    );
  }

通过以上源码分析以及不同的场景,我们不难发现 Container 主要就是通过设置不同的参数,然后使用 LimitedBox、ConstrainedBox、Align、Padding、ColoredBox、DecoratedBox、Transform、ClipPath 等 Widget 进行组合而来。

更多精彩请关注我们的公众号「百瓶技术」,有不定期福利呦!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK