本文主要是针对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的位置。
-
官方示例:
image.png
那么谈判将会像这样:
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)]
