深入理解Flutter布局约束

1,904 阅读15分钟

本篇文章翻译自👉Understanding constraints,文章通过二十多个案例复原了Flutter的布局过程,希望可以帮助到大家。

作者: Marcelo Glasberg


有时学习Flutter的人会问一个问题,为什么有些widget设置了 width:100 ,但是并没有显示100的像素宽度。这时有人会回答:把那个widget放到 Center 组件中。对吧。

不要这么回答

如果这么回答了,他们还会一遍一遍的在问:为什么一些 FittedBox 没有起作用,为什么有的 Column 超出范围了,为什么 IntrinsicWidth 应该怎么做。

相反,首先告诉他们 Flutter 的布局和 HTML 布局是非常不同的,然后让他们记住下面的布局规则:

向下👇传递约束。向上👆传递尺寸。父节点设置位置

不理解上面的规则的话。Flutter布局就不能被真正的理解,所以开发者应该尽早理解这个规则。 更多的细节:

  • Widget 从它的父节点获得自己的  constraints(约束)constraint(约束) 由四个double类型的值设置,分别是:最小和最大的宽大,最小和最大的高度。

  • 然后 Widget 遍历他自己的所有子节点,一个一个的告诉子节点的 constraints(约束) ,每一个子节点的约束都不同,并且问每一个子节点想要的尺寸是多少。

  • 再次,Widget 会一个一个的摆放他的 子节点,水平使用 x 轴,垂直使用 y 轴。

  • 最后,Widget会告诉它的父节点它自己的 size(尺寸) ,这个尺寸会在第一步的约束范围之内。

我们举个例子🌰, 一个组合型的Widget包含一个带有Padding间距的Column,并且想要按照下图摆放它的两个子节点:

layout.png

布局的过程就像下面的对话:

Widget: “Hey 父节点, 我的约束是什么”

父节点: “你的宽度必须在 80 到 300 像素范围内,你的高度必须在  80 到 300 范围内”

Widget: “Hmmm, 因为我想要有一个 5 像素的间距,那么我的子节点们最多宽 290 像素、高 75 像素”

Widget: “Hey 第一个子节点,你的宽度必须在0 到 290 像素内, 高度必须在 0 到 75 像素内”

First child: “OK, 我希望我的宽度是 290 像素,高度是 20 像素”

Widget: “Hmmm,因为我想把第二个子节点放在第一个子节点的下面,这样的话就只给第二个子节点留下了 55 像素的高度”

Widget: “Hey 第二个子节点, 你的宽度必须在 0 到 290 像素内, 高度必须在 0 到 55 像素内”

Second child: “OK, 我希望我的宽度是 140 像素, 高度是 30 像素”

Widget: “很棒. 第一个子节点的位置是x: 5 、 y: 5, 第二个子节点的位置是 x: 80 、 y: 25

Widget: “Hey 父节点, 我已经决定了我的尺寸,宽度是 300 像素, 高度是 60 像素”

限制

由于上面提到的布局规则,Flutter布局引擎有几个重要的限制:

  • 一个 Widget 只能在父节点给定的约束内决定字节的尺寸信息,这就意味着Widget通常不能有自己想要的尺寸信息

  • 由于父节点决定了子节点的位置,所以一个 Widget 不知道也不能决定字节在屏幕上的位置

  • 由于父节点的尺寸和位置信息也依赖它的父节点,所以如果不把组件树作为一个整体来考虑,就不可能精确地定义任何组件的大小和位置信息

  • If a child wants a different size from its parent and the parent doesn’t have enough information to align it, then the child’s size might be ignored. Be specific when defining alignment.

如果子节点想要的尺寸与父节点的给定的范围不同,而父节点又没有足够的信息去对齐摆放子节点,那么子节点的尺寸诉求就被忽略了。具体的尺寸信息要定义对齐信息

案例

DartPad 可以使用来运行案例代码。一共有29个案例,每一个案例都有编号。 👉案例代码

Example 1

Container(color: red)

layout-1.png

屏幕是 Container 的父节点,屏幕强制 Container 的大小和screen相同。

因此, Container 使用红色充满了整个屏幕。

Example 2

Container(width: 100, height: 100, color: red)

layout-2.png

Container 想要 100 × 100,但是没有按着这个想法。因为,屏幕强制 Container 的大小和screen相同。 因此, Container 仍然使用红色充满了整个屏幕。

Example 3

Center(
  child: Container(width: 100, height: 100, color: red),
)

layout-3.png

屏幕强制 Center 的尺寸是屏幕的尺寸,所以 Center 充满了整个屏幕。

Center 告诉 Container ,在屏幕范围内, Container 可以有任何它想要的尺寸。

所以,Container 可以做到了 100 × 100。

Example 4

Align(
  alignment: Alignment.bottomRight,
  child: Container(width: 100, height: 100, color: red),
)

Example 4 layout

和前一个案例不同,这里使用 Align 代替了 Center组件。

和前一个案例差不多,Align 也告诉 Container 在屏幕范围内,它可以是任何尺寸。相对于居中,在可用的范围内,Align 右下角对齐 Container 。

Example 5

Center(
  child: Container(
      width: double.infinity, height: double.infinity, color: red),
)

Example 5 layout

屏幕强制 Center 的尺寸是屏幕的尺寸,所以 Center 充满了整个屏幕。

Center 告诉 Container ,在屏幕范围内, Container 可以有任何它想要的尺寸。

Container 想要无限的尺寸信息,但是由于它不能超过屏幕。

所以:Container 仅仅填充了整个屏幕。

Example 6

Center(
  child: Container(color: red),
)

Example 6 layout

屏幕强制 Center 的尺寸是屏幕的尺寸,所以 Center 充满了整个屏幕。

Center 告诉 Container ,在屏幕范围内, Container 可以有任何它想要的尺寸。由于Container 没有子节点,也没有固定的大小,Container 决定它想要尽可能大,所以它充满了整个屏幕。

但是为什么 Container 决定它尽可能大呢?因为这是组件设计者的想法💡。可以参考👉Container 文档 

Example 7

Center(
  child: Container(
    color: red,
    child: Container(color: green, width: 30, height: 30),
  ),
)

Example 7 layout

屏幕强制 Center 的尺寸是屏幕的尺寸,所以 Center 充满了整个屏幕。

Center 告诉 Container ,在屏幕范围内,Container 可以有任何它想要的尺寸。因为红色的Container 没有尺寸信息,但是有一个子节点。Container 决定它想要的尺寸和child一样。

红色的 Container 告诉它的子节点,在在屏幕范围内,它可以有任何它想要的尺寸。

绿色的 Container 想要的尺寸是 30 × 30。假设红色的 Container 将自身大小调整为其子元素的大小,所以它也是 30 × 30。 因为红色被绿色完全覆盖,红色的 Container 不可见。

Example 8

Center(
  child: Container(
    padding: const EdgeInsets.all(20.0),
    color: red,
    child: Container(color: green, width: 30, height: 30),
  ),
)

Example 8 layout

红色 Container 将自己的大小调整为子节点的大小,但它会考虑自己的间距。所以就是 30 × 30 加上四周的20间距,正是由于间距的存在,所以红色是可见的。

和前一个例子相似,绿色 Container 依然是 30 × 30。

Example 9

ConstrainedBox(
  constraints: const BoxConstraints(
    minWidth: 70,
    minHeight: 70,
    maxWidth: 150,
    maxHeight: 150,
  ),
  child: Container(color: red, width: 10, height: 10),
)

Example 9 layout

你也许会认为 Container 的范围是 70 到 150 像素,但是这是不对的。 ConstrainedBox 仅仅是对父布局传递的约束强加了额外的约束

在这里,屏幕强制 ConstrainedBox 的尺寸是精确的屏幕尺寸,所以它告诉它的 Container 也是屏幕的尺寸,因此无视了 constraints 参数。

Example 10

Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 10, height: 10),
  ),
)

Example 10 layout

现在,Center 让 ConstrainedBox 的尺寸是屏幕尺寸范围内。ConstrainedBox 强加了 额外 的约束给他的子节点。

Container 必须在70 到 150 像素范围内,它想要10像素,所以它只能是70像素。

Example 11

Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 1000, height: 1000),
  ),
)

Example 11 layout

同样,Center 让 ConstrainedBox 的尺寸是屏幕尺寸范围内任意大小。ConstrainedBox 强加了额外的constraints 约束给它的子节点。

Container 必须在70到150的像素范围内,而它想要1000像素,因此它的尺寸就是150.

Example 12

Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 100, height: 100),
  ),
)

Example 12 layout

同样,Center 让 ConstrainedBox 的尺寸是屏幕尺寸范围内任意大小。 ConstrainedBox 强加了额外的constraints 约束给它的子节点。

Container 必须在70到150的像素范围内,而它想要的100正好在范围内,所以它的尺寸就是100。

Example 13

UnconstrainedBox(
  child: Container(color: red, width: 20, height: 50),
)

Example 13 layout

屏幕强制 UnconstrainedBox 的尺寸就是屏幕尺寸,但是 UnconstrainedBox 可以让它的子节点 Container 是任何子节点想要的尺寸。

所以就是20*50了。

Example 14

UnconstrainedBox(
  child: Container(color: red, width: 4000, height: 50),
)

Example 14 layout

屏幕强制 UnconstrainedBox 的尺寸就是屏幕尺寸,并且UnconstrainedBox 可以让它的子节点 Container 的尺寸是任何子节点想要的尺寸。

但是在这里例子中,UnconstrainedBox的子节点Container 想要的4000像素太大了,因此 UnconstrainedBox 展示了 “overflow warning” 警告。

Example 15

OverflowBox(
  minWidth: 0.0,
  minHeight: 0.0,
  maxWidth: double.infinity,
  maxHeight: double.infinity,
  child: Container(color: red, width: 4000, height: 50),
)

Example 15 layout

屏幕强制 OverflowBox 的尺寸就是屏幕尺寸,而且OverflowBox 可以让它的子节点 Container 的尺寸是任何子节点想要的尺寸。

OverflowBox 和 UnconstrainedBox 十分相似,不同的是当子节点过大的时候, OverflowBox 不会显示上面的警告。

在这个例子里,Container 的宽度是4000像素,已经超过了 OverflowBox 的范围,但是 OverflowBox 会尽可能展示内容,不会显示警告。

Example 16

UnconstrainedBox(
  child: Container(color: Colors.red, width: double.infinity, height: 100),
)

Example 16 layout

这里没有渲染任何组件,并且你在控制台也看到了错误信息。

UnconstrainedBox 让它的子节点 Container 的尺寸是任何子节点想要的尺寸。但是 Container 的尺寸是无限的。

Flutter 不会渲染无限的尺寸,因此它抛出了带有 BoxConstraints forces an infinite width. 信息的异常

Example 17

UnconstrainedBox(
  child: LimitedBox(
    maxWidth: 100,
    child: Container(
      color: Colors.red,
      width: double.infinity,
      height: 100,
    ),
  ),
)

Example 17 layout

这里你就不会看到错误信息了,因为在 UnconstrainedBox 中给定了 LimitedBox 组件。它会传递一个最大宽度——100像素给子节点。

如果将 UnconstrainedBox 替换为 Center 组件,LimitedBox 就不会再应用 limit 信息,因为只有约束是无限的时候,才会应用 limit 信息,并且Container 的宽度可以超过给定的100像素的限制。

这就解释了 LimitedBox 和 ConstrainedBox 的不同。

Example 18

const FittedBox(
  child: Text('Some Example Text.'),
)

Example 18 layout

屏幕强制 FittedBox 的尺寸就是屏幕尺寸。Text 也会有一些自然宽度或者叫做固有宽度,这个宽度取决于文本的多少、字体的大小等等。

FittedBoxText 的尺寸是任何它想要的尺寸,但是 Text 把自己的尺寸告诉 FittedBox 之后,FittedBox 会缩放 Text ,沾满整个可用的宽度。

Example 19

const Center(
  child: FittedBox(
    child: Text('Some Example Text.'),
  ),
)

Example 19 layout

当你把 FittedBox 放置在 Center 中会发生什么呢?Center 让 FittedBox 的尺寸是屏幕范围内的任何尺寸。

FittedBox 确定自己的大小为 Text 的大小,并且让 Text 的尺寸是 Text 想要尺寸。 因为 FittedBoxText 的尺寸相同,所以就不会发生缩放了。

Example 20

const Center(
  child: FittedBox(
    child: Text(
        'This is some very very very large text that is too big to fit a regular screen in a single line.'),
  ),
)

Example 20 layout

但是,假如  Center 组件内部的 FittedBox 子节点 Text 比屏幕还要宽,会怎么样呢?

FittedBox 确定自己的大小为 Text 的大小,但是它的大小超过了屏幕。然后它将大小设定为屏幕大小,并调整 Text 的大小,也让 Text 为屏幕宽度。

Example 21

const Center(
  child: Text(
      'This is some very very very large text that is too big to fit a regular screen in a single line.'),
)

Example 21 layout

但是,如果你移除了 FittedBox, Text 组件的宽度就是屏幕宽度,文本组件会换行,所以也会适应屏幕尺寸,不会发生报错。

Example 22

Example 22 layout

FittedBox(
  child: Container(
    height: 20.0,
    width: double.infinity,
    color: Colors.red,
  ),
)

FittedBox 仅仅可以缩放非无限范围的组件,否则,它不会渲染任何东西,并且你在控制台中还会看到错误信息。

Example 23

Row(
  children: [
    Container(color: red, child: const Text('Hello!', style: big)),
    Container(color: green, child: const Text('Goodbye!', style: big)),
  ],
)

Example 23 layout

屏幕强制 Row 组件的尺寸是屏幕的尺寸。

就像 UnconstrainedBox 一样, Row 不会添加约束给他的子节点,而是让子节点的尺寸就是他们想要的尺寸。然后 Row 组件会并排摆放子节点,剩余的空间就是空的。

Example 24

Row(
  children: [
    Container(
      color: red,
      child: const Text(
        'This is a very long text that '
        'won't fit the line.',
        style: big,
      ),
    ),
    Container(color: green, child: const Text('Goodbye!', style: big)),
  ],
)

Example 24 layout

因为 Row 不会强加任何约束给他的子节点,所以子节点可能非常大,导致无法适应 Row 的宽度。 在这个例子🌰中,就像 UnconstrainedBox , Row 出现了 “overflow warning”警告。

Example 25

Row(
  children: [
    Expanded(
      child: Center(
        child: Container(
          color: red,
          child: const Text(
            'This is a very long text that won't fit the line.',
            style: big,
          ),
        ),
      ),
    ),
    Container(color: green, child: const Text('Goodbye!', style: big)),
  ],
)

Example 25 layout

When a Row’s child is wrapped in an Expanded widget, the Row won’t let this child define its own width anymore. 假如 Row 的子节点被 Expanded 组件包裹,那么 Row 就不会让子节点定义自己的尺寸了。

相反,Row 会根据其他的子节点来定义 Expanded 的宽度,只有这样 Expanded 才会强制被包裹的子节点的宽度和 Expanded’s 宽度一样。也就是说,只要使用了 Expanded,被包裹的子节点的原始宽度是无关紧要的了。

Example 26

Row(
  children: [
    Expanded(
      child: Container(
        color: red,
        child: const Text(
          'This is a very long text that won't fit the line.',
          style: big,
        ),
      ),
    ),
    Expanded(
      child: Container(
        color: green,
        child: const Text(
          'Goodbye!',
          style: big,
        ),
      ),
    ),
  ],
)

Example 26 layout

如果 Row 的子节点都被 Expanded 包裹,每个 Expanded 的尺寸和它的 'flex' 参数相关。只有这样 Expanded 才会强制被包裹的子节点的宽度和 Expanded’s 宽度一样,也就是说,Expanded 会无视子节点原始的宽度。

Example 27

Row(
  children: [
    Flexible(
      child: Container(
        color: red,
        child: const Text(
          'This is a very long text that won't fit the line.',
          style: big,
        ),
      ),
    ),
    Flexible(
      child: Container(
        color: green,
        child: const Text(
          'Goodbye!',
          style: big,
        ),
      ),
    ),
  ],
)

Example 27 layout

Flexible 和 Expanded 的不同点是,Flexible 的宽度会大于等于被包裹的子节点的宽度,而 Expanded 会强制子节点的宽度就是Expanded 的宽度。相同点是,两者都会忽略子节点的宽度。

Example 28

Scaffold(
  body: Container(
    color: blue,
    child: Column(
      children: const [
        Text('Hello!'),
        Text('Goodbye!'),
      ],
    ),
  ),
)

Example 28 layout

屏幕强制 Scaffold 的尺寸就是屏幕尺寸。因此,Scaffold 占满了整个屏幕。Scaffold 告诉 Container 它可以是屏幕范围内的任意尺寸。

When a widget tells its child that it can be smaller than a certain size, we say the widget supplies loose constraints to its child. More on that later.

当一个 Widget 告诉它的子节点,它可以小于某个特定的尺寸时。我们就说这个 Widget 为子节点提供了 loose 的约束。

Example 29

Scaffold(
  body: SizedBox.expand(
    child: Container(
      color: blue,
      child: Column(
        children: const [
          Text('Hello!'),
          Text('Goodbye!'),
        ],
      ),
    ),
  ),
)

Example 29 layout

如果你想让 Scaffold 的子节点的大小和 Scaffold 本身一样,你可以使用 SizedBox.expand 包裹它的子节点。

当一个 Widget 告诉它的子节点,子节点必须是准确的尺寸。我们就说这个 Widget 给它的子节点了一个 tight 的约束。

Tight vs. loose 约束

我们经常听到 tight 或者 loose 约束,那么这两个是什么意思呢?

tight约束提供了一种可能性:精确的尺寸。换句话说,在tight约束下,宽度和高度的最大值等于其最小值。在 Flutter 的源码中,可以找到 BoxConstraints 的一个构造方法:

BoxConstraints.tight(Size size)
   : minWidth = size.width,
     maxWidth = size.width,
     minHeight = size.height,
     maxHeight = size.height;

再看一下上面的Example 2,我们提到屏幕强制红色 Container 的大小是屏幕大小,就是通过tight约束做到的,屏幕给 Container 的约束是最大值和最小值都是屏幕尺寸的 tight 约束。

另一方面,loose 约束,只是设定了最大的宽度和高度, 子节点只要比最大值就可以了。也就是说,loose 约束的宽度和高度的最小值是 zero:

BoxConstraints.loose(Size size)
   : minWidth = 0.0,
     maxWidth = size.width,
     minHeight = 0.0,
     maxHeight = size.height;

在看一下上面的Example 3Center 让红色的 Container不大于屏幕尺寸,能做到这一点就是,Center 传递了loose 约束给 Container。最终,Center 从父节点获得了 tight 约束,将其转为 loose 约束,并将转化后的约束传递给了子节点。

了解特定 Widget 的布局规则

知道通用的布局规则是十分有必要的,但是止步于此,是远远不够的。

在运用通用规则的时候,每个 Widget 有很高的自由度。因此仅仅通过 Widget 的名字是无法知道它要干啥的。

如果仅仅是猜测,那么很可能会猜错。只有翻读源码和文档,才能准确的知道 Widget 背后的规则。

布局的源码一般是比较复杂的,因此读文档更好接受一点点🤏。但是如果你决定学习布局的源码,你可以通过IDE的导航功能来定位源码。

下面是例子:

  • 找到 Column 的远嘛. To do this, 在Android Studio 或者 IntelliJ 中使用 command+B (macOS) 或者 control+B (Windows/Linux) 快捷键。你就能达到 basic.dart 文件,因为 Column 继承自 Flex ,继续跟踪到 Flex 源码(也在 basic.dart 中)。

  • 找到 createRenderObject() 方法。如你所见,方法返回了 RenderFlex。这就是 Column背后的render-object。现在跟踪到 flex.dart 文件的 RenderFlex 源码中。

  • 找到 performLayout() 方法。这个方法就是 Column 的布局规则方法。

A goodbye layout