【译】Flutter | 深入理解布局约束
原文:Flutter: The Advanced Layout Rule Even Beginners Must Know
译者:Vadaski
校对:Luke Cheng、Alex

前言
这篇文章最初来自于 Marcelo Glasberg 在 Medium 发表的 Flutter: The Advanced Layout Rule Even Beginners Must Know。后被 Flutter Team 发现并收录到 flutter.dev。
在认真阅读完这篇文章后,我认为它对 Flutter 开发者来说具有相当的 指导意义,每一位 Flutter 开发都应该认真理解其中的布局约束过程,是非常必要的!因此,在翻译本文的过程中,我们对译文反复打磨,尽可能保留原文向传递给读者的内容。希望让每一位看到此文的开发者都能够有所收获。
深入理解布局约束
我们会经常听到一些开发者在学习 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 个不同的示例。
你可以在 flutter.cn 上找到该源码。
如果你愿意的话,你还可以在 这个 Github 仓库中 获取其代码。
以下各节将介绍这些示例。
样例 1

Container(color: Colors.red)
整个屏幕作为 Container
的父级,并且强制 Container
变成和屏幕一样的大小。
所以这个 Container
充满了整个屏幕,并绘制成红色。
样例 2

Container(width: 100, height: 100, color: Colors.red)
红色的 Container
想要变成 100 x 100 的大小,
但是它无法变成,因为屏幕强制它变成和屏幕一样的大小。
所以 Container
充满了整个屏幕。
样例 3

Center(
child: Container(width: 100, height: 100, color: Colors.red)
)
屏幕强制 Center
变得和屏幕一样大,所以 Center
充满了屏幕。
然后 Center
告诉 Container
可以变成任意大小,但是不能超出屏幕。
现在,Container
可以真正变成 100 × 100 大小了。
样例 4

Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: Colors.red),
)
与上一个样例不同的是,我们使用了 Align
而不是 Center
。
Align
同样也告诉 Container
,你可以变成任意大小。
但是,如果还留有空白空间的话,它不会居中 Container
。
相反,它将会在允许的空间内,把 Container
放在右下角(bottomRight)。
样例 5

Center(
child: Container(
color: Colors.red,
width: double.infinity,
height: double.infinity,
)
)
屏幕强制 Center
变得和屏幕一样大,所以 Center
充满屏幕。
然后 Center
告诉 Container
可以变成任意大小,但是不能超出屏幕。
现在,Container
想要无限的大小,但是由于它不能比屏幕更大,
所以就仅充满屏幕。
样例 6

Center(child: Container(color: Colors.red))
屏幕强制 Center
变得和屏幕一样大,所以 Center
充满屏幕。
然后 Center
告诉 Container
可以变成任意大小,但是不能超出屏幕。
由于 Container
没有子级而且没有固定大小,所以它决定能有多大就有多大,
所以它充满了整个屏幕。
但是,为什么 Container
做出了这个决定?
非常简单,因为这个决定是由 Container
widget 的创建者决定的。
可能会因创造者而异,而且你还得阅读
Container
文档
来理解不同场景下它的行为。
样例 7

Center(
child: Container(
color: Colors.red,
child: Container(color: Colors.green, width: 30, height: 30),
)
)
屏幕强制 Center
变得和屏幕一样大,所以 Center
充满屏幕。
然后 Center
告诉红色的 Container
可以变成任意大小,但是不能超出屏幕。
由于 Container
没有固定大小但是有子级,所以它决定变成它 child 的大小。
然后红色的 Container
告诉它的 child 可以变成任意大小,但是不能超出屏幕。
而它的 child 是一个想要 30 × 30 大小绿色的 Container
。由于红色的 Container
和其子级一样大,所以也变为 30 × 30。由于绿色的 Container
完全覆盖了红色 Container
,
所以你看不见它了。
样例 8

Center(
child: Container(
color: Colors.red,
padding: const EdgeInsets.all(20.0),
child: Container(color: Colors.green, width: 30, height: 30),
)
)
红色 Container
变为其子级的大小,但是它将其 padding 带入了约束的计算中。
所以它有一个 30 x 30 的外边距。由于这个外边距,所以现在你能看见红色了。
而绿色的 Container
则还是和之前一样。
样例 9

ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 10, height: 10),
)
你可能会猜想 Container
的尺寸会在 70 到 150 像素之间,但并不是这样。
ConstrainedBox
仅对其从其父级接收到的约束下施加其他约束。
在这里,屏幕迫使 ConstrainedBox
与屏幕大小完全相同,
因此它告诉其子 Widget
也以屏幕大小作为约束,
从而忽略了其 constraints
参数带来的影响。
样例 10

Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 10, height: 10),
)
)
现在,Center
允许 ConstrainedBox
达到屏幕可允许的任意大小。
ConstrainedBox
将 constraints
参数带来的约束附加到其子对象上。
Container 必须介于 70 到 150 像素之间。虽然它希望自己有 10 个像素大小, 但最终获得了 70 个像素(最小为 70)。
样例 11

Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 1000, height: 1000),
)
)
现在,Center
允许 ConstrainedBox
达到屏幕可允许的任意大小。
ConstrainedBox
将 constraints
参数带来的约束附加到其子对象上。
Container
必须介于 70 到 150 像素之间。
虽然它希望自己有 1000 个像素大小,
但最终获得了 150 个像素(最大为 150)。
样例 12

Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 100, height: 100),
)
)
现在,Center
允许 ConstrainedBox
达到屏幕可允许的任意大小。
ConstrainedBox
将 constraints
参数带来的约束附加到其子对象上。
Container
必须介于 70 到 150 像素之间。
虽然它希望自己有 100 个像素大小,
因为 100 介于 70 至 150 的范围内,所以最终获得了 100 个像素。
样例 13

UnconstrainedBox(
child: Container(color: Colors.red, width: 20, height: 50),
)
屏幕强制 UnconstrainedBox
变得和屏幕一样大,而 UnconstrainedBox
允许其子级的 Container
可以变为任意大小。
样例 14

UnconstrainedBox(
child: Container(color: Colors.red, width: 4000, height: 50),
)
屏幕强制 UnconstrainedBox
变得和屏幕一样大,
而 UnconstrainedBox
允许其子级的 Container
可以变为任意大小。
不幸的是,在这种情况下,容器的宽度为 4000 像素,
这实在是太大,以至于无法容纳在 UnconstrainedBox
中,
因此 UnconstrainedBox
将显示溢出警告(overflow warning)。
样例 15

OverflowBox(
minWidth: 0.0,
minHeight: 0.0,
maxWidth: double.infinity,
maxHeight: double.infinity,
child: Container(color: Colors.red, width: 4000, height: 50),
);
屏幕强制 OverflowBox
变得和屏幕一样大,
并且 OverflowBox
允许其子容器设置为任意大小。
OverflowBox
与 UnconstrainedBox
类似,但不同的是,
如果其子级超出该空间,它将不会显示任何警告。
在这种情况下,容器的宽度为 4000 像素,并且太大而无法容纳在 OverflowBox
中,
但是 OverflowBox
会全部显示,而不会发出警告。
样例 16

UnconstrainedBox(
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
这将不会渲染任何东西,而且你能在控制台看到错误信息。
UnconstrainedBox
让它的子级决定成为任何大小,
但是其子级是一个具有无限大小的 Container
。
Flutter 无法渲染无限大的东西,所以它抛出以下错误:
BoxConstraints forces an infinite width.
(盒子约束强制使用了无限的宽度)
样例 17

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

FittedBox(
child: Text('Some Example Text.'),
)
屏幕强制 FittedBox
变得和屏幕一样大,
而 Text
则是有一个自然宽度(也被称作 intrinsic 宽度),
它取决于文本数量,字体大小等因素。
FittedBox
让 Text
可以变为任意大小。
但是在 Text
告诉 FittedBox
其大小后,
FittedBox
缩放文本直到填满所有可用宽度。
样例 19

Center(
child: FittedBox(
child: Text('Some Example Text.'),
)
)
但如果你将 FittedBox
放进 Center
widget 中会发生什么?
Center
将会让 FittedBox
能够变为任意大小,
取决于屏幕大小。
FittedBox
然后会根据 Text
调整自己的大小,
然后让 Text
可以变为所需的任意大小,
由于二者具有同一大小,因此不会发生缩放。
样例 20

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

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

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

Row(
children:[
Container(color: Colors.red, child: Text('Hello!')),
Container(color: Colors.green, child: Text('Goodbye!')),
]
)
屏幕强制 Row
变得和屏幕一样大,所以 Row
充满屏幕。
和 UnconstrainedBox
一样,
Row
也不会对其子代施加任何约束,
而是让它们成为所需的任意大小。
Row
然后将它们并排放置,
任何多余的空间都将保持空白。
样例 24

Row(
children:[
Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.')),
Container(color: Colors.green, child: Text('Goodbye!')),
]
)
由于 Row
不会对其子级施加任何约束,
因此它的 children 很有可能太大
而超出 Row
的可用宽度。在这种情况下,
Row
会和 UnconstrainedBox
一样显示溢出警告。
样例 25

Row(
children:[
Expanded(
child: Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.'))
),
Container(color: Colors.green, child: Text('Goodbye!')),
]
)
当 Row
的子级被包裹在了 Expanded
widget 之后,
Row
将不会再让其决定自身的宽度了。
取而代之的是,Row
会根据所有 Expanded
的子级
来计算其该有的宽度。
换句话说,一旦你使用 Expanded
,
子级自身的宽度就变得无关紧要,直接会被忽略掉。
样例 26

Row(
children:[
Expanded(
child: Container(color: Colors.red, child: Text(‘This is a very long text that won’t fit the line.’)),
),
Expanded(
child: Container(color: Colors.green, child: Text(‘Goodbye!’),
),
]
)
如果所有 Row
的子级都被包裹了 Expanded
widget,
每一个 Expanded
大小都会与其 flex 因子成比例,
并且 Expanded
widget 将会强制其子级具有与 Expanded
相同的宽度。
换句话说,Expanded
忽略了其子 Widget
想要的宽度。
样例 27

Row(children:[
Flexible(
child: Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.'))),
Flexible(
child: Container(color: Colors.green, child: Text(‘Goodbye!’))),
]
)
如果你使用 Flexible
而不是 Expanded
的话,
唯一的区别是,Flexible
会让其子级具有与
Flexible
相同或者更小的宽度。
而 Expanded
将会强制其子级具有和
Expanded
相同的宽度。
但无论是 Expanded
还是 Flexible
在它们决定子级大小时都会忽略其宽度。
这意味着,
Row
要么使用子级的宽度, 要么使用Expanded
和Flexible
从而忽略子级的宽度。
样例 28

Scaffold(
body: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
]
)))
屏幕强制 Scaffold
变得和屏幕一样大,
所以 Scaffold
充满屏幕。
然后 Scaffold
告诉 Container
可以变为任意大小,
但不能超出屏幕。
当一个 widget 告诉其子级可以比自身更小的话, 我们通常称这个 widget 对其子级使用 宽松约束(loose)。
样例 29

Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
],
))))
如果你想要 Scaffold
的子级变得和 Scaffold
本身一样大的话,
你可以将这个子级外包裹一个 SizedBox.expand
。
当一个 widget 告诉它的子级必须变成某个大小的时候, 我们通常称这个 widget 对其子级使用 严格约束(tight)。
严格约束(Tight) vs 宽松约束(loose)
以后你经常会听到一些约束为严格约束或宽松约束, 你花点时间来弄明白它们是值得的。
严格约束给你了一种获得确切大小的选择。 换句话来说就是,它的最大/最小宽度是一致的,高度也一样。
如果你到 Flutter 的 box.dart
文件中搜索
BoxConstraints
构造器,你会发现以下内容:
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
如果你重新阅读 样例 2,
它告诉我们屏幕强制 Container
变得和屏幕一样大。
为何屏幕能够做到这一点,
原因就是给 Container
传递了严格约束。
一个宽松约束换句话来说就是设置了最大宽度/高度, 但是让允许其子 widget 获得比它更小的任意大小。 换句话来说,宽松约束的最小宽度/高度为 0。
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()
方法, 由该方法执行列布局。

最后,十分感谢参与校对的程路、Alex,以及帮助打磨译文的 CaiJingLong、任宇杰、孙恺 以上几位同学,谢谢!
希望看完这篇文章,能够对你有所收获。如果你遇到任何疑惑,或者想要与我讨论,欢迎在底部评论区一起交流,或是通过邮箱与我联系。Happy coding!
转载自:https://juejin.cn/post/6846687593745088526