如果有人问你Flutter中某个widget设置了属性width: 100
,但是显示的并不是100像素。通常最直接的答案就是把这个widget放到 Center
中。但这样真的对吗?
不要这样回答!
如果这样回答,那么后续别人会不停地来问为什么FittedBox
表现不正常,Column
为什么显示overflow了,或者IntrinsicWidth
是干什么用的?
正确的答案是,首先告诉他们Flutter的布局方式和HTML不同(如果对方碰巧是前端开发),然后告诉他们记住下面的规则:
约束向下传递。尺寸向上传递。父控件设置位置
理解上面这个规则是理解Flutter布局的关键,Flutter开发者应当尽早的理解上述规则。
详细说明:
- widget接收来自父widget的约束。约束由4个double组成:最大、最小宽度和最大、最小高度。
- widget遍历自己的子widget。将每个相应的约束传递给相应的子widget,然后询问每个子widget本身想要的大小。
- 然后,widget通过x,y坐标将子widget放到布局中
- 最后,widget告诉他的父widget自身的大小(当然,还有开始的布局约束)
举例,如果一个widget组合包含一个有padding的column里面有两个child:
那么整个布局和尺寸的确定流程如下:
- Column:“Hey,父widget,我的约束是多少?”
- Column的父Widget: “你的宽度是0-300像素,高度是0-85像素”
- Column:“我自身有5个像素的padding,那么我的child最多可以有290像素宽,75像素高”
- Column:“First Child,你的约束是0-290像素宽,0-75像素高”
- First Child:“好的,我自己想要的size是290像素宽,20像素高”
- Column:“我还有一个child,那么他的约束是0-290像素宽,55像素高(75-20 =55)”
- Column:“Second Child, 你的约束是0-290像素宽,0-55像素高”
- Second Child:“OK,我自己想要的size是140像素宽,30像素高”
-
Column:“好的,那么我的First Child的位置是
x:5,y:5
,我的Second Child的位置是x:80,y:25
” - Column:"我的父Widget,我确定了自身的大小是300像素宽,60像素高"
限制
Flutter的布局引擎被设计成单次完成布局。虽然这种方法效率非常高,但是也会存在一些限制:
- Widget只能根据父Widget给出的约束来确定自身的大小。这会导致widget无法完全按自身的逻辑设置自身的大小。
- Widget无法知道也无法决定自身在屏幕中的位置,因为只有父widget才能确定子widget的位置。
- 由于父Widget也依赖父父Widget确定自身的大小和位置,只有在遍历整个布局树后才能精确获取Widget的大小和位置。
- 如果子Widget的size和父Widget的size不同,父Widget又没有足够的信息来对齐子Widget,那么子Widget的size会被忽略。定义aligment的时候需要当心。
Flutter中,Widget由底层的RenderBox
渲染。Flutter中的许多box,特别是只有一个child的box,都会将约束传递给他们的child。
通常根据处理约束的方式不同分为三种box:
- 自身尽可能大的类型。例如
Center
和ListView
使用的box。 - 保持和children一样大的类型。例如
Transform
和Opacity
使用的box。 - 使用确定size的类型。例如
Image
和Text
使用的box。
有一些Widget比如Container
,会根据构造函数传参的不同产生不同的约束处理方式。比如,如果使用默认构造函数,Container
会使用自身尽可能大的类型,但是如果传了width、height
那么会尽量使用确定size类型。
举例
接下来将通过29个例子来具体分析之前讲的理论。不用担心例子过多,例子之间的逻辑关联最终会串联他们之间的关系并推导出简洁的结论。
例1
Container(color: red)
屏幕尺寸就是Container
的父Widget尺寸,所以Container
和屏幕的size一样大。
例2
Container(width: 100, height: 100, color: red)
红色的Container
想要自身大小为100*100,但是实际的大小被限制为屏幕大小。
例3
Center(child: Container(width: 100, height: 100, color: red))
屏幕限制Center
为屏幕大小,所以Center
占满屏幕。
Center
限制Container
为任何大小,但是不要超过屏幕。这时,Container
大小是100*100。
例4
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: red),
)
这里和前一个例子用Center
不同的是使用了Align
。
Align
和Center
类似,Container
可以是任何大小 。但是布局在父容器的右下角。
例5
Center(
child: Container(
width: double.infinity,
height: double.infinity,
color: red,
),
)
屏幕限制Center
为屏幕大小,所以Center
占满屏幕。
Center
限制Container
可以是不超过屏幕size的任何size。Container
想要无限大,但是受到不超过屏幕size的限制,所以只能是屏幕size。
Example6
Center(child: Container(color: red))
屏幕限制Center
的大小为屏幕尺寸,所以Center
占满屏幕。
Center
限制Container
的大小为任何不超过屏幕的大小。因为Container
没有限制大小也没有任何child,所以这里会尽可能大,占满屏幕。
例7
Center(
child: Container(
color: red,
child: Container(color: green, width: 30, height: 30),
),
)
屏幕限制Center
的大小为屏幕尺寸,所以Center
占满屏幕。
Center
限制Container
的大小为任何不超过屏幕的大小。Container
没有指定的size但是有一个child,所以会把自身的大小设置为child的大小。
红色的Container
限制child可以是不超过屏幕的任何大小。
绿色的Container
child大小是30X30。所以红色的Container
大小是30X30。这里红色的Container
不可见是因为被绿色的Container
正好盖住了。
例8
Center(
child: Container(
padding: const EdgeInsets.all(20),
color: red,
child: Container(color: green, width: 30, height: 30),
),
)
红色的Container
的size就是child的size加上padding。即size是70X70。
例9
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 10, height: 10),
)
根据代码字面上的意思是Container
的size是在70X70到150X150之间,但是这是错误的。ConstrainedBox
的constraints参数是在父Widget传来的约束上附加的额外约束。
这里ConstrainedBox
大小是屏幕的大小,所以它传递给child Container
的约束也是屏幕的大小,所以constraints
参数被忽略了。
例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
从constraints
中获取附加限制并传递给它的child。
Container
的长宽都必须在70-150像素之间。自身想要的大小是10X10,最终结果是70X70。
例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
中获取附加限制并传递给它的child。
Container
的长宽都必须在70-150像素之间。自身想要的大小是1000X1000,最终结果是150X150。
例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
中获取附加限制并传递给它的child。
Container
的长宽都必须在70-150像素之间。自身想要的大小是100X100,在限制范围内,最终结果是100X100。
例13
UnconstrainedBox(
child: Container(color: red, width: 20, height: 50),
)
UnconstrainedBox
的大小是屏幕大小。但是UnconstrainedBox
的child可以是任何大小。
例14
UnconstrainedBox(
child: Container(color: red, width: 4000, height: 50),
)
UnconstrainedBox
的大小是屏幕大小。UnconstrainedBox
的child Container
可以是任何大小。
但是这里Container
宽度是4000像素,超过了UnconstrainedBox
的大小,所以UnconstrainedBox
显示了“overflow warining”。
例15
OverflowBox(
minWidth: 0,
minHeight: 0,
maxWidth: double.infinity,
maxHeight: double.infinity,
child: Container(color: red, width: 4000, height: 50),
)
OverflowBox
的大小为屏幕的大小,OverflowBox
的child可以是任何大小。
OverflowBox
和UnconstrainedBox
很像,区别是OverflowBox
的child如果size超过了OverflowBox
的大小不会显示overflow警告。
例16
UnconstrainedBox(
child: Container(color: Colors.red, width: double.infinity, height: 100),
)
这里不会渲染任何内容,并且会在console中看到错误log输出。
UnconstrainedBox
的child可以是任何大小,但是child的宽度是无限。Flutter不能渲染无限大小的Widget,所以会抛出BoxConstraints forces an infinite width
异常。
例17
UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
),
),
)
和例16相比,这里不会报错,因为LimitedBox
在UnconstrainedBox
限制上给出了一个有限的限制,宽度最多100。
如果 用 Center
取代UnconstrainedBox
,LimitedBox
不会再附加任何限制,因为LimitedBox
收到了一个有限制的约束,LimitedBox
本身的限制不会生效,所以Container
的宽度允许超过100。
这里显示了UnconstrainedBox
和 LimitedBox
区别。
例18
const FittedBox(child: Text('Some Example Text.'))
FittedBox
的大小是屏幕的大小。Text
的大小由自身显示内容及性质决定。
FittedBox
对于Text
的大小没有限制,但是在Text
确定了大小后,FittedBox
会缩放Text
直到占满剩下空间。
例19
const Center(child: FittedBox(child: Text('Some Example Text.')))
如果将FittedBox
放到 Center
里面呢? Center
限制FittedBox
的最大大小是屏幕大小。
FittedBox
根据Text
的size确定自身的size。这样FittedBox
和 Text
大小相同,无需缩放。
例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.',
),
),
)
如果FittedBox
在 Center
里面,但是Text
的内容非常长呢?
FittedBox
尝试根据Text
的大小来设置自身的大小,但是不能超过屏幕的大小。最终采用的是屏幕的size来resize Text
的大小。
例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
的宽度限制就是屏幕的宽度,所以发生了换行来适配屏幕的宽度。
例22
FittedBox(
child: Container(height: 20, width: double.infinity, color: Colors.red),
)
FittedBox
只能缩放有固定宽高的widget,否则不会渲染内容,并在console中输出错误。
例23
Row(
children: [
Container(color: red, child: const Text('Hello!', style: big)),
Container(color: green, child: const Text('Goodbye!', style: big)),
],
)
Row
的大小就是屏幕的大小。和UnconstrainedBox
一样,Row
不会限制child的大小,所以child可以是自身想要的任何大小。
例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
不会给child任何限制,所以child很容易就因为太长超出Row
的宽度。所以这里和 UnconstrainedBox
一样,显示了"overflow warning"。
例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)),
],
)
当Row
的child被Expanded
包裹的时候,Row
就不会让child自身决定自身的大小了。这时,会根据其他child的大小来确定Expanded
的宽度,然后Expanded
会强制被包裹的child使用Expanded
的大小。
换句话说,如果使用了Expanded
,内部包裹的child的宽度会被忽略。
例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
所有的child都用Expanded
包裹,那么每个Expanded
的size和它的flex参数大小正比,并且就是每个Expanded
包裹的child大小。换言之,Expanded
忽略了child自身的大小。
例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
,这样child的宽度就可以小于等于Flexible
的宽度,而不是像Expanded
一样强制child的宽度和自己一样。但是Flexible
和Expanded
在测量自身宽度的时候都会忽略child的宽度。
例28
Scaffold(
body: Container(
color: blue,
child: const Column(children: [Text('Hello!'), Text('Goodbye!')]),
),
)
Scaffold
被强制使用屏幕的大小,所以Scaffold
会占满屏幕。Scaffold
给Container
的限制是不超过屏幕的任何大小。
注意
当约束是不超过某个确定的大小时,就是所谓的loose constraints。
例29
Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: const Column(children: [Text('Hello!'), Text('Goodbye!')]),
),
),
)
如果想要Scaffold
的child和Scaffold
大小一样,那么可以使用SizedBox.expand
包裹这个child。
Tight vs loose约束
Tight 约束
所谓的tight约束就是有精确的大小。换句话说就是tight约束的最大宽/高等于最小宽/高。
最常见的例子就是包含在RenderView
类中的App
widget:应用的build
方法返回的box,使用的就是tight约束,大小就是屏幕的大小。
另一个例子是,如果你在应用的render tree的root中嵌套box,每个box都被强制使用tight 约束,这样就能完全填充。
如果看一下box.dart
的源码,搜索BoxConstraints
构造函数:
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
和前面讲的例2一样,屏幕强制Container
使用屏幕的大小,所以Container
的大小被忽略了。
Loose 约束
loose约束就是最大宽/高不等于0,最小宽/高等于0。
例如Center
这样的box,就会loose从父控件收到的约束。例3中,Center
的child Container
就可以小于Center
的大小(但是也不能超过屏幕传递个Center的大小)。
Unbounded 约束
在某些情况下,box的约束是unbounded 或者infinite 的。也就是说宽或者高被设为 double.infinity
。
如果一个自身为尽可能大的box的约束是Unbounded约束,那么在debug 模式下,会抛出异常。
最常见的例子就是在flex box(例如Row或者Column)或者可滚动区域(例如LIstView或者其他ScrollView子类)里面有一个Unbounded约束的render box。
Flex
具体到Flex box(例如Row或者Column)Unbounded约束的影响还取决于是否在发生在主方向上(Row的宽,Column的高)。
Flex box在bounded约束的情况下,在主方向上尽可能大。
Flex box在Unbounded约束的情况下,会尽可能满足child在这个方向上的大小。每个child的flex 值都需要被设置为0,也就是说无法在Unbounded约束的flex box或者scrollable里使用Expanded,否则会抛出异常。
Flex box的交叉方向(Row的高,Column的宽)必须是bounded,否则无法布局内部的child。