本篇文章翻译自👉Understanding constraints,文章通过二十多个案例复原了Flutter的布局过程,希望可以帮助到大家。
作者: Marcelo Glasberg
有时学习Flutter的人会问一个问题,为什么有些widget设置了 width:100
,但是并没有显示100的像素宽度。这时有人会回答:把那个widget放到 Center
组件中。对吧。
不要这么回答
如果这么回答了,他们还会一遍一遍的在问:为什么一些 FittedBox
没有起作用,为什么有的 Column
超出范围了,为什么 IntrinsicWidth
应该怎么做。
相反,首先告诉他们 Flutter 的布局和 HTML 布局是非常不同的,然后让他们记住下面的布局规则:
向下👇传递约束。向上👆传递尺寸。父节点设置位置
不理解上面的规则的话。Flutter布局就不能被真正的理解,所以开发者应该尽早理解这个规则。 更多的细节:
-
Widget 从它的父节点获得自己的 constraints(约束)。constraint(约束) 由四个double类型的值设置,分别是:最小和最大的宽大,最小和最大的高度。
-
然后 Widget 遍历他自己的所有子节点,一个一个的告诉子节点的 constraints(约束) ,每一个子节点的约束都不同,并且问每一个子节点想要的尺寸是多少。
-
再次,Widget 会一个一个的摆放他的 子节点,水平使用
x
轴,垂直使用y
轴。 -
最后,Widget会告诉它的父节点它自己的 size(尺寸) ,这个尺寸会在第一步的约束范围之内。
我们举个例子🌰, 一个组合型的Widget包含一个带有Padding间距的Column,并且想要按照下图摆放它的两个子节点:
布局的过程就像下面的对话:
Widget: “Hey 父节点, 我的约束是什么”
父节点: “你的宽度必须在 80
到 300
像素范围内,你的高度必须在 80
到 300
范围内”
Widget: “Hmmm, 因为我想要有一个 5
像素的间距,那么我的子节点们最多宽 290
像素、高 75
像素”
Widget: “Hey 第一个子节点,你的宽度必须在0
到 290
像素内, 高度必须在 0
到 75
像素内”
First child: “OK, 我希望我的宽度是 290
像素,高度是 20
像素”
Widget: “Hmmm,因为我想把第二个子节点放在第一个子节点的下面,这样的话就只给第二个子节点留下了 55
像素的高度”
Widget: “Hey 第二个子节点, 你的宽度必须在 0
到 290
像素内, 高度必须在 0
到 55
像素内”
Second child: “OK, 我希望我的宽度是 140
像素, 高度是 30
像素”
Widget: “很棒. 第一个子节点的位置是x: 5
、 y: 5
, 第二个子节点的位置是 x: 80
、 y: 25
”
Widget: “Hey 父节点, 我已经决定了我的尺寸,宽度是 300
像素, 高度是 60
像素”
限制
由于上面提到的布局规则,Flutter布局引擎有几个重要的限制:
-
一个 Widget 只能在父节点给定的约束内决定字节的尺寸信息,这就意味着Widget通常不能有自己想要的尺寸信息
-
由于父节点决定了子节点的位置,所以一个 Widget 不知道也不能决定字节在屏幕上的位置
-
由于父节点的尺寸和位置信息也依赖它的父节点,所以如果不把组件树作为一个整体来考虑,就不可能精确地定义任何组件的大小和位置信息
-
If a child wants a different size from its parent and the parent doesn’t have enough information to align it, then the child’s size might be ignored. Be specific when defining alignment.
如果子节点想要的尺寸与父节点的给定的范围不同,而父节点又没有足够的信息去对齐摆放子节点,那么子节点的尺寸诉求就被忽略了。具体的尺寸信息要定义对齐信息
案例
DartPad 可以使用来运行案例代码。一共有29个案例,每一个案例都有编号。 👉案例代码
Example 1
Container(color: red)
屏幕是 Container
的父节点,屏幕强制 Container
的大小和screen相同。
因此, Container
使用红色充满了整个屏幕。
Example 2
Container(width: 100, height: 100, color: red)
Container
想要 100 × 100,但是没有按着这个想法。因为,屏幕强制 Container
的大小和screen相同。
因此, Container
仍然使用红色充满了整个屏幕。
Example 3
Center(
child: Container(width: 100, height: 100, color: red),
)
屏幕强制 Center
的尺寸是屏幕的尺寸,所以 Center
充满了整个屏幕。
Center
告诉 Container
,在屏幕范围内, Container
可以有任何它想要的尺寸。
所以,Container
可以做到了 100 × 100。
Example 4
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: red),
)
和前一个案例不同,这里使用 Align
代替了 Center
组件。
和前一个案例差不多,Align
也告诉 Container
在屏幕范围内,它可以是任何尺寸。相对于居中,在可用的范围内,Align
右下角对齐 Container
。
Example 5
Center(
child: Container(
width: double.infinity, height: double.infinity, color: red),
)
屏幕强制 Center
的尺寸是屏幕的尺寸,所以 Center
充满了整个屏幕。
Center
告诉 Container
,在屏幕范围内, Container
可以有任何它想要的尺寸。
Container
想要无限的尺寸信息,但是由于它不能超过屏幕。
所以:Container
仅仅填充了整个屏幕。
Example 6
Center(
child: Container(color: red),
)
屏幕强制 Center
的尺寸是屏幕的尺寸,所以 Center
充满了整个屏幕。
Center
告诉 Container
,在屏幕范围内, Container
可以有任何它想要的尺寸。由于Container
没有子节点,也没有固定的大小,Container
决定它想要尽可能大,所以它充满了整个屏幕。
但是为什么 Container
决定它尽可能大呢?因为这是组件设计者的想法💡。可以参考👉Container
文档
Example 7
Center(
child: Container(
color: red,
child: Container(color: green, width: 30, height: 30),
),
)
屏幕强制 Center
的尺寸是屏幕的尺寸,所以 Center
充满了整个屏幕。
Center
告诉 Container
,在屏幕范围内,Container
可以有任何它想要的尺寸。因为红色的Container
没有尺寸信息,但是有一个子节点。Container
决定它想要的尺寸和child一样。
红色的 Container
告诉它的子节点,在在屏幕范围内,它可以有任何它想要的尺寸。
绿色的 Container
想要的尺寸是 30 × 30。假设红色的 Container
将自身大小调整为其子元素的大小,所以它也是 30 × 30。
因为红色被绿色完全覆盖,红色的 Container
不可见。
Example 8
Center(
child: Container(
padding: const EdgeInsets.all(20.0),
color: red,
child: Container(color: green, width: 30, height: 30),
),
)
红色 Container
将自己的大小调整为子节点的大小,但它会考虑自己的间距。所以就是 30 × 30 加上四周的20间距,正是由于间距的存在,所以红色是可见的。
和前一个例子相似,绿色 Container
依然是 30 × 30。
Example 9
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 10, height: 10),
)
你也许会认为 Container
的范围是 70 到 150 像素,但是这是不对的。
ConstrainedBox
仅仅是对父布局传递的约束强加了额外的约束。
在这里,屏幕强制 ConstrainedBox
的尺寸是精确的屏幕尺寸,所以它告诉它的 Container
也是屏幕的尺寸,因此无视了 constraints
参数。
Example 10
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 10, height: 10),
),
)
现在,Center
让 ConstrainedBox
的尺寸是屏幕尺寸范围内。ConstrainedBox
强加了 额外 的约束给他的子节点。
Container
必须在70 到 150 像素范围内,它想要10像素,所以它只能是70像素。
Example 11
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 1000, height: 1000),
),
)
同样,Center
让 ConstrainedBox
的尺寸是屏幕尺寸范围内任意大小。ConstrainedBox
强加了额外的的 constraints
约束给它的子节点。
Container
必须在70到150的像素范围内,而它想要1000像素,因此它的尺寸就是150.
Example 12
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 100, height: 100),
),
)
同样,Center
让 ConstrainedBox
的尺寸是屏幕尺寸范围内任意大小。
ConstrainedBox
强加了额外的的 constraints
约束给它的子节点。
Container
必须在70到150的像素范围内,而它想要的100正好在范围内,所以它的尺寸就是100。
Example 13
UnconstrainedBox(
child: Container(color: red, width: 20, height: 50),
)
屏幕强制 UnconstrainedBox
的尺寸就是屏幕尺寸,但是 UnconstrainedBox
可以让它的子节点 Container
是任何子节点想要的尺寸。
所以就是20*50了。
Example 14
UnconstrainedBox(
child: Container(color: red, width: 4000, height: 50),
)
屏幕强制 UnconstrainedBox
的尺寸就是屏幕尺寸,并且UnconstrainedBox
可以让它的子节点 Container
的尺寸是任何子节点想要的尺寸。
但是在这里例子中,UnconstrainedBox
的子节点Container
想要的4000像素太大了,因此 UnconstrainedBox
展示了 “overflow warning” 警告。
Example 15
OverflowBox(
minWidth: 0.0,
minHeight: 0.0,
maxWidth: double.infinity,
maxHeight: double.infinity,
child: Container(color: red, width: 4000, height: 50),
)
屏幕强制 OverflowBox
的尺寸就是屏幕尺寸,而且OverflowBox
可以让它的子节点 Container
的尺寸是任何子节点想要的尺寸。
OverflowBox
和 UnconstrainedBox
十分相似,不同的是当子节点过大的时候, OverflowBox
不会显示上面的警告。
在这个例子里,Container
的宽度是4000像素,已经超过了 OverflowBox
的范围,但是 OverflowBox
会尽可能展示内容,不会显示警告。
Example 16
UnconstrainedBox(
child: Container(color: Colors.red, width: double.infinity, height: 100),
)
这里没有渲染任何组件,并且你在控制台也看到了错误信息。
UnconstrainedBox
让它的子节点 Container
的尺寸是任何子节点想要的尺寸。但是 Container
的尺寸是无限的。
Flutter 不会渲染无限的尺寸,因此它抛出了带有 BoxConstraints forces an infinite width.
信息的异常
Example 17
UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
),
),
)
这里你就不会看到错误信息了,因为在 UnconstrainedBox
中给定了 LimitedBox
组件。它会传递一个最大宽度——100像素给子节点。
如果将 UnconstrainedBox
替换为 Center
组件,LimitedBox
就不会再应用 limit 信息,因为只有约束是无限的时候,才会应用 limit 信息,并且Container
的宽度可以超过给定的100像素的限制。
这就解释了 LimitedBox
和 ConstrainedBox
的不同。
Example 18
const FittedBox(
child: Text('Some Example Text.'),
)
屏幕强制 FittedBox
的尺寸就是屏幕尺寸。Text
也会有一些自然宽度或者叫做固有宽度,这个宽度取决于文本的多少、字体的大小等等。
FittedBox
让 Text
的尺寸是任何它想要的尺寸,但是 Text
把自己的尺寸告诉 FittedBox
之后,FittedBox
会缩放 Text
,沾满整个可用的宽度。
Example 19
const Center(
child: FittedBox(
child: Text('Some Example Text.'),
),
)
当你把 FittedBox
放置在 Center
中会发生什么呢?Center
让 FittedBox
的尺寸是屏幕范围内的任何尺寸。
FittedBox
确定自己的大小为 Text
的大小,并且让 Text
的尺寸是 Text
想要尺寸。
因为 FittedBox
和 Text
的尺寸相同,所以就不会发生缩放了。
Example 20
const 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.'),
),
)
但是,假如 Center
组件内部的 FittedBox
子节点 Text
比屏幕还要宽,会怎么样呢?
FittedBox
确定自己的大小为 Text
的大小,但是它的大小超过了屏幕。然后它将大小设定为屏幕大小,并调整 Text
的大小,也让 Text
为屏幕宽度。
Example 21
const 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
组件的宽度就是屏幕宽度,文本组件会换行,所以也会适应屏幕尺寸,不会发生报错。
Example 22
FittedBox(
child: Container(
height: 20.0,
width: double.infinity,
color: Colors.red,
),
)
FittedBox
仅仅可以缩放非无限范围的组件,否则,它不会渲染任何东西,并且你在控制台中还会看到错误信息。
Example 23
Row(
children: [
Container(color: red, child: const Text('Hello!', style: big)),
Container(color: green, child: const Text('Goodbye!', style: big)),
],
)
屏幕强制 Row
组件的尺寸是屏幕的尺寸。
就像 UnconstrainedBox
一样, Row
不会添加约束给他的子节点,而是让子节点的尺寸就是他们想要的尺寸。然后 Row
组件会并排摆放子节点,剩余的空间就是空的。
Example 24
Row(
children: [
Container(
color: red,
child: const Text(
'This is a very long text that '
'won't fit the line.',
style: big,
),
),
Container(color: green, child: const Text('Goodbye!', style: big)),
],
)
因为 Row
不会强加任何约束给他的子节点,所以子节点可能非常大,导致无法适应 Row
的宽度。
在这个例子🌰中,就像 UnconstrainedBox
, Row
出现了 “overflow warning”警告。
Example 25
Row(
children: [
Expanded(
child: Center(
child: Container(
color: red,
child: const Text(
'This is a very long text that won't fit the line.',
style: big,
),
),
),
),
Container(color: green, child: const Text('Goodbye!', style: big)),
],
)
When a Row
’s child is wrapped in an Expanded
widget, the Row
won’t let this child define its own width anymore.
假如 Row
的子节点被 Expanded
组件包裹,那么 Row
就不会让子节点定义自己的尺寸了。
相反,Row
会根据其他的子节点来定义 Expanded
的宽度,只有这样 Expanded
才会强制被包裹的子节点的宽度和 Expanded
’s 宽度一样。也就是说,只要使用了 Expanded
,被包裹的子节点的原始宽度是无关紧要的了。
Example 26
Row(
children: [
Expanded(
child: Container(
color: red,
child: const Text(
'This is a very long text that won't fit the line.',
style: big,
),
),
),
Expanded(
child: Container(
color: green,
child: const Text(
'Goodbye!',
style: big,
),
),
),
],
)
如果 Row
的子节点都被 Expanded
包裹,每个 Expanded
的尺寸和它的 'flex' 参数相关。只有这样 Expanded
才会强制被包裹的子节点的宽度和 Expanded
’s 宽度一样,也就是说,Expanded
会无视子节点原始的宽度。
Example 27
Row(
children: [
Flexible(
child: Container(
color: red,
child: const Text(
'This is a very long text that won't fit the line.',
style: big,
),
),
),
Flexible(
child: Container(
color: green,
child: const Text(
'Goodbye!',
style: big,
),
),
),
],
)
Flexible
和 Expanded
的不同点是,Flexible
的宽度会大于等于被包裹的子节点的宽度,而 Expanded
会强制子节点的宽度就是Expanded
的宽度。相同点是,两者都会忽略子节点的宽度。
Example 28
Scaffold(
body: Container(
color: blue,
child: Column(
children: const [
Text('Hello!'),
Text('Goodbye!'),
],
),
),
)
屏幕强制 Scaffold
的尺寸就是屏幕尺寸。因此,Scaffold
占满了整个屏幕。Scaffold
告诉 Container
它可以是屏幕范围内的任意尺寸。
When a widget tells its child that it can be smaller than a certain size, we say the widget supplies loose constraints to its child. More on that later.
当一个 Widget 告诉它的子节点,它可以小于某个特定的尺寸时。我们就说这个 Widget 为子节点提供了 loose 的约束。
Example 29
Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: Column(
children: const [
Text('Hello!'),
Text('Goodbye!'),
],
),
),
),
)
如果你想让 Scaffold
的子节点的大小和 Scaffold
本身一样,你可以使用 SizedBox.expand
包裹它的子节点。
当一个 Widget 告诉它的子节点,子节点必须是准确的尺寸。我们就说这个 Widget 给它的子节点了一个 tight 的约束。
Tight vs. loose 约束
我们经常听到 tight 或者 loose 约束,那么这两个是什么意思呢?
tight约束提供了一种可能性:精确的尺寸。换句话说,在tight约束下,宽度和高度的最大值等于其最小值。在 Flutter 的源码中,可以找到 BoxConstraints
的一个构造方法:
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
再看一下上面的Example 2,我们提到屏幕强制红色 Container
的大小是屏幕大小,就是通过tight约束做到的,屏幕给 Container
的约束是最大值和最小值都是屏幕尺寸的 tight 约束。
另一方面,loose 约束,只是设定了最大的宽度和高度, 子节点只要比最大值就可以了。也就是说,loose 约束的宽度和高度的最小值是 zero:
BoxConstraints.loose(Size size)
: minWidth = 0.0,
maxWidth = size.width,
minHeight = 0.0,
maxHeight = size.height;
在看一下上面的Example 3,Center
让红色的 Container
不大于屏幕尺寸,能做到这一点就是,Center
传递了loose 约束给 Container
。最终,Center
从父节点获得了 tight 约束,将其转为 loose 约束,并将转化后的约束传递给了子节点。
了解特定 Widget 的布局规则
知道通用的布局规则是十分有必要的,但是止步于此,是远远不够的。
在运用通用规则的时候,每个 Widget 有很高的自由度。因此仅仅通过 Widget 的名字是无法知道它要干啥的。
如果仅仅是猜测,那么很可能会猜错。只有翻读源码和文档,才能准确的知道 Widget 背后的规则。
布局的源码一般是比较复杂的,因此读文档更好接受一点点🤏。但是如果你决定学习布局的源码,你可以通过IDE的导航功能来定位源码。
下面是例子:
-
找到
Column
的远嘛. To do this, 在Android Studio 或者 IntelliJ 中使用command+B
(macOS) 或者control+B
(Windows/Linux) 快捷键。你就能达到basic.dart
文件,因为Column
继承自Flex
,继续跟踪到Flex
源码(也在basic.dart
中)。 -
找到
createRenderObject()
方法。如你所见,方法返回了RenderFlex
。这就是Column
背后的render-object。现在跟踪到flex.dart
文件的RenderFlex
源码中。 -
找到
performLayout()
方法。这个方法就是Column
的布局规则方法。