Flutter 初学者必读的高级布局规则
对于Flutter学习者来说,掌握Flutter的布局行为,直接决定了开发者在布局的时候是否能做到高效、快速的开发,但是初学者面对茫茫多的Widget以及各种无法预料的布局行为,总是很难将心中所想,转化为Flutter的代码。
假设有人正在学习 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 来观察每个示例的效果。
https://dartpad.dev/60174a95879612e500203084a0588f94
另外可以从这个 GitHub 存储库中获取最新代码。
https://github.com/marcglasberg/flutter_layout_article
Container(color: Colors.red)
屏幕是 Container 的父项。它强制红色的 Container 与屏幕大小完全相同。这样 Container 就会填满整个屏幕,并且全都变成红色。
Container(width: 100, height: 100, color: Colors.red)
红色的 Container 想要设为 100×100 的大小,但这是不行的,因为屏幕会强制使其大小与屏幕完全相同。因此,Container 将填满整个屏幕。
Center(
child: Container(width: 100, height: 100, color: Colors.red)
)
屏幕会强制将 Center 设置为与屏幕大小完全相同。因此 Center 将填满整个屏幕。Center 告诉 Container,后者的大小不能超出屏幕。现在,Container 就可以是 100×100。
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 没有子项且没有固定大小,因此它决定要尽可能变大,结果就填满了屏幕。
但为什么 Container 要这样决定呢?因为这是 Container widget 的创建者的设计决策。它也可能会有其他设计,所以你需要阅读 Container 的文档以了解它在不同情况下的行为方式。
Center(
child: Container(
color: Colors.red,
child: Container(color: Colors.green, width: 30, height: 30),
)
)
屏幕会强制将 Center 设置为与屏幕大小完全相同。因此 Center 将填满整个屏幕。
Center 告诉红色 Container,后者的大小不能超出屏幕。由于红色 Container 没有大小,但有一个子项,因此它决定要与子项的大小相同。
红色的 Container 告知其子项,后者的大小不能超出屏幕。
这个子项恰好是一个绿色的 Container,希望自己的大小是 30×30。如上所述,红色的 Container 会将自己的大小设为子项的大小,因此它也会是 30×30。结果红色是显示不出来的,因为绿色的 Container 会完全覆盖红色的 Container。
Center(
child: Container(
color: Colors.red,
padding: const EdgeInsets.all(20.0),
child: Container(color: Colors.green, width: 30, height: 30),
)
)
红色的 Container 会根据子项的大小设置自己的大小,但同时会考虑自己的 padding。因此它将是 70×70(=30×30 加上各个面的 20 像素 padding)。由于存在 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 也不能超出屏幕大小,这样就忽略了它的 constraints 参数。
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 参数中为其子项施加 额外的 约束。
因此,Container 必须介于 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 相似,不同之处在于,如果子项超出了它的范围,它也不会显示任何警告。
在这个例子中下,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 赋予一个无限的大小时,后者将向自己的子项传递 100 的宽度上限。
请注意,如果将 UnconstrainedBox 更改为 Center widget,则 LimitedBox 就不会再应用自己的限制(因为其限制仅在约束为无限时才会应用),并且 Container 的宽度将被允许超过 100。
这清楚表明了 LimitedBox 和 ConstrainedBox 之间的区别。
FittedBox(
child: Text('Some Example Text.'),
)
屏幕强制 FittedBox 与屏幕大小完全相同。Text 将有一些自然宽度(也称为其固有宽度),该宽度取决于文本的数量和字体大小等。
FittedBox 将让 Text 自由设定大小,但是在 Text 将其大小告知 FittedBox 之后,FittedBox 会对其进行缩放,使其填满可用宽度。
Center(
child: FittedBox(
child: Text('Some Example Text.'),
)
)
但是,如果将 FittedBox 放在 Center 内会怎样?Center 会让 FittedBox 的大小最大不能超出屏幕。
然后,FittedBox 会将其自身调整为 Text 的大小,并让 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 只能缩放 有界的 widget(宽度和高度都不是无限的)。否则,它将无法渲染任何内容,并且你会在控制台中收到错误消息。
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 将显示“溢出警告”。
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 将不再允许该子项定义自己的宽度。
相反,它将根据其他子项定义 Expanded 的宽度,只有这样 Expanded widget 才会强制原始子项的宽度与 Expanded 相同。
换句话说,一旦你使用了 Expanded,原始子项的宽度就不重要了,并且将被忽略。
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 会忽略其子项的首选宽度。
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 子项。Row 要么使用与子项相同的宽度,或者在使用 Expanded 或 Flexible 时完全忽略子项。
Scaffold(
body: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
]
)))
屏幕会强制 Scaffold 与屏幕完全相同。因此 Scaffold 会填满屏幕。
Scaffold 告诉 Container,后者不能超出屏幕大小。
注意:当 widget 告诉其子项可以小于某个特定大小时,我们说该 widget 为其子项提供了“宽松”的约束。稍后会进一步说明。
Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
],
))))
如果我们希望 Scaffold 的子项大小与 Scaffold 本身完全相同,则可以将其子项包装到一个 SizedBox.expand 中。
注意:当 widget 告诉其子项必须等于某个大小时,我们说该 widget 为其子项提供了“严格”的约束。
我们经常听到某些约束是“严格”或“宽松”的 说法 ,因此这里讲讲它们的含义。
严格的约束只提供了一种可能性:一个确定的大小。换句话说,严格约束的最大宽度等于其最小宽度,并且其最大高度等于最小高度。
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
再次回顾上面的示例 2,它告诉我们屏幕强制红色的 Container 与屏幕尺寸完全相同。当然,屏幕是将严格的约束传递给 Container 来实现这一点的。
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,然后导航到其源代码(IntelliJ 中按下 Ctrl-B)。你将被带到 basic.dart 文件。由于 Column 扩展了 Flex,因此请导航至 Flex 源代码(也位于 basic.dart 中)。
现在向下滚动,直到找到一个名为 createRenderObject 的方法。如你所见,此方法返回一个 RenderFlex。这是和 Column 对应的渲染对象。现在导航到 RenderFlex 的源代码,IDE 会带你进入 flex.dart 文件。
现在向下滚动,直到找到一个名为 performLayout 的方法。这就是为 Column 布局的方法。
非常感谢 Simon Lightfoot( https://github.com/slightfoot)校对本文,提供标题图片并为本文提供内容建议。
备注:本文已加入 Flutter 官方文档:
https://flutter.dev/docs/development/ui/layout/constraints
https://medium.com/flutter-community/flutter-the-advanced-layout-rule-even-beginners-must-know-edc9516d1a2