原文在这里。
本问现在是官方文档的一部分了。此文绝对值一看。Flutter有着非常不同的布局体系,本文有非常丰富的例子,从总体到某些细节都做了讲解。而且,作为初学者看到了很多完全没见过的布局组件。
当Fluter初学者问你为什么组件里的width:100不是100像素的时候,默认的答案就是告诉他们把组件放进一个Center里,对吧?
不要这么干。
如果你这么干了,他们会一次一次的问你为什么FittedBox有问题,为什么Column会overflow,又或者IntrinsicWidth是做什么的。
所以,一开始就告诉他们Flutter的布局和html有很大的不同,他们很可能就是html的高手,然后让他们记住以下的规则:
👉 约束(Constraint)向下,大小(Size)向上,位置父决定
不理解这个规则,Flutter的布局是没法弄清楚的。所以,我(作者)觉得最好今早的学会它。
细节:
- 一个组件都是从它的父组件获得约束(constraint)。一个约束就是四个double值:一个最小、最大宽度和一个最小、最大高度。
- 然后,这个组件遍历它的子组件。一个个的通知它的子组件他们的约束(每个子组件都可能不一样),然后询问他们想要的size。
- 然后,这个组件沿着横向的x轴和纵向的y轴排列它的子组件的位置。
- 最后,每个组件告诉它的父组件它自己在约束下的size。
比如一个Column组件,已经设定了padding值,现在要给它的两个子组件设定布局:
组件 -- 询问父组件约束是啥。 父组件 -- 你只能是
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的位置。 组件 -- 嗨,父组件。我的size是300宽,60高。
限制(Limitation)
Flutter布局引擎在上面规则的基础上还有一些其他的限制:
- 一个组件只可以在父组件传过来的约束的范围内确定它的大小(size)。也就是说,一般一个组件不能想多大就多大。
- 一个组件不知道,也不能决定它在屏幕上的位置。组件的位置是由它的父组件决定的。
- 父组件的大小和位置也是由它的父组件决定的,只有在树的概念下才能决定一个组件的大小和位置。
示例
下面是一个互动示例。
原文提到了CodePen,也可以在以下两种方法里选一种。
- 使用DartPad.
- 代码的github repo
示例 1
Container(color: Colors.red);
屏幕是Container的父组件,它会把红色的Container严丝合缝的约束在整个的屏幕内部。
所以,Container填满了整个屏幕,到处都是红色。
示例 2
Container(color: Colors.red, width: 100, height: 100)
Container想要宽100,高100,但是不行。屏幕会强制它填满屏幕。
所以Container填满了屏幕。
示例 3
屏幕强制Center填满整个屏幕,所以Center显示在全屏。
Center告诉Container可以拥有想要的大小,但是不能比屏幕还大。所以,Container的大小就是100x100。
示例 4
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: Colors.red)
)
这和前一个例子并不一样,这里用的是Align而不是Center。
Align也会告诉Container可以任意大小,但是如果有任意的可用空间,是不让Container居中的,它会把Contaienr放在右下角。
示例 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会告诉Container可以为任意大小,但是不能比屏幕大。因为Container没有子组件,也没有固定的大小。它会决定显示为尽可能的大,所以填充了屏幕。
但是,为什么Container要这么决定呢?这是设计决定的。所以,Container在这样的情况下会如何显示,你要查看文档。
示例 7
Center(
child: Container(
color: Colors.red,
child: Container(color: Colors.green, width: 30, height: 30),
)
)
Center会填充屏幕。
Center会告诉Container可以任意大小。Container没有大小,但是有一个子组件,所以它决定和它的子组件一样大小。
红色的Container告诉它的子组件可以为任意大小,但是不能比屏幕还大。
绿色的Container想要30 x 30。就像上面说的,红色的Container就会显示为绿色的Container的大小,也是30x30。没有红色可以显示出来,因为绿色的把红色全部覆盖住了。
示例 8
Center(
child: Container(
color: Colors.red,
padding: const EdgeInsets.all(20.0),
child: Container(color: Colors.green, width: 30, height: 30),
)
)
红色的Container会显示为其子组件的大小,但是它自己还有padding所以它本身的大小是70x70(=30x30 + 20的padding值)。最后红色因为有padding值是可见的,绿色Container和上例一样有30x30的大小。
示例 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显示到屏幕的大小,所以constaints参数的值都被忽略了。
示例 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之间,Container虽然设定为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允许ConstraintedBox是屏幕内的任意大小。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会对子组件施加额外的约束。
所以,Container必须是70到150之间的值。它设定的值是100,所以它就会有这个值,因为它是在70到150之间的。
示例 13
UnconstrainedBox(
child: Container(color: Colors.red, width: 20, height: 50),
)
屏幕强制UnconstrainedBox拥有和屏幕一样的大小。而UnconstrainedBox允许它的子组件有任意大小。
示例 14
UnconstrainedBox(
child: Container(color: Colors.red, width: 4000, height: 50),
);
屏幕强制UnconstrainedBox和屏幕一样大小,而UnconstrainedBox让它的Container子组件拥有任意大小。
但是,本例中Container设定的是4000的宽,这样太大了没法放进UnconstrainedBox,所以UnconstrainedBox会显示出“overflow warning”。
示例 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类似,不同的地方是,如果子组件比它大的话不会包warning。
在本例中Container的宽是4000,太大了。但是OverflowBox在这里就不会像上例的UnconstrainedBox一样报警。
示例 16
UnconstrainedBox(
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
它不会绘制出任何的东西,只会在console里报错。
UnconstrainedBox让它的子组件可以拥有任意大小,然而它的子组件的宽是double.infinity。
Flutter没法绘制无限宽的大小,所以它会抛出一个错误:BoxConstraints forces an infinite width。
示例 17
UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
)
示例 18
FittedBox(
child: Text('Some Example Text.'),
)
屏幕强制FittedBox和屏幕同样大小。Text会有自己的宽度(也叫做intrinsic宽度)。这个值依赖于字体和文字的多少等。
FittedBox会让Text拥有任意的大小,但是Text把它自己的大小通知FittedBox之后,FittedBox会做缩放,直到填满整个的宽度。
示例 19
Center(
child: FittedBox(
child: Text('Some Example Text.'),
)
)
但是,如果把FittedBox放在Center里面的话会发生什么呢?Center会让FittedBox拥有任意它想要的大小。
FittedBox然后会把自己的大小缩放到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里,而且文字内容多到屏幕放不下的时候会发生什么呢?
FittedBox会缩放到适应Text的大小,但是它不可能比屏幕还大。它会首先占用屏幕的大小,然后对Text缩放到可以显示在屏幕里。
示例 21
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就会使用屏幕的宽度,然后折行来适应这个宽度。
示例 22
FittedBox(
child: Container(
height: 20.0,
width: double.infinity,
)
)
注意FittedBox只能缩放一个有边界的组件(宽度或者高度都没有无限值)。否则它不会绘任何的东西,只会在console里显示一条报错信息。
示例 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一样显示错误信息“overflow warning”。
示例 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包裹的话,那么这个组件的宽度就不会再起作用了。它会使用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包裹,那么每个子组件所占的比例是由flex参数决定的。每个子组件租后都会接受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都会忽略子组件的大小。
也就是说在Row里不可能根据子组件的大小按比例缩放子组件。Row要么使用子组件的宽,要么在你使用了Expanded或者Flexible的时候完全忽略他们的宽度。
示例 28
屏幕强制Scaffold显示到屏幕的大小。所以,Scaffold填充了屏幕。
Scaffold告诉Container它可以使屏幕内的任意大小。
注意:当一个组件告诉它的子组件比某个特定的值小,我们可以认为这个组件给它的子组件提供了“松散”的约束。后面会有更详细讲解。
示例 29
Scaffold(
body: **SizedBox.expand**(
child: Container(
color: blue,
child: Column(
children: \[
Text('Hello!'),
Text('Goodbye!'),
],
))))
如果我们要Scaffold的子组件和它有相同的大小。我们可以把它的子组件放到SizedBox.expand。
注意:当一个组件告诉它的子组件必须是某个特定的值,我们可以说这个组件给它的子组件提供了“紧”约束。
紧的和松散的约束
经常会听到一种说法“紧”或者“松散”的约束,那么他们到底指的是什么呢?
一个紧约束只提供了一种可能,一个准确的值。也就是说,一个紧约束,它的最大宽和最小宽是一样的,最大高和最小高也是一样的。
如果你到Flutter的box.dart文件查找BoxConstraints构造函数,你会发现:
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
如果你再看示例 2,屏幕强制红色的Container显示到屏幕的大小。屏幕可以这么做就是给Container传入了紧约束。
一个松散的约束是指给子组件设定最大宽和高,但是让子组件尽可能的小。也就是说,一个松散约束有最小的宽和高,他们都等于0.
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,然后跳转到它的源码。最终你会到basic.dart文件。Column继承自Flex,还可以跳转到Flex,它也在basic.dart文件里。 - 往下找到一个叫做
createRenderObject的方法。这个方法返回RenderFlex。这就是Column对应的绘制对象。再跳转到RenderFlex的源码,你就会到flex.dart文件。 - 往下找到~performLayout
的方法,这个方法就是为Column`布局的方法。
非常感谢Simon Lightfoot的校验,文章开头的图片和对文章内容的建议。