这篇文章是 Flutter 官方文档的一部分,还有 Kirill Matrosov 翻译的俄语版、Suat Özkaya 翻译的土耳其语版和 Saeed Hassankhan 翻译的波斯语版。
假设有一个整个学习 Flutter 的人问你,为什么一些 width:100
的微件的宽度不是 100 像素,标准答案是让他把微件放在一个 Center
里面,对吗?
不对。
如果你这么回答他,他会有层出不穷的问题,比如:为什么某些 FittedBox
不工作了?为什么那个 Column
溢出?或者 IntrinsicWidth
是用来做什么的?
所以,我们应该告诉他们 Flutter 布局与 HTML 布局有很大差异(他最先接触的大概率是 HTML ),让他们记住以下规则。
**
约束(Constraints)在下面,大小(Sizes)在上面。位置(Positions)由父元素(Parents)决定。
**
想要真正理解 Flutter 的布局,就得搞清楚上面这条规则,所以大家都应该尽早学会它。
具体来说:
-
微件从其父元素处获得自己的约束 。一个“约束”是由 4 个 double 值组成的:分别是最小和最大宽度以及最小和最大高度。
-
然后,微件会遍历自己的子元素(children) 列表。微件会逐个告诉每个子元素它们的约束 (每个子元素的约束可以是不同的),然后询问每个子元素想要设置的大小。
-
接下来,微件会确定每个子元素的位置 (在
x
轴上确定水平位置,在y
轴上确定垂直位置)。 -
最后,微件将其自身大小告知父元素(当然这个大小也要符合原始约束)。
例如,如果一个微件是一个带有一些 padding 的 column,并且想要布局自己的两个子元素:
微件:你好,父元素,我的约束是什么?
父元素:你的宽度必须在 90
到 300
像素之间,高度在 30
到 85
像素之间。
微件:我想有 5
像素的 padding,所以我的子项最多有 290
像素的宽度和 75
像素的高度。
微件:你好,第一个子元素,你的宽度必须在 0
到 290
像素之间,高度在 0
到 75
像素之间。
第一个子元素:好的,那么我希望自己的宽度是 290
像素,高度为 20
像素。
微件:那么,因为我想将第二个子元素放在第一个子元素之下,因此第二个子元素只剩下 55
像素的高度。
微件:你好第二个子元素,你的宽度必须介于 0
到 290
像素之间,并且高度必须介于 0
到 55
像素之间。
第二个子元素:好吧,我希望宽度是 140
像素,高度为 30
像素。
微件:很好。我将把第一个子元素放在 x: 5
和 y: 5
的位置,将第二个子元素放在 x: 80
和 y: 25
的位置。
微件:你好父元素,我决定将自己的宽度设为 300
像素,高度设为 60
像素。
限制
因为上述布局规则的关系,Flutter 的布局引擎有一些重要的限制:
-
一个微件只能在其父元素赋予的约束内决定其自身的大小,所以微件往往不能自由决定自己的大小 。
-
微件不知道、也无法确定自己在屏幕上的位置 ,因为它的位置是由父元素决定的。
-
由于父元素的大小和位置又取决于上一级父元素,因此只有考虑整个树才能精确定义每个微件的大小和位置。
示例
为获得互动体验:
-
运行下面的 CodePen(先点击
Run Pen
按钮,然后点击出现在右下方的Rerun
按钮); -
或者运行 DartPad;
-
或者从 GitHub repo 获取最新代码。
https://codepen.io/marcglasberg/embed/ExVmwed?default-tab=&theme-id=
例 1
Container(color: Colors.red)
屏幕是 Container
的父体。它强制红色 Container
与屏幕的大小完全相同。
这样 Container
就会填满整个屏幕,整个屏幕都变成红色。
例 2
Container(width: 100, height: 100, color: Colors.red)
红色的 Container
的尺寸不能被设置为 100×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
对齐放到可用空间的右下角。
例 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
微件的开发人员的设计。有可能有其他设计,所以我们需要阅读 Container
文档,了解它在不同情况下的行为方式。
例 7
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
。
例 8
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
的大小与上个示例中的相同。
例 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
大小设置为与屏幕完全相同的尺寸,因此它会告诉自己的子 Container
不能超出屏幕大小,这样就忽略了它的 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 像素( 最小约束值 )。
例 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 像素( 最大约束值 )。
例 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 像素,结果就会是这个大小,因为这个值介于 70 到 150 之间。
例 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
子元素自由设定大小。
但是,这个例子中的 Container
的宽度为 4000 像素,宽度过大,所以无法容纳在 UnconstrainedBox
中,因此 UnconstrainedBox
会有“溢出警告”。
例 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
允许 Container
子元素自由设定大小。
这里的的 OverflowBox
与 UnconstrainedBox
相似,不同之处在于,如果子元素超出了它的范围,它也不会显示任何警告。
在这个例子中,Container
的宽度为 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
赋予无限的大小时,LimitedBox
会向自己的子元素传递 100 的宽度上限。
注意:如果将 UnconstrainedBox
更改为 Center
微件,则 LimitedBox
就不会再应用自己的限制(因为其限制仅在约束为无限时才会应用),并且 Container
的宽度可以超过 100。
这清楚表明了 LimitedBox
和 ConstrainedBox
的区别。
例 18
FittedBox(
child: Text('Some Example Text.'),
)
屏幕强制 FittedBox
与屏幕大小完全相同。Text
有自然宽度(也称为其固有宽度),该宽度取决于文本的数量和字体大小等。
FittedBox
让 Text
自由设定大小,但是在 Text
将其大小告知 FittedBox
之后,FittedBox
会对其进行缩放,使其填满可用宽度。
例 19
Center(
child: FittedBox(
child: Text('Some Example Text.'),
)
)
但是,如果将 FittedBox
放在 Center
内会怎样?Center
会让 FittedBox
选择任何尺寸,但不能超出屏幕。
然后,·FittedBox· 会将其自身尺寸调整为 Text
的大小,并让 Text
自由设定大小。由于 FittedBox
和 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
但是,如果我们移除 FittedBox
,则 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.'),
)
例 22
FittedBox(
child: Container(
height: 20.0,
width: double.infinity,
)
)
注意 FittedBox
只能缩放有界的微件(宽度和高度都不能使无限的)。否则,它将无法渲染任何内容,而且你会从控制台收到错误消息。
例 23
Row(
children:[
Container(color: Colors.red, child: Text('Hello!')),
Container(color: Colors.green, child: Text('Goodbye!)),
]
)
屏幕强制 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
不会对其子元素施加任何约束,因此子元素可能会太大,超出可用的 Row
的宽度。在这种情况下,就像 UnconstrainedBox
一样,Row
会显示“溢出警告”。
例 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
微件中时,Row
将不再允许该子元素定义自己的宽度。
相反,它将根据其他子元素定义 Expanded
的宽度,只有这样 Expanded
微件才会强制原始子元素的宽度与 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
微件中,则每个 Expanded
的大小将与其 flex
参数成比例,只有这样,每个 Expanded
微件才会强制其子元素的宽度等于 Expanded
。
换句话说,Expanded
会忽略其子元素的首选宽度。
例 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
子元素。Row
要么使用与子元素相同的宽度,要么在使用 Expanded
或 Flexible
时完全忽略子元素。
例 28
Scaffold(
body: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
]
)))
屏幕会强制 Scaffold
与屏幕完全相同,因此 Scaffold
会填满屏幕。
Scaffold
告诉 Container
可以选择任何尺寸,但不能超出屏幕大小。
注意:当微件告诉其子元素可以小于某个特定大小时,我们说该微件为其子元素提供了“宽松”的约束,稍后会进一步说明。
例 29
Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
],
))))
如果我们希望 Scaffold
的子元素大小与 Scaffold
完全相同,则可以将其子元素封装到一个 SizedBox.expand
中。
注意:当微件告诉其子元素必须设为某个特定尺寸时,我们说该微件为其子元素提供了“严格”的约束。
严格/松散的约束
我们经常听到某些约束是“严格”或“宽松”的说法 ,因此这里讲讲它们的含义。
严格的约束只提供了一种可能性:一个确定的大小,即严格约束的最大宽度等于其最小宽度,最大高度等于其最小高度。
如果你找到 Flutter 的 box.dart
文件并搜索 BoxConstraints
构造器,你会发现以下内容:
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
再次回顾例 2,例 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
)的宽松约束。
学习特定微件的布局规则
我们需要了解通用的布局规则,但只了解这些还不够。
每个微件在应用通用规则时都有很大的自由度,因此只看微件的名称是没法知道它的确切功能。
只靠猜测的话可能会猜错,所以要阅读微件的文档,或者研究其源代码,来了解微件的确切行为。
布局源码往往非常复杂,所以看源码的文档也很重要。但如果你决定要研究布局的源码,可以使用 IDE 的导航功能轻松找到它。
下面是一个示例:
-
在你的代码中找一些
Column
,然后找到其源代码(IntelliJ 中按下Ctrl-B
)。你会被带到basic.dart
文件,由于Column
扩展了Flex
,因此请找到Flex
源代码(也在basic.dart
中)。 -
向下滚动鼠标,找到一个名为
createRenderObject
的方法,此方法返回一个RenderFlex
,这是和Column
对应的渲染对象。然后找到RenderFlex
的源代码,进入flex.dart
文件。 -
接着滚动鼠标,找到一个名为
performLayout
的方法,这就是布局Column
的方法。
Simon Lightfoot 校对了本文,提供了本文的标题图片,还对本文的内容提出了宝贵的建议,特此感谢!
原文作者 Marcelo Glasberg
原文链接 https://medium.com/flutter-community/flutter-the-advanced-layout-rule-even-beginners-must-know-edc9516d1a2