写点什么

不听话的 Container

作者:百瓶技术
  • 2022 年 1 月 10 日
  • 本文字数:6654 字

    阅读完需:约 22 分钟

不听话的 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.dartBoxConstraints.tight(Size size)    : minWidth = size.width,      maxWidth = size.width,      minHeight = size.height,      maxHeight = size.height;
复制代码


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


// flutter/lib/src/rendering/box.dartBoxConstraints.loose(Size size)    : minWidth = 0.0,      maxWidth = size.width,      minHeight = 0.0,      maxHeight = size.height;
复制代码

Container 部分源码

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


// flutter/lib/src/widgets/container.dartclass 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.dartconst 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.dartBoxConstraints 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.@overridebool get isTight => hasTightWidth && hasTightHeight;
复制代码


// flutter/lib/src/widgets/container.dartif (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 进行组合而来。

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

发布于: 刚刚阅读数: 2
用户头像

百瓶技术

关注

还未添加个人签名 2021.12.27 加入

还未添加个人简介

评论

发布
暂无评论
不听话的 Container