假设有人正在学习 Flutter,他问你为什么有的 width:100 的 widget 宽度不是 100 像素,标准答案是让他将 widget 放在一个 Center 里面,对吗?
别这么做。
如果你这么回答他,他就会一次又一次跑回来问你新的问题,比如说为什么某些 FittedBox 无法正常工作,为什么那个 Column 溢出,或者 IntrinsicWidth 是用来做什么的,诸如此类。
这时候你应该告诉他:Flutter 布局与 HTML 布局(他之前可能接触的就是后者)有着很大不同,然后让他记住以下规则:
约束(Constraints)在下面,大小(Sizes)在上面。位置(Positions)由父项(Parents)决定。
想要真正理解 Flutter 的布局,就得搞清楚上面这条规则,所以大家都应该尽早学会它。
具体来说:
widget 从其父项获得自己的约束。一个“约束”是由 4 个 double 值组成的:分别是最小和最大宽度,以及最小和最大高度。
然后,widget 会遍历自己的子项(children)列表。widget 会逐个向每个子项告知它们的约束(各个子项的约束可以是不同的),然后询问每个子项想要设置的大小。
接下来,widget 一个个确定子项的位置(在 x 轴上确定水平位置,在 y 轴上确定垂直位置)。
最后,widget 将其自身大小告知父项(当然这个大小也要符合原始约束)。
例如,如果一个 widget 是一个带有一些 padding 的 column,并且想要布局自己的两个子项:
Widget:你好父项,我的约束是什么?
父项:你的宽度必须在 90 到 300 像素之间,高度在 30 到 85 像素之间。
Widget:我想有 5 像素的 padding,所以我的子项最多有 290 像素的宽度和 75 像素的高度。
Widget:你好第一个子项,你的宽度必须在 0 到 290 像素之间,高度在 0 到 75 像素之间。
第一个子项:好的,那么我希望自己的宽度是 290 像素,高度为 20 像素。
Widget:那么,因为我想将第二个子项放在第一个子项之下,因此第二个子项只剩下 55 像素的高度。
Widget:你好第二个子项,你的宽度必须介于 0 到 290 像素之间,并且高度必须介于 0 到 55 像素之间。
第二个子项:好吧,我希望宽度是 140 像素,高 30 像素。
Widget:很好。我将把第一个子项放在 x: 5 和 y: 5 的位置,将第二个子项放在 x: 80 和 y: 25 的位置。
Widget:你好父项,我决定将自己设为 300 像素宽和 60 像素高。
限制
因为上述布局规则的关系,Flutter 的布局引擎有一些重要的限制:
一个 widget 只能在其父项赋予的约束内决定其自身的大小。这意味着 widget 往往不能自由决定自己的大小。
widget 不知道,也无法确定自己在屏幕上的位置,因为它的位置是由父项决定的。
由于父项的大小和位置又取决于上一级父项,因此只有考虑整个树才能精确定义每个 widget 的大小和位置。
示例
可以运行这个DartPad来观察每个示例的效果。另外可以从这个GitHub存储库中获取最新代码。
示例 1
屏幕是 Container 的父项。它强制红色的 Container 与屏幕大小完全相同。
这样 Container 就会填满整个屏幕,并且全都变成红色。
示例 2
红色的 Container 想要设为 100×100 的大小,但这是不行的,因为屏幕会强制使其大小与屏幕完全相同。
因此,Container 将填满整个屏幕。
示例 3
屏幕会强制将 Center 设置为与屏幕大小完全相同。因此 Center 将填满整个屏幕。
Center 告诉 Container,后者的大小不能超出屏幕。现在,Container 就可以是 100×100。
示例 4
这与前面的示例不同之处是使用了 Align 代替 Center。
Align 还告诉 Container,后者的大小可以自由决定,但是如果有空白空间,它不会让 Container 居中,而是将其对齐到可用空间的右下角。
示例 5
屏幕会强制将 Center 设置为与屏幕大小完全相同。因此 Center 将填满整个屏幕。
Center 告诉 Container,后者的大小不能超出屏幕。Container 希望具有无限大的尺寸,但由于存在前述约束,因此它只能填满屏幕。
示例 6
屏幕会强制将 Center 设置为与屏幕大小完全相同。因此 Center 将填满整个屏幕。
Center 告诉 Container,后者的大小不能超出屏幕。由于 Container 没有子项且没有固定大小,因此它决定要尽可能变大,结果就填满了屏幕。
但为什么 Container 要这样决定呢?因为这是 Container widget 的创建者的设计决策。它也可能会有其他设计,所以你需要阅读 Container 的文档以了解它在不同情况下的行为方式。
示例 7
屏幕会强制将 Center 设置为与屏幕大小完全相同。因此 Center 将填满整个屏幕。
Center 告诉红色 Container,后者的大小不能超出屏幕。由于红色 Container 没有大小,但有一个子项,因此它决定要与子项的大小相同。
红色的 Container 告知其子项,后者的大小不能超出屏幕。
这个子项恰好是一个绿色的 Container,希望自己的大小是 30×30。如上所述,红色的 Container 会将自己的大小设为子项的大小,因此它也会是 30×30。结果红色是显示不出来的,因为绿色的 Container 会完全覆盖红色的 Container。
示例 8
红色的 Container 会根据子项的大小设置自己的大小,但同时会考虑自己的 padding。因此它将是 70×70(=30×30 加上各个面的 20 像素 padding)。由于存在 padding,因而红色将是可见的,绿色的 Container 的大小与上一个示例中的相同。
示例 9
你可能会以为 Container 会是 70 到 150 像素之间,但是你错了。ConstrainedBox 只会在 widget 从父项获得的约束基础之上施加额外的约束。
在这里,屏幕将 ConstrainedBox 强制为与屏幕大小完全相同,因此它将告诉自己的子 Container 也不能超出屏幕大小,这样就忽略了它的 constraints 参数。
示例 10
现在,Center 将允许 ConstrainedBox 的大小最大不能超出屏幕。ConstrainedBox 将从其 constraints 参数中为其子项施加额外的约束。
因此,Container 必须介于 70 到 150 像素之间。它希望自己是 10 个像素,所以结果会是 70 像素(最小约束值)。
示例 11
Center 将允许 ConstrainedBox 的大小最大不能超出屏幕。ConstrainedBox 将从其 constraints 参数中为其子项施加额外的约束。
因此,Container 必须介于 70 到 150 像素之间。它希望自己是 1000 个像素,所以最后会是 150 像素(最大约束值)。
示例 12
Center 将允许 ConstrainedBox 的大小最大不能超出屏幕。ConstrainedBox 将从其 constraints 参数中为其子项施加额外的约束。
因此,Container 必须介于 70 到 150 像素之间。它希望自己是 100 像素,结果就会是这个大小,因为这个值介于 70 到 150 之间。
示例 13
屏幕强制 UnconstrainedBox 与屏幕大小完全相同。但是,UnconstrainedBox 允许其 Container 子项自由设定大小。
示例 14
屏幕强制 UnconstrainedBox 与屏幕大小完全相同,UnconstrainedBox 允许 Container 子项自由设定大小。
不幸的是,在这个例子中 Container 的宽度为 4000 像素,因为太大而无法容纳在 UnconstrainedBox 中,因此 UnconstrainedBox 将显示让人胆战心惊的“溢出警告”。
示例 15
屏幕强制 OverflowBox 与屏幕大小完全相同,并且 OverflowBox 允许 Container 子项自由设定大小。
这里的的 OverflowBox 与 UnconstrainedBox 相似,不同之处在于,如果子项超出了它的范围,它也不会显示任何警告。
在这个例子中下,Container 的宽度为 4000 像素,因为太大而无法容纳在 OverflowBox 中,但是 OverflowBox 只会显示自己能显示的部分,而不会发出警告。
示例 16
这不会渲染任何内容,并且你会在控制台中收到错误消息。
UnconstrainedBox 允许其子项自由设定大小,但是其 Container 子项的大小是无限的。
Flutter 无法渲染无限的大小,因此会显示以下错误消息:BoxConstraints forces an infinite width。
示例 17
这里你不会再遇到错误,因为当 UnconstrainedBox 为 LimitedBox 赋予一个无限的大小时,后者将向自己的子项传递 100 的宽度上限。
请注意,如果将 UnconstrainedBox 更改为 Center widget,则 LimitedBox 就不会再应用自己的限制(因为其限制仅在约束为无限时才会应用),并且 Container 的宽度将被允许超过 100。
这清楚表明了 LimitedBox 和 ConstrainedBox 之间的区别。
示例 18
屏幕强制 FittedBox 与屏幕大小完全相同。Text 将有一些自然宽度(也称为其固有宽度),该宽度取决于文本的数量和字体大小等。
FittedBox 将让 Text 自由设定大小,但是在 Text 将其大小告知 FittedBox 之后,FittedBox 会对其进行缩放,使其填满可用宽度。
示例 19
但是,如果将 FittedBox 放在 Center 内会怎样?Center 会让 FittedBox 的大小最大不能超出屏幕。
然后,FittedBox 会将其自身调整为 Text 的大小,并让 Text 自由设定大小。由于 FittedBox 和 Text 的大小相同,因此不会发生缩放。
示例 20
但是,如果 FittedBox 位于 Center 内部,但 Text 太大而超出了屏幕该怎么办?
FittedBox 将尝试让自己和 Text 一样大,但它不能超出屏幕。然后,它会设定和屏幕大小一样的目标,并调整 Text 的大小以使其也适合屏幕。
示例 21
但是,如果我们移除 FittedBox,则 Text 将从屏幕获得自己的最大宽度,并且会换行来适合屏幕宽度。
示例 22
注意 FittedBox 只能缩放有界的 widget(宽度和高度都不是无限的)。否则,它将无法渲染任何内容,并且你会在控制台中收到错误消息。
示例 23
屏幕强制 Row 与屏幕大小完全相同。
就像 UnconstrainedBox 一样,Row 不会对其子项施加任何约束,而是让它们自由设定大小。然后 Row 会将子项并排放置,并且空下剩余的空间。
示例 24
由于 Row 不会对其子项施加任何约束,因此子项可能会太大而超出了可用的 Row 宽度。在这种情况下,就像 UnconstrainedBox 一样,Row 将显示“溢出警告”。
示例 25
当一个 Row 子项包装在一个 Expanded widget 中时,Row 将不再允许该子项定义自己的宽度。
相反,它将根据其他子项定义 Expanded 的宽度,只有这样 Expanded widget 才会强制原始子项的宽度与 Expanded 相同。
换句话说,一旦你使用了 Expanded,原始子项的宽度就不重要了,并且将被忽略。
示例 26
如果所有 Row 子项都包装在 Expanded widget 中,则每个 Expanded 的大小将与其 flex 参数成比例,只有这样,每个 Expanded widget 才会强制其子项的宽度等于 Expanded。
换句话说,Expanded 会忽略其子项的首选宽度。
示例 27
如果使用 Flexible 代替 Expanded,则唯一的区别是 Flexible 将使其子项的宽度小于等于 Flexible 自身,而 Expanded 会强制其子项的宽度和 Expanded 完全相同。
但是,Expanded 和 Flexible 在调整自己的大小时都会忽略自己子项的宽度。
请注意,这意味着我们无法按大小比例扩展 Row 子项。Row 要么使用与子项相同的宽度,或者在使用 Expanded 或 Flexible 时完全忽略子项。
示例 28
屏幕会强制 Scaffold 与屏幕完全相同。因此 Scaffold 会填满屏幕。
Scaffold 告诉 Container,后者不能超出屏幕大小。
注意:当 widget 告诉其子项可以小于某个特定大小时,我们说该 widget 为其子项提供了“宽松”的约束。稍后会进一步说明。
示例 29
如果我们希望 Scaffold 的子项大小与 Scaffold 本身完全相同,则可以将其子项包装到一个 SizedBox.expand 中。
注意:当 widget 告诉其子项必须等于某个大小时,我们说该 widget 为其子项提供了“严格”的约束。
严格×宽松约束
我们经常听到某些约束是“严格”或“宽松”的说法,因此这里讲讲它们的含义。
严格的约束只提供了一种可能性:一个确定的大小。换句话说,严格约束的最大宽度等于其最小宽度,并且其最大高度等于最小高度。
转到 Flutter 的 box.dart 文件并搜索 BoxConstraints 构造器,你会发现以下内容:
再次回顾上面的示例 2,它告诉我们屏幕强制红色的 Container 与屏幕尺寸完全相同。当然,屏幕是将严格的约束传递给 Container 来实现这一点的。
另一方面,宽松的约束可设置最大宽度/高度,但允许 widget 自由取小于这个值的大小。换句话说,宽松约束的最小宽度/高度都等于零:
重新查看示例 3,它告诉我们:Center 让红色的 Container 大小不能大于屏幕。Center 将宽松的约束传递给 Container 来做到这一点。最终,Center 的主要目的是将其从父项(屏幕)获得的严格约束转换为对其子项(Container)的宽松约束。
学习特定 widget 的布局规则
我们需要了解通用的布局规则,但光是这样这还不够。
每个 widget 在应用通用规则时都有很大的自由度,因此只看 widget 的名称是没法知道它会做什么事情的。
如果你只靠猜测的话可能会猜错。除非你已阅读过 widget 的文档或研究了其源代码,否则你无法知道 widget 的确切行为。
布局源码往往是很复杂的,因此最好去看它们的文档。但是如果你决定要研究布局的源码,则可以使用 IDE 的导航功能轻松找到它。
下面是一个示例:
在你的代码中找到一些 Column,然后导航到其源代码(IntelliJ 中按下 Ctrl-B)。你将被带到 basic.dart 文件。由于 Column 扩展了 Flex,因此请导航至 Flex 源代码(也位于 basic.dart 中)。
现在向下滚动,直到找到一个名为 createRenderObject 的方法。如你所见,此方法返回一个 RenderFlex。这是和 Column 对应的渲染对象。现在导航到 RenderFlex 的源代码,IDE 会带你进入 flex.dart 文件。
现在向下滚动,直到找到一个名为 performLayout 的方法。这就是为 Column 布局的方法。
非常感谢Simon Lightfoot校对本文,提供标题图片并为本文提供内容建议。
备注:本文已加入Flutter官方文档。
参考阅读:
评论