Skip to content

理解布局约束

我们会经常听到一些开发者在学习 Flutter 时的疑惑:为什么我设置了 width:100,但是看上去却不是 100 像素宽呢。
(注意,本文中的“像素”均指的是逻辑像素)通常你会回答,将这个 Widget 放进 Center 中,对吧?

别这么干!

如果你这样做了,他们会不断找你询问这样的问题:为什么 FittedBox 又不起作用了?
为什么 Column 又溢出边界,亦或是 IntrinsicWidth 应该做什么。

其实我们首先应该做的,是告诉他们 Flutter 的布局方式与 HTML 的布局差异相当大(这些开发者很可能是 Web 开发),然后要让他们熟记这条规则:

首先,上层 widget 向下层 widget 传递约束条件;
然后,下层 widget 向上层 widget 传递大小信息。
最后,上层 widget 决定下层 widget 的位置。

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

更多细节:

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

例如,如果一个 widget 中包含了一个具有 padding 的 Column,并且要对 Column 的子 widget 进行如下的布局:

那么谈判将会像这样:

Widget: “嘿!我的父级。我的约束是多少?”

Parent: “你的宽度必须在 80 到 300 像素之间,高度必须在 30 到 85 之间。”

Widget: “嗯…我想要 5 个像素的内边距,这样我的子级能最多拥有 290 个像素宽度和 75 个像素高度。”

Widget: “嘿,我的第一个子级,你的宽度必须要在 0 到 290,长度在 0 到 75 之间。”

First child: “OK,那我想要 290 像素的宽度,20 个像素的长度。”

Widget: “嗯…由于我想要将我的第二个子级放在第一个子级下面,所以我们仅剩 55 个像素的高度给第二个子级了。”

Widget: “嘿,我的第二个子级,你的宽度必须要在 0 到 290,长度在 0 到 55 之间。”

Second child: “OK,那我想要 140 像素的宽度,30 个像素的长度。”

Widget: “很好。我的第一个子级将被放在 x: 5 & y: 5 的位置,而我的第二个子级将在 x: 80 & y: 25 的位置。”

Widget: “嘿,我的父级,我决定我的大小为 300 像素宽度,60 像素高度。”

限制

正如上述所介绍的布局规则中所说的那样, Flutter 的布局引擎有一些重要限制:

  • 一个 widget 仅在其父级给其约束的情况下才能决定自身的大小。这意味着 widget 通常情况下 不能任意获得其想要的大小
  • 一个 widget 无法知道,也不需要决定其在屏幕中的位置。因为它的位置是由其父级决定的。
  • 当轮到父级决定其大小和位置的时候,同样的也取决于它自身的父级。所以,在不考虑整棵树的情况下,几乎不可能精确定义任何 widget 的大小和位置。
  • 如果子级想要拥有和父级不同的大小,然而父级没有足够的空间对其进行布局的话,子级的设置的大小可能会不生效。 这时请明确指定它的对齐方式

样例

下面的示例由 DartPad 提供,具有良好的交互体验。使用下面水平滚动条的编号切换 29 个不同的示例。

样例 1

1
Container(color: red)

整个屏幕作为 Container 的父级,并且强制 Container 变成和屏幕一样的大小。

所以这个 Container 充满了整个屏幕,并绘制成红色。

样例 2

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

红色的 Container 想要变成 100 x 100 的大小,但是它无法变成,因为屏幕强制它变成和屏幕一样的大小。

所以 Container 充满了整个屏幕。

样例 3

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

屏幕强制 Center 变得和屏幕一样大,所以 Center 充满了屏幕。

然后 Center 告诉 Container 可以变成任意大小,但是不能超出屏幕。现在,Container 可以真正变成 100 × 100 大小了。

样例 4

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

与上一个样例不同的是,我们使用了 Align 而不是 Center。

Align 同样也告诉 Container,你可以变成任意大小。
但是,如果还留有空白空间的话,它不会居中 Container。
相反,它将会在允许的空间内,把 Container 放在右下角(bottomRight)。

样例 5

1
2
3
4
Center(
  child: Container(
      width: double.infinity, height: double.infinity, color: red),
)

屏幕强制 Center 变得和屏幕一样大,所以 Center 充满屏幕。

然后 Center 告诉 Container 可以变成任意大小,但是不能超出屏幕。
现在,Container 想要无限的大小,但是由于它不能比屏幕更大,所以就仅充满屏幕。

样例 6

1
2
3
Center(
  child: Container(color: red),
)

屏幕强制 Center 变得和屏幕一样大,所以 Center 充满屏幕。

然后 Center 告诉 Container 可以变成任意大小,但是不能超出屏幕。
由于 Container 没有子级而且没有固定大小,所以它决定能有多大就有多大,所以它充满了整个屏幕。

但是,为什么 Container 做出了这个决定?非常简单,因为这个决定是由 Container widget 的创建者决定的。
可能会因创造者而异,而且你还得阅读 Container 文档 来理解不同场景下它的行为。

样例 7

1
2
3
4
5
6
Center(
  child: Container(
    color: red,
    child: Container(color: green, width: 30, height: 30),
  ),
)

屏幕强制 Center 变得和屏幕一样大,所以 Center 充满屏幕。

然后 Center 告诉红色的 Container 可以变成任意大小,但是不能超出屏幕。
由于 Container 没有固定大小但是有子级,所以它决定变成它 child 的大小。

然后红色的 Container 告诉它的 child 可以变成任意大小,但是不能超出屏幕。

而它的 child 是一个想要 30 × 30 大小绿色的 Container。
由于红色的 Container 和其子级一样大,所以也变为 30 × 30。
由于绿色的 Container 完全覆盖了红色 Container,所以你看不见它了。

样例 8

1
2
3
4
5
6
7
Center(
  child: Container(
    padding: const EdgeInsets.all(20.0),
    color: red,
    child: Container(color: green, width: 30, height: 30),
  ),
)

红色 Container 变为其子级的大小,但是它将其 padding 带入了约束的计算中。
所以它有一个 30 x 30 的外边距。由于这个外边距,所以现在你能看见红色了。
而绿色的 Container 则还是和之前一样。

样例 9

1
2
3
4
5
6
7
8
9
ConstrainedBox(
  constraints: const BoxConstraints(
    minWidth: 70,
    minHeight: 70,
    maxWidth: 150,
    maxHeight: 150,
  ),
  child: Container(color: red, width: 10, height: 10),
)

你可能会猜想 Container 的尺寸会在 70 到 150 像素之间,但并不是这样。
ConstrainedBox 仅对其从其父级接收到的约束下施加其他约束。

在这里,屏幕迫使 ConstrainedBox 与屏幕大小完全相同,因此它告诉其子 Widget 也以屏幕大小作为约束,从而忽略了其 constraints 参数带来的影响。

样例 10

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 10, height: 10),
  ),
)

现在,Center 允许 ConstrainedBox 达到屏幕可允许的任意大小。
ConstrainedBox 将 constraints 参数带来的约束附加到其子对象上。

Container 必须介于 70 到 150 像素之间。虽然它希望自己有 10 个像素大小,但最终获得了 70 个像素(最小为 70)。

样例 11

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 1000, height: 1000),
  ),
)

现在,Center 允许 ConstrainedBox 达到屏幕可允许的任意大小。
ConstrainedBox 将 constraints 参数带来的约束附加到其子对象上。

Container 必须介于 70 到 150 像素之间。
虽然它希望自己有 1000 个像素大小,但最终获得了 150 个像素(最大为 150)。

样例 12

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 100, height: 100),
  ),
)

现在,Center 允许 ConstrainedBox 达到屏幕可允许的任意大小。 ConstrainedBox 将 constraints 参数带来的约束附加到其子对象上。

Container 必须介于 70 到 150 像素之间。
虽然它希望自己有 100 个像素大小,因为 100 介于 70 至 150 的范围内,所以最终获得了 100 个像素。

样例 13

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

屏幕强制 UnconstrainedBox 变得和屏幕一样大,而 UnconstrainedBox 允许其子级的 Container 可以变为任意大小。

样例 14

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

屏幕强制 UnconstrainedBox 变得和屏幕一样大,而 UnconstrainedBox 允许其子级的 Container 可以变为任意大小。

不幸的是,在这种情况下,容器的宽度为 4000 像素,这实在是太大,以至于无法容纳在 UnconstrainedBox 中,因此 UnconstrainedBox 将显示溢出警告(overflow warning)。

样例 15

1
2
3
4
5
6
7
OverflowBox(
  minWidth: 0.0,
  minHeight: 0.0,
  maxWidth: double.infinity,
  maxHeight: double.infinity,
  child: Container(color: red, width: 4000, height: 50),
)

屏幕强制 OverflowBox 变得和屏幕一样大,并且 OverflowBox 允许其子容器设置为任意大小。

OverflowBox 与 UnconstrainedBox 类似,但不同的是,如果其子级超出该空间,它将不会显示任何警告。

在这种情况下,容器的宽度为 4000 像素,并且太大而无法容纳在 OverflowBox 中,但是 OverflowBox 会全部显示,而不会发出警告。

样例 16

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

这将不会渲染任何东西,而且你能在控制台看到错误信息。

UnconstrainedBox 让它的子级决定成为任何大小,但是其子级是一个具有无限大小的 Container。

Flutter 无法渲染无限大的东西,所以它抛出以下错误: BoxConstraints forces an infinite width.(盒子约束强制使用了无限的宽度)

样例 17

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
UnconstrainedBox(
  child: LimitedBox(
    maxWidth: 100,
    child: Container(
      color: Colors.red,
      width: double.infinity,
      height: 100,
    ),
  ),
)

这次你就不会遇到报错了。 UnconstrainedBox 给 LimitedBox 一个无限的大小;但它向其子级传递了最大为 100 的约束。

如果你将 UnconstrainedBox 替换为 Center,则LimitedBox 将不再应用其限制(因为其限制仅在获得无限约束时才适用),并且容器的宽度允许超过 100。

上面的样例解释了 LimitedBox 和 ConstrainedBox 之间的区别。

样例 18

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

屏幕强制 FittedBox 变得和屏幕一样大,而 Text 则是有一个自然宽度(也被称作 intrinsic 宽度),它取决于文本数量,字体大小等因素。

FittedBox 让 Text 可以变为任意大小。但是在 Text 告诉 FittedBox 其大小后, FittedBox 缩放文本直到填满所有可用宽度。

样例 19

1
2
3
4
5
const Center(
  child: FittedBox(
    child: Text('Some Example Text.'),
  ),
)

但如果你将 FittedBox 放进 Center widget 中会发生什么? Center 将会让 FittedBox 能够变为任意大小,取决于屏幕大小。

FittedBox 然后会根据 Text 调整自己的大小,然后让 Text 可以变为所需的任意大小,由于二者具有同一大小,因此不会发生缩放。

样例 20

1
2
3
4
5
6
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.'),
  ),
)

然而,如果 FittedBox 位于 Center 中,但 Text 太大而超出屏幕,会发生什么?

FittedBox 会尝试根据 Text 大小调整大小,但不能大于屏幕大小。然后假定屏幕大小,并调整 Text 的大小以使其也适合屏幕。

样例 21

1
2
3
4
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.'),
)

然而,如果你删除了 FittedBox,Text 则会从屏幕上获取其最大宽度,并在合适的地方换行。

样例 22

1
2
3
4
5
6
7
FittedBox(
  child: Container(
    height: 20.0,
    width: double.infinity,
    color: Colors.red,
  ),
)

FittedBox 只能在有限制的宽高中对子 widget 进行缩放(宽度和高度不会变得无限大)。
否则,它将无法渲染任何内容,并且你会在控制台中看到错误。

样例 23

1
2
3
4
5
6
Row(
  children: [
    Container(color: red, child: const Text('Hello!', style: big)),
    Container(color: green, child: const Text('Goodbye!', style: big)),
  ],
)

屏幕强制 Row 变得和屏幕一样大,所以 Row 充满屏幕。

和 UnconstrainedBox 一样, Row 也不会对其子代施加任何约束,而是让它们成为所需的任意大小。
Row 然后将它们并排放置,任何多余的空间都将保持空白。

样例 24

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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)),
  ],
)

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

样例 25

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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)),
  ],
)

当 Row 的子级被包裹在了 Expanded widget 之后, Row 将不会再让其决定自身的宽度了。

取而代之的是,Row 会根据所有 Expanded 的子级来计算其该有的宽度。

换句话说,一旦你使用 Expanded,子级自身的宽度就变得无关紧要,直接会被忽略掉。

样例 26

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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,
        ),
      ),
    ),
  ],
)

如果所有 Row 的子级都被包裹了 Expanded widget,每一个 Expanded 大小都会与其 flex 因子成比例,并且 Expanded widget 将会强制其子级具有与 Expanded 相同的宽度。

换句话说,Expanded 忽略了其子 Widget 想要的宽度。

样例 27

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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,
        ),
      ),
    ),
  ],
)

如果你使用 Flexible 而不是 Expanded 的话,唯一的区别是,Flexible 会让其子级具有与 Flexible 相同或者更小的宽度。
而 Expanded 将会强制其子级具有和 Expanded 相同的宽度。
但无论是 Expanded 还是 Flexible 在它们决定子级大小时都会忽略其宽度。

提示:
这意味着,Row 要么使用子级的宽度,要么使用Expanded 和 Flexible 从而忽略子级的宽度。

样例 28

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Scaffold(
  body: Container(
    color: blue,
    child: Column(
      children: const [
        Text('Hello!'),
        Text('Goodbye!'),
      ],
    ),
  ),
)

屏幕强制 Scaffold 变得和屏幕一样大,所以 Scaffold 充满屏幕。
然后 Scaffold 告诉 Container 可以变为任意大小,但不能超出屏幕。

提示:
当一个 widget 告诉其子级可以比自身更小的话,我们通常称这个 widget 对其子级使用 宽松约束(loose)。

样例 29

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Scaffold(
  body: SizedBox.expand(
    child: Container(
      color: blue,
      child: Column(
        children: const [
          Text('Hello!'),
          Text('Goodbye!'),
        ],
      ),
    ),
  ),
)

如果你想要 Scaffold 的子级变得和 Scaffold 本身一样大的话,你可以将这个子级外包裹一个 SizedBox.expand。

提示: 当一个 widget 告诉它的子级必须变成某个大小的时候,我们通常称这个 widget 对其子级使用 严格约束(tight)。

严格约束(Tight) vs 宽松约束(loose)

以后你经常会听到一些约束为严格约束或宽松约束,你花点时间来弄明白它们是值得的。

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

如果你到 Flutter 的 box.dart 文件中搜索 BoxConstraints 构造器,你会发现以下内容:

1
2
3
4
5
BoxConstraints.tight(Size size)
   : minWidth = size.width,
     maxWidth = size.width,
     minHeight = size.height,
     maxHeight = size.height;

如果你重新阅读 样例 2,它告诉我们屏幕强制 Container 变得和屏幕一样大。为何屏幕能够做到这一点,原因就是给 Container 传递了严格约束。

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

1
2
3
4
5
BoxConstraints.loose(Size size)
   : minWidth = 0.0,
     maxWidth = size.width,
     minHeight = 0.0,
     maxHeight = size.height;

如果你访问 样例 3,它将会告诉我们 Center 让红色的 Container 变得更小,但是不能超出屏幕。
Center 能够做到这一点的原因就在于给 Container 的是一个宽松约束。
总的来说,Center 起的作用就是从其父级(屏幕)那里获得的严格约束,为其子级(Container)转换为宽松约束。

了解如何为特定 widget 制定布局规则

掌握通用布局是非常重要的,但这还不够。

应用一般规则时,每个 widget 都具有很大的自由度,所以没有办法只看 widget 的名称就知道可能它长什么样。

如果你尝试推测,可能就会猜错。除非你已阅读 widget 的文档或研究了其源代码,否则你无法确切知道 widget 的行为。

布局源代码通常很复杂,因此阅读文档是更好的选择。但是当你在研究布局源代码时,可以使用 IDE 的导航功能轻松找到它。

下面是一个例子:

  • 在你的代码中找到一个 Column 并跟进到它的源代码。为此,请在 (Android Studio/IntelliJ) 中使用 command+B(macOS)或 control+B(Windows/Linux)。你将跳到 basic.dart 文件中。由于 Column 扩展了 Flex,请导航至 Flex 源代码(也位于 basic.dart 中)。
  • 向下滚动直到找到一个名为 createRenderObject() 的方法。如你所见,此方法返回一个 RenderFlex。它是 Column 的渲染对象,现在导航到 flex.dart 文件中的 RenderFlex 的源代码。
  • 向下滚动,直到找到 performLayout() 方法,由该方法执行列布局。