作者 | Marcelo Glasberg
原文 | https://flutter.dev/docs/development/ui/layout/constraints
转载自公众号:非著名开发者
阅读此文大概需要 3分钟
在学习 Flutter 的时候,是不是经常有人问你为什么有些 Widget 设置了宽度 width: 100 而展示出来并不是 100 像素呢?而你一般是告诉他们把 Widget 放在 Center 里面,对吗?
不要这样做。
如果你这样做,他们会一次又一次地回来,询问为什么有些 FittedBox 无法正常工作,为什么 Column 溢出,或者 IntrinsicWidth 是用来做什么用的。
这时你应该先告诉他们 Flutter 布局与 HTML 布局有着很大的不同(他们之前的经验可能是 HTML ),然后让他们记住以下规则:
约束向下传递,Size向上传递,父级设置位置。
Constraints go down. Sizes go up. Parent sets position.
如果不了解此规则,就无法真正理解 Flutter 的布局,因此 Flutter 开发人员应该尽早学习。
更详细地解释:
Widget 从其父级获得自己的约束。约束仅由4个 double 类型组成:最小和最大宽度、最小和最大高度。
然后, Widget 会遍历自己的子项(children),逐个向每个子项告知它们的约束(各个子项的约束可以是不同的),然后询问每个子项想要设置的大小。
然后,Widget 一个个确定子项的位置(在 x 轴上确定水平位置,在 y 轴上确定垂直位置)。
最后,Widget 将自己的大小告诉父级(当然这个大小也要符合原始约束)。
例如,一个组合的 Widget 包含一个带有 padding 的 Column ,这个 Column 想要如下布局两个子项:
这个 Column 是按如下方式判断的:
Widget:你好父项(Parent) ,我的约束是什么?
Parent:你的宽度必须在 80 到 300 像素之间,高度在30到85像素之间。
Widget:Hmmm,因为我想要 5 像素的 padding,所以我的子项最多有 290 像素的宽度和 75 像素的高度。
Widget:你好第一个子项(First Child),你的宽度必须在 0 到 290 像素之间,高度在 0 到 75 像素之间。
First Child:好的,我想要 290 像素宽度和 20 像素高度。
Widget:Hmmm,因为我想让第二个子项(Second Child)在第一个子项(First Child)的下面,所以第二个子项只剩下 5 像素的高度。
Widget:你好第二个子项(Second Child),你的宽度必须在 0 到 290 像素之间,高度必须在 0 到 55像素之间。
Second Child:好的,我想要 140 像素宽度,30 像素高度。
Widget:非常好,我将把第一个子项(First Child)放在 x: 5 y:5 的位置,第二个子项(Second Child)放在 x: 80 y: 25 的位置。
Widget:你好父项(Parent),我决定将自己的宽度设置为 300 像素,高度设置成 60 像素。
由于上述布局规则,Flutter 的布局引擎有一些重要的限制:
Widget 只能在父级(Parent)的限制内决定自身的大小。这意味着 Widget 通常不能拥有它想要的任意大小。
Widget 不知道也无法确定其在屏幕上的位置,因为它的位置是由父级(Parent)决定的。
由于父级(Parent)的大小和位置又取决于其父级(Parent),因此只有考虑整个布局树的情况下才能精确定义所有 Widget 的大小和位置。
可以运行 DartPad 来观察每个示例的效果,点击水平的数字可以在不同示例之间切换,总共有 29 个不同的示例。
此文中不能直接嵌入 DartPad ,所以仅截图示意。
如果你愿意,可以从 Github 获取源代码
https://github.com/marcglasberg/flutterlayoutarticle
下面将依次讲解每个示例。
Container(color: Colors.red)
屏幕是 Container 的父级,它强制 Container 与屏幕大小完全相同。
因此,Container 将填充屏幕并绘制为红色。
Container(width: 100, height: 100, color: Colors.red)
红色的 Container 要设为 100x100 ,但它不能,因为屏幕会强制将其尺寸设置与屏幕完全相同。
因此,Container 将填满整个屏幕。
Center(
child: Container(width: 100, height: 100, color: Colors.red)
)
屏幕会强制 Center 与屏幕尺寸完全相同,因此 Center 会填满整个屏幕。
Center 告诉 Container,它可以是所需的任意大小,但不能大于屏幕。所以现在 Container 就是 100x100 。
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: Colors.red),
)
这与前面的示例不同,因为它使用 Align 而不是 Center 。
Align 告诉 Container 可以是任意大小,但如果有空白空间,将 Container 与可用空间的右下角对齐。
Center(
child: Container(
color: Colors.red,
width: double.infinity,
height: double.infinity,
)
)
屏幕会强制 Center 与屏幕大小完全相同,因此 Center 会填满整个屏幕。
Center 告诉 Container ,它可以是所需的任意大小,但不能大于屏幕。 Container 希望具有无限大的尺寸,但是由于不能大于屏幕,因此只能填满屏幕。
Center(child: Container(color: Colors.red))
屏幕会强制 Center 与屏幕大小完全相同,因此 Center 会填满屏幕。
Center 告诉 Container ,它可以是所需的任意大小,但不能大于屏幕。由于 Container 没有子项(Child)并且没有固定的大小,因此它决定要使其尽可能大,以便填满整个屏幕。
但是,为什么 Container 要这样决定呢?这仅仅是因为创建 Container 的人设计决定的。它的创建方式可能会有所不同,你必须阅读 Container 文档了解它的行为。
Center(
child: Container(
color: Colors.red,
child: Container(color: Colors.green, width: 30, height: 30),
)
)
屏幕会强制 Center 与屏幕大小完全相同,因此 Center 会填满整个屏幕。
Center 告诉红色 Container ,它可以是所需的任意大小,但不能大于屏幕。由于红色 Container 没有大小,但它有一个 child ,因此它决定要与 child 的大小相同。
红色 Container 告诉 child 可以是所需的任意大小,但不能大于屏幕大小。
child 是一个绿色的 Container ,其尺寸为 30x30 。因为红色 Container 大小与 child 相同,所以红色 Container 也是 30x30 ,但是红色是不可见的,因为绿色 child 完全覆盖了红色 Container 。
Center(
child: Container(
color: Colors.red,
padding: const EdgeInsets.all(20.0),
child: Container(color: Colors.green, width: 30, height: 30),
)
)
红色的 Container 将自己调整为 child 的大小,同时会考虑自己的 padding ,因此它的大小为 70×70 。由于有 padding ,所以红色是可见的,绿色的 Container 具有与前面示例相同的大小。
ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 10, height: 10),
)
您可能会猜测 Container 必须在 70 到 150 像素之间,但是你错了。 ConstrainedBox 只会在 Widget 从父级获取的约束基础之上施加额外的约束。
在这里,屏幕强制 ConstrainedBox 的大小与屏幕大小完全相同,因此它告诉其子 Container 也不能超出屏幕大小,从而忽略了它的 constrains 参数。
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个像素(最小约束值)。
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 参数中为其子项施加额外的约束。
因此,容器必须介于 70 到 150 像素之间。它希望有 1000 个像素,但最终只有150个像素(最大约束限制)。
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 像素,结果就是这个大小,因为它介于 70 到 150 之间。
UnconstrainedBox(
child: Container(color: Colors.red, width: 20, height: 50),
)
屏幕强制 , UnconstrainedBox 与屏幕大小完全相同。但是 UnconstrainedBox 允许其子项 Container 自由设定大小。
UnconstrainedBox(
child: Container(color: Colors.red, width: 4000, height: 50),
)
屏幕强制 UnconstrainedBox 与屏幕大小完全相同, UnconstrainedBox 允许其子项 Container 自由设定大小。
不幸的是,在这种情况下, Container 的宽度为 4000 像素,太大而无法容纳在 UnconstrainedBox 中,因此 UnconstrainedBox 会显示令人恐惧的“溢出警告”。
OverflowBox(
minWidth: 0.0,
minHeight: 0.0,
maxWidth: double.infinity,
maxHeight: double.infinity,
child: Container(color: Colors.red, width: 4000, height: 50),
);
屏幕将强制 OverflowBox 与屏幕大小完全相同,并且 OverflowBox 允许其子 Container 自由设定大小。
OverflowBox 与 UnconstrainedBox 相似;所不同的是,如果 child 超出了父级的范围,它不会显示任何警告。
在这种情况下, Container 的宽度为 4000 像素,因为太大而无法容纳在 OverflowBox 中,但是 OverflowBox 只会显示能自己能显示的部分,而不会发出警告。
UnconstrainedBox(
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
这不会渲染任何内容,并且您会在控制台中看到错误。
UnconstrainedBox 允许其子项自由设置大小,但是其子项 Container 的大小是无限大。
Flutter 无法呈现无限大小,因此会引发以下错误消息: BoxConstraints forces an infinite width.
UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
)
这里将不会再出现错误,因为当 UnconstrainedBox 为 LimitedBox 赋予无限大小时, LimitedBox 向子项传递 100 像素宽度上限。
如果你将 UnconstrainedBox 换成 Center ,则 LimitedBox 将不再应用自己的限制(因为其限制仅在获得无限约束时才有效),所以 Container 的宽度允许超过 100 像素。
这解释了 LimitedBox 和 ConstrainedBox 之间的区别。
FittedBox(
child: Text('Some Example Text.'),
)
屏幕将强制 FittedBox 与屏幕大小完全相同。 Text 具有一定的自然宽度(也称为其固有宽度),该宽度取决于文本的数量,字体大小等。
FittedBox 让 Text 自由设定大小,但是在 Text 将其大小告知 FittedBox 之后, FittedBox 会缩放 Text 直到填满所有可用宽度。
Center(
child: FittedBox(
child: Text('Some Example Text.'),
)
)
但是,如果将 FittedBox 放在 Center 内会发生什么? Center 会让 FittedBox 的大小不能超出屏幕。
然后, FittedBox 会将其自身调整为文本大小,并让 Text 自由设定大小。由于 FittedBox 和 Text 具有相同的大小,因此不会发生缩放。
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 的大小以使其也适合屏幕。
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 将从屏幕获取最大宽度,并会换行以使其适合屏幕。
FittedBox(
child: Container(
height: 20.0,
width: double.infinity,
)
)
FittedBox 只能缩放有界的 child(宽度和高度非无限大)。否则,它将无法渲染任何内容,并且您会在控制台中看到错误。
Row(
children:[
Container(color: Colors.red, child: Text('Hello!')),
Container(color: Colors.green, child: Text('Goodbye!')),
]
)
屏幕会强制使 Row 与屏幕大小完全相同。
就像 UnconstrainedBox 一样, Row 不会对其子项施加任何约束,而是让它们自由设定大小。然后,Row 会将它们并排放置,并且空下剩余的空间。
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 不会对其子项施加任何约束,因此很有可能子项太大而无法容纳 Row 的可用宽度。在这种情况下,就像 UnconstrainedBox 一样,行将显示“溢出警告”。
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 中时, Row 将不再允许子项定义自己的宽度。
取而代之的是,它根据其他子项定义 Expanded 宽度,然后, Expanded 才强制原始子项(即 Expanded 的子项)具有 Expanded 的宽度。
换句话说,一旦你使用了 Expanded ,原始 child 的宽度就变得无关紧要,并且会被忽略。
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 中,则每个 Expanded 的尺寸都与其 flex 参数成比例,然后,每个 Expanded 都会强制其子项具有 Expanded 的宽度。
换句话说, Expanded 会忽略其子项的首选宽度。
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 在调整自己的大小时都会忽略 child 的宽度。
注意:这意味着无法按大小比例扩展 Row 子项。Row 要么使用确切的 child 宽度,要么在使用 Expanded 或 Flexible 时完全忽略 child 的宽度。
Scaffold(
body: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
]
)
)
)
屏幕会迫使 Scaffold 与屏幕大小完全相同,因此 Scaffold 会填满屏幕。 Scaffold 告诉容器,它可以是所需的任何大小,但不能大于屏幕。
注意:当 Widget 告诉其子项它可以小于某个特定大小时,我们说该 Widget 为其子项提供了宽松的约束(loose constraints)。稍后进一步说明。
Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
],
)),
),
);
如果你希望 Scaffold 的 child 的大小与 Scaffold 本身的大小完全相同,则可以使用 SizedBox.expand 包装 child 。
注意:当 Widget 告诉其子项必须具有一定大小时,我们说该 Widget 为其子项提供了严格的约束(tight constraints)。
经常听到一些约束是“严格”或“宽松”的说法,这里讲讲它们的含义。
严格的约束提供了一种可能性,即确切的大小。换句话说,严格约束的最大宽度等于其最小宽度。并且其最大高度等于其最小高度。
如果转到 Flutter 的 box.dart 文件并搜索 BoxConstraints 构造函数,则会发现以下内容:
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
如果您重新查看上面的 示例2 ,它告诉我们屏幕强制红色 Container 与屏幕大小完全相同。当然,屏幕是通过将严格的约束传递给 Container 来实现的。
另一方面,宽松的约束设置了最大宽度和高度,但使 Widget 尽可能小。换句话说,宽松约束的最小宽度和高度都等于零:
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 的行为。
布局源代码通常很复杂,因此最好阅读文档。但是,如果你决定研究布局源代码,则可以使用IDE的导航功能轻松找到它。
举个例子:
假设我们要查看 Column 的源码,在 Android Studio 或 IntelliJ 中使用 command + B (macOS)或 control + B (Windows / Linux),你将被带到 basic.dart 文件。由于 Column 扩展了 Flex ,因此请导航至 Flex 源代码(也在 basic.dart 中)。
向下滚动,直到找到一个名为 createRenderObject() 的方法。如你所见,此方法返回 RenderFlex 。这是 Column 的渲染对象。现在,导航到 RenderFlex 的源代码,它将带您进入 flex.dart 文件。
向下滚动,直到找到一个名为 performLayout() 的方法。这是进行 Column 布局的方法。
Marcelo Glasberg 的文章
Marcelo 最初将此内容发布为 Flutter: The Advanced Layout Rule Even Beginners Must Know 。我们喜欢它,并请求他允许我们在 flutter.dev 上发布,他对此表示赞同。谢谢,Marcelo!你可以在 GitHub 和 pub.dev 上找到Marcelo。
另外,还要感谢 Simon Lightfoot 在文章顶部创建标题图像。
为感谢大家对「Flutter编程指南」公众号的关注,今特意发起一次免费抽奖送书活动,点击下面小程序进行抽奖,中奖者 将获得《Flutter技术入门与实战》、《Flutter:从0到1构建大前端应用》、《Dart编程语言》和《Flutter技术解析与实战-闲鱼技术演进》等 4 本书籍中的一本,详情见抽奖详情页。