本文主要是针对flutter中布局约束原理的一个总结,以及对其29个展示样例做出分析。
flutter英文文档:https://docs.flutter.dev/ui/layout/constraints
flutter中文文档:https://docs.flutter.cn/ui/layout/constraints
我们会经常听到一些开发者在学习 Flutter 时的疑惑:为什么我设置了 width:100
,但是看上去却不是 100 像素宽呢。(注意,本文中的“像素”均指的是逻辑像素)通常你会回答,将这个 Widget 放进 Center
中,对吧?
如果你这样做了,他们会不断找你询问这样的问题:为什么 FittedBox
又不起作用了?为什么 Column
又溢出边界,亦或是 IntrinsicWidth
应该做什么。
其实我们首先应该做的,是告诉他们 Flutter 的布局方式与 HTML 的布局差异相当大(这些开发者很可能是 Web 开发),然后要让他们熟记这条规则:
Constraints go down. Sizes go up. Parent sets position.
向下传递约束、向上传递尺寸、父widget决定子widget的位置。
-
官方示例:
那么谈判将会像这样:
Widget: "嘿!我的父级。我的约束是多少?"
Parent: "你的宽度必须在
0
到300
像素之间,高度必须在0
到85
之间。"Widget: "嗯...我想要
5
个像素的内边距,这样我的子级能最多拥有290
个像素宽度和75
个像素高度。"Widget: "嘿,我的第一个子级,你的宽度必须要在
0
到290
,长度在0
到75
之间。"First child: "OK,那我想要
290
像素的宽度,20
个像素的长度。"Widget: "嗯...由于我想要将我的第二个子级放在第一个子级下面,所以我们仅剩
55
个像素的高度给第二个子级了。"Widget: "嘿,我的第二个子级,你的宽度必须要在
0
到290
,长度在0
到55
之间。"Second child: "OK,那我想要
140
像素的宽度,30
个像素的长度。"Widget: "很好。我的第一个子级将被放在
x: 5
&y: 5
的位置,而我的第二个子级将在x: 80
&y: 25
的位置。"Widget: "嘿,我的父级,我决定我的大小为
300
像素宽度,60
像素高度。"
首先了解两个概念
Terminology
When the minimum constraints and the maximum constraint in an axis are the same, that axis is tightly constrained.
An axis with a minimum constraint of 0.0 is loose (regardless of the maximum constraint; if it is also 0.0, then the axis is simultaneously tight and loose!).
原文地址: https://api.flutter.dev/flutter/rendering/BoxConstraints-class.html
严格约束 (Tight) 与宽松约束 (loose),我们通常也叫 紧约束、松约束。
严格约束:它的最大/最小宽度一样,最大/最小高度一样。
宽松约束:约束的最小宽度/高度为 0。
//紧约束
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
//松约束
BoxConstraints.loose(Size size)
: minWidth = 0.0,
maxWidth = size.width,
minHeight = 0.0,
maxHeight = size.height;
注意
:松约束、紧约束并不是互斥的,当只要满足对应条件即可。比如当一个约束的最大最小约束均为0时,那么这个约束即是松约束,同时也是紧约束。
当父级widget是一个紧约束时,无论子widget设置宽高值时多少,都会强制子widget宽高为紧约束的值。
当父级widget是一个松约束时,当widget设置的宽高值在约束条件内时,宽高值生效。
获取和设置约束条件
-
获取约束条件
通过对widget包裹一层
LayoutBuilder
,可以获取父级对其约束条件,用法:
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
print("constraints = ${constraints}");//打印约束条件
return Container(color: Colors.red);
},
)
//可能输出结果:
//紧约束
flutter: constraints = BoxConstraints(w=393.0, h=852.0)
//松约束
flutter: constraints = BoxConstraints(0.0<=w<=393.0, 0.0<=h<=852.0)
// w:松约束 h:无限约束
flutter: constraints = BoxConstraints(0.0<=w<=393.0, 0.0<=h<=Infinity)
-
设置约束条件
在widget外面包裹
ConstrainedBox
,通过 constraints 属性进行设置,
ConstrainedBox(
constraints: BoxConstraints(
minWidth: 10,
maxWidth: 100,
minHeight: 10,
maxHeight: 100,
),
child: Container(color: Colors.red),
)
Container 源码
- Container源码分析
class Container extends StatelessWidget {
Container({
super.key,
this.alignment,
this.padding,
this.color,
this.decoration,
this.foregroundDecoration,
double? width,
double? height,
BoxConstraints? constraints,
this.margin,
this.transform,
this.transformAlignment,
this.child,
this.clipBehavior = Clip.none,
}) :
//当宽或高不为空时,重新对constraints进行赋值
constraints =
(width != null || height != null)
? constraints?.tighten(width: width, height: height)
?? BoxConstraints.tightFor(width: width, height: height)
: constraints;
......
@override
Widget build(BuildContext context) {
Widget? current = child;
//当child为空,约束为空或着是一个紧约束时,设置约束为BoxConstraints.expand()
if (child == null && (constraints == null || !constraints!.isTight)) {
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
} else if (alignment != null) {
current = Align(alignment: alignment!, child: current);
}
......
//设置颜色
if (color != null) {
current = ColoredBox(color: color!, child: current);
}
......
//约束条件
if (constraints != null) {
current = ConstrainedBox(constraints: constraints!, child: current);
}
......
return current!;
}
}
- BoxConstraints.expand
const BoxConstraints.expand({
double? width,
double? height,
}) : minWidth = width ?? double.infinity,
maxWidth = width ?? double.infinity,
minHeight = height ?? double.infinity,
maxHeight = height ?? double.infinity;
样例分析
- 样例1
Container(color: Colors.red)
整个屏幕(root)作为 Container
的父级,并且强制 Container
变成和屏幕一样的大小(紧约束)。
所以这个 Container
充满了整个屏幕,并绘制成红色。
本质原因:
因为Container的父级对其是一个紧约束。强制要求width、height位屏幕尺寸大小。可以通过下面样例1数结构图可以知,也可在Container
外面包裹LayoutBuilder
后打印约束条件。
- 样例2
Container(
width: 100,
height: 100,
color: Colors.red,
)
红色的 Container
想要变成 100 x 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
放在右下角(bottomRight)。
[图片上传失败...(image-3e78ee-1721805129145)]
- 样例5
Center(
child: Container(
width: double.infinity, height: double.infinity, color: Colors.red),
)
屏幕强制 Center
变得和屏幕一样大,所以 Center
充满屏幕。
然后 Center
告诉 Container
可以变成任意大小(松约束),但是不能超出屏幕。现在,Container
想要无限的大小,但是由于它不能比屏幕更大,所以就仅充满屏幕。
[图片上传失败...(image-d0345a-1721805129145)]
- 样例6
Center(
child: Container(color: Colors.red),
)
屏幕强制 Center
变得和屏幕一样大,所以 Center
充满屏幕。
然后 Center
告诉 Container
可以变成任意大小,但是不能超出屏幕。由于 Container
没有子级而且没有固定大小,所以它决定能有多大就有多大,所以它充满了整个屏幕。
因为Container源码中,如果child == null 且 constraints == null时,设置了其约束为BoxConstraints.expand()
- 样例7
Center(
child: Container(
color: Colors.red,
child: Container(color: Colors.green, width: 30, height: 30),
),
)
屏幕强制 Center
变得和屏幕一样大,所以 Center
充满屏幕。
然后 Center
告诉红色的 Container
可以变成任意大小,但是不能超出屏幕。由于 Container
没有固定大小但是有子级,所以它决定变成它 child 的大小。
然后红色的 Container
告诉它的 child 可以变成任意大小,但是不能超出屏幕。
而它的 child 是一个想要 30 × 30 大小绿色的 Container
。由于红色的 Container
和其子级一样大,所以也变为 30 × 30。由于绿色的 Container
完全覆盖了红色 Container
,所以你看不见它了。
- 样例8
Center(
child: Container(
padding: const EdgeInsets.all(20),
color: Colors.red,
child: Container(color: Colors.green, width: 30, height: 30),
),
)
红色 Container
变为其子级的大小,但是它将其 padding 带入了约束的计算中。所以它有一个 20 x 20 的外边距。由于这个外边距,所以现在你能看见红色了。而绿色的 Container
则还是和之前一样。
[图片上传失败...(image-3c2d30-1721805129145)]
- 样例9
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 10, height: 10),
)
你可能会猜想 Container
的尺寸会在 70 到 150 像素之间,但并不是这样。 ConstrainedBox
仅对其从其父级接收到的约束下施加其他约束。
在这里,root 对 ConstrainedBox
是一个紧约束,使其与屏幕大小完全相同,因此它告诉其子 Widget
也以屏幕大小作为约束,从而忽略了其 constraints
参数带来的影响。
[图片上传失败...(image-6d1963-1721805129145)]
- 样例10
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 10, height: 10),
),
)
外部添加 Center
后,Center
对ConstrainedBox
的约束变成了松约束,这样对ConstrainedBox
设置的约束满足父级的约束,ConstrainedBox
将 constraints
参数带来的约束附加到其子对象上。
ConstrainedBox
对 Container
约束,要求 Container
必须介于 70 到 150 像素之间。虽然它希望自己有 10 个像素大小,但最终获得了 70 个像素(最小为 70)。
- 样例11
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 1000, height: 1000),
),
)
同样例10
- 样例12
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 100, height: 100),
),
)
同样例10
- 样例13
UnconstrainedBox(
child: Container(color: Colors.red, width: 20, height: 50),
)
屏幕强制 UnconstrainedBox
变得和屏幕一样大,而 UnconstrainedBox
允许其子级的 Container
可以变为任意大小,对子级的约束条件是无约束 BoxConstraints(unconstrained)
;
他是一个松约束(BoxConstraints( minWidth: 0, minHeight: 0, maxWidth: Infinity, maxHeight: Infinity))
。
[图片上传失败...(image-af1583-1721805129145)]
- 样例14
UnconstrainedBox(
child: Container(color: Colors.red, width: 4000, height: 50),
)
屏幕强制 UnconstrainedBox
变得和屏幕一样大,而 UnconstrainedBox
允许其子级的 Container
可以变为任意大小。
不幸的是,在这种情况下,容器的宽度为 4000 像素,这实在是太大,以至于无法容纳在 UnconstrainedBox
中,因此 UnconstrainedBox
将显示溢出警告(overflow warning)。
[图片上传失败...(image-517937-1721805129145)]
- 样例15
OverflowBox(
minWidth: 0,
minHeight: 0,
maxWidth: double.infinity,
maxHeight: double.infinity,
child: Container(color: Colors.red, width: 4000, height: 50),
)
屏幕强制 OverflowBox
变得和屏幕一样大,并且 OverflowBox
允许其子容器设置为任意大小。
OverflowBox
与 UnconstrainedBox
类似,但不同的是,如果其子级超出该空间,它将不会显示任何警告。
在这种情况下,容器的宽度为 4000 像素,并且太大而无法容纳在 OverflowBox
中,但是 OverflowBox
会全部显示,而不会发出警告。
[图片上传失败...(image-c61c4e-1721805129145)]
- 样例16
UnconstrainedBox(
child: Container(color: Colors.red, width: double.infinity, height: 100),
)
这将不会渲染任何东西,而且你能在控制台看到错误信息。
UnconstrainedBox
让它的子级决定成为任何大小,但是其子级是一个具有无限大小的 Container
。
Flutter 无法渲染无限大的东西,所以它抛出以下错误: BoxConstraints forces an infinite width.
(盒子约束强制使用了无限的宽度)
[图片上传失败...(image-bbe9f2-1721805129145)]
- 样例17
UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
),
),
)
这次你就不会遇到报错了。 UnconstrainedBox
给 LimitedBox
一个无限的大小;但它向其子级传递了最大为 100 的约束。
如果你将 UnconstrainedBox
替换为 Center
,则LimitedBox
将不再应用其限制(因为其限制仅在获得无限约束时才适用),并且容器的宽度允许超过 100。
上面的样例解释了 LimitedBox
和 ConstrainedBox
之间的区别。
[图片上传失败...(image-178c6-1721805129145)]
- 样例18
const FittedBox(
child: Text(
'Some Example Text.',
textDirection: TextDirection.ltr,
),
)
屏幕强制 FittedBox
变得和屏幕一样大,而 Text
则是有一个自然宽度(也被称作 intrinsic 宽度),它取决于文本数量,字体大小等因素。
FittedBox
让 Text
可以变为任意大小。但是在 Text
告诉 FittedBox
其大小后, FittedBox
缩放文本直到填满所有可用宽度。
主要原因,FittedBox对子级widget的约束是无限制约束,当子级widget渲染后尺寸,不满足FittedBox的约束条件时,就会产生缩放。
- 样例19
const Center(
child: FittedBox(
child: Text(
'Some Example Text.',
textDirection: TextDirection.ltr,
),
),
)
但如果你将 FittedBox
放进 Center
widget 中会发生什么? Center
将会让 FittedBox
能够变为任意大小,取决于屏幕大小。
FittedBox
然后会根据 Text
调整自己的大小,然后让 Text
可以变为所需的任意大小,由于二者具有同一大小,因此不会发生缩放。
[图片上传失败...(image-34a04f-1721805129145)]
- 样例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.',
textDirection: TextDirection.ltr,
),
),
)
然而,如果 FittedBox
位于 Center
中,但 Text
太大而超出屏幕,会发生什么?
FittedBox 会尝试根据 Text
大小调整大小,但不能大于屏幕大小。然后假定屏幕大小,并调整 Text
的大小以使其也适合屏幕。
- 样例21
const Center(
child: Text(
textDirection: TextDirection.ltr,
'This is some very very very large text that is too big to fit a regular screen in a single line.'),
)
然而,如果你删除了 FittedBox, Text 则会从屏幕上获取其最大宽度,并在合适的地方换行。
[图片上传失败...(image-5cbd7f-1721805129145)]
- 样例22
FittedBox(
child: Container(
height: 20,
width: double.infinity,
color: Colors.red,
),
)
FittedBox
只能在有限制的宽高中对子 widget 进行缩放(宽度和高度不会变得无限大)。否则,它将无法渲染任何内容,并且你会在控制台中看到错误。
[图片上传失败...(image-df78a0-1721805129145)]
- 样例23
Directionality(
textDirection: TextDirection.ltr,
child: Row(
children: [
Container(
color: Colors.red,
child: const Text(
'Hello!',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Container(
color: Colors.green,
child: const Text(
'Goodbye!',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
)
屏幕强制 Row
变得和屏幕一样大,所以 Row
充满屏幕。
和 UnconstrainedBox
一样, Row
也不会对其子代施加任何约束,而是让它们成为所需的任意大小。 Row
然后将它们并排放置,任何多余的空间都将保持空白。
[图片上传失败...(image-7acd51-1721805129145)]
- 样例24
Directionality(
textDirection: TextDirection.ltr,
child: Row(
children: [
Container(
color: Colors.red,
child: const Text(
'This is a very long text that '
'This is a very long text that '
'won't fit the line.',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Container(
color: Colors.green,
child: const Text(
'Goodbye!',
style: TextStyle(fontWeight: FontWeight.bold),
)),
],
),
)
由于 ,并不是不施加约束,而是对子级宽度不限制,高度为父级对其约束的高度。Row
不会对其子级施加任何约束
因此它的 children 很有可能太大而超出 Row
的可用宽度。在这种情况下, Row
会和 UnconstrainedBox
一样显示溢出警告。
[图片上传失败...(image-9fe202-1721805129145)]
- 样例25
Directionality(
textDirection: TextDirection.ltr,
child: Row(
children: [
Expanded(
child: Center(
child: Container(
color: Colors.red,
child: const Text(
'This is a very long text that won't fit the line.'
'This is a very long text that won't fit the line.',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
),
Container(
color: Colors.green,
child: const Text(
'Goodbye!',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
)
当 Row
的子级被包裹在了 Expanded
widget 之后, Row
将不会再让其决定自身的宽度了。
取而代之的是,Row
会根据所有 Expanded
的子级来计算其该有的宽度。
换句话说,一旦你使用 Expanded
,子级自身的宽度就变得无关紧要,直接会被忽略掉。
这是因为 Expanded
是 FlexFit.tight
,是一个紧约束,其实这里有些特殊,这里是仅限制宽度是一个紧约束,而高度是一个松约束。通过下图中 widget Details Tree 可以看出
class Expanded extends Flexible {
/// Creates a widget that expands a child of a [Row], [Column], or [Flex]
/// so that the child fills the available space along the flex widget's
/// main axis.
const Expanded({
super.key,
super.flex,
required super.child,
}) : super(fit: FlexFit.tight);
}
[图片上传失败...(image-e43482-1721805129145)]
- 样例26
Directionality(
textDirection: TextDirection.ltr,
child: Row(
children: [
Expanded(
child: Container(
color: Colors.red,
child: const Text(
'This is a very long text that won't fit the line.',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
Expanded(
child: Container(
color: Colors.green,
child: const Text(
'Goodbye!',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
],
),
)
如果所有 Row
的子级都被包裹了 Expanded
widget,每一个 Expanded
大小都会与其 flex 因子成比例,并且 Expanded
widget 将会强制其子级具有与 Expanded
相同的宽度。
换句话说,Expanded
忽略了其子 Widget
想要的宽度。
[图片上传失败...(image-152446-1721805129145)]
- 样例27
Directionality(
textDirection: TextDirection.ltr,
child: Row(
children: [
Flexible(
child: Container(
color: Colors.red,
child: const Text(
'This is a very long text that won't fit the line.',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
Flexible(
child: Container(
color: Colors.green,
child: const Text(
'Goodbye!',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
],
),
)
如果你使用 Flexible
而不是 Expanded
的话,唯一的区别是,Flexible
会让其子级具有与 Flexible
相同或者更小的宽度。而 Expanded
将会强制其子级具有和 Expanded
相同的宽度。但无论是 Expanded
还是 Flexible
在它们决定子级大小时都会忽略其宽度。
Flexible: 默认传入的 fit = FlexFit.loose
对其子级是一个松约束,子级大小会根据他的子级内容大小进行自适应、
Expanded: 继承自Flexible,传入的 fit = FlexFit.tight
对其子级在宽度上是一个紧约束,会强制子级宽度与其相等
[图片上传失败...(image-cbfaf8-1721805129145)]
- 样例28
MaterialApp(
home: Scaffold(
body: Container(
color: Colors.blue,
child: const Column(
children: [
SizedBox(height: 100),
Text(
'Hello!',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(
'Goodbye!',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
),
)
屏幕强制 Scaffold
变得和屏幕一样大,所以 Scaffold
充满屏幕。然后 Scaffold
告诉 Container
可以变为任意大小,但不能超出屏幕。
[图片上传失败...(image-b8a8cc-1721805129145)]
- 样例29
MaterialApp(
home: Scaffold(
body: SizedBox.expand(
child: Container(
color: Colors.blue,
child: const Column(
children: [
SizedBox(height: 100),
Text(
'Hello!',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(
'Goodbye!',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
),
),
)
[图片上传失败...(image-6d3802-1721805129145)]