Flutter 布局三步走
Flutter 的布局核心规则是:
- 父给子约束(constraints: min/max 宽高)
- 子在约束内确定自己的大小(大小可自由 or 被强制)
- 父再根据子大小决定自己的大小和位置
约束的类型
紧约束 (tight constraints):宽或高的 min = max,子必须是这个大小。
👉 子没自由。
松散约束 (loose constraints):只有 max,min=0,子可以小于 max。
👉 子有自由。
常见组件的约束传递
父组件 | 给子约束的类型 | 子能否决定大小? | 说明 |
---|---|---|---|
Container(width: 140, height: 42) | 紧约束:w=140, h=42 | ❌ 不能 | 子被强制 (140,42),即使子写了 maximumSize 也无效 |
SizedBox(width: 100, height: 50) | 紧约束:w=100, h=50 | ❌ 不能 | 子必须是 (100,50) |
Expanded / Flexible(在 Row/Column 里) | 紧约束(填满主轴空间) | ❌ 完全受控 | 子 被拉伸到父要求的大小 |
Center | 松散约束:最大等于父范围 | ✅ 可以 | 子决定大小,例如 Button 遵循 maximumSize |
Align | 松散约束 | ✅ 可以 | 类似 Center,但带对齐 |
Row / Column(mainAxis) | 紧约束(如果用 Expanded/Flexible) | ❌ 不能 | 子被拉伸填满剩余空间 |
Row / Column(无 Expanded/Flexible) | 松散约束 | ✅ 可以 | 子自由决定大小,Row/Column 再排布 |
UnconstrainedBox | 不传递约束(但还受祖先约束) | ✅ 可以 | 子完全根据自身大小渲染,可能溢出父 |
ConstrainedBox | 传递 min/max 范围 | ✅ 部分 | 子大小必须落在范围内 |
父组件没有设置大小时的情况
父组件类型 | 没有写宽高时的行为 | 给子约束 |
---|---|---|
Container() | 如果没写 width/height ,它会根据 子组件大小 来决定自己的大小(受祖先约束限制)。 |
把祖先的约束原封不动传递给子(通常是松约束)。 |
Center() | 自己会尽量占满父(受祖先约束),但给子传 松约束(最大值等于父)。 | 松约束 |
Align() | 类似 Center,自己先占满父,但给子松约束。 | 松约束 |
Row/Column | 沿着主轴给子松约束(除非用 Expanded/Flexible ),交叉轴是如果Row/Column有其父类的约束,那么它的约束就是紧约束,如果没有父类的约束,那么它会根据 子组件大小 来决定自己的大小。 |
松 or 紧混合 |
Expanded | 它会强制子在主轴方向填满(紧约束)。 | 紧约束 |
ListView/SingleChildScrollView | 在滚动方向上给子松约束(允许无限大),另一方向是紧约束。 | 松+紧 |
父组件 没写宽高 ≠ 一定是松约束。
要看父组件是什么:
- Center、Align、Padding:一般给子松约束
- Row/Column:部分方向紧,部分方向松
- Expanded/Flexible:紧约束
- Container:看子来决定大小,但受祖先约束
父 vs 子 谁说了算?
父决定子(tight constraints)
- Container/SizedBox 直接写死宽高
- Expanded/Flexible 在 Row/Column 中
- 父 min=max 的情况
Container+Contaier
Container(
width: 100,
height: 100,
color: Colors.blue,
child: Container(width: 50, height: 50, color: Colors.red),
),
在这里案例中,尽管子组件Container设定了自己的长宽都为50,但是由于父组件是紧约束,子组件还是遵循了父组件的约束,大小变为width=100
,height=100
。

Container+ElevatedButton
Container(
height: 42,
width: 140,
child: ElevatedButton(
onPressed: () {},
style: ButtonStyle(
// width<=80 height<=40
maximumSize: WidgetStateProperty.all(Size(80, 40)),
backgroundColor: WidgetStateProperty.all(Colors.orange),
),
child: const Text(
"Get Started",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w300,
color: Colors.white,
),
),
),
),
在上面这个案例中,经过我设置了ElevatedButton的width<=80 height<=40
,但是由于受到父组件严格约束的影响,ElevatedButton的大小还是变成了width=140 height=42
。
子决定父(loose constraints)
Center / Align / Padding 等传递松散约束
IntrinsicWidth / IntrinsicHeight 测量子后再决定父
父只提供上限,子返回自己需要的大小
Scaffold+Column+Container
我希望实现的效果:

我实际实现的效果:

为什么会这样?我我分析一下我的代码:
class _SplashPageState extends State<SplashPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(color: AppColors.backgroundSplash),// 没有效果
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, // 没有效果
children: [
Container(
alignment: Alignment.center,
width: 120,
height: 120,
color: Colors.white,
),
],
),
),
);
}
}
为什么背景色会实现:
BoxDecoration(color: AppColors.backgroundSplash)
:我的背景色设置了蓝色。这个蓝色不是整个页面Scaffold
的蓝色,而是Container
的蓝色。而
Container
由于自己没有设置大小,那么它的大小完全由子组件Column
决定。其中Column
的w=120
,所以Container
的w=120
。因此背景色也只是120
的宽度,而不是覆盖整个页面。
为什么交叉轴没有居中?
- 因为子组件
Container
的大小等于Column
的大小。所以Column
的width=120
。 - 所以,实际上在交叉轴上,子组件
Container
是居中的,只是Column
宽度等于Container
的宽度,所以看不出来。
正确的做法:
class _SplashPageState extends State<SplashPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.backgroundSplash,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(width: 120, height: 120, color: Colors.white),
Text("Online Market"),
Text("10"),
],
),
),
);
}
}
实际上,我们发现Column
的width并没有变。但是Center
是父组件的大小:
Center()
:在没有写大小的时候,自己会尽量占满父(受祖先约束),但给子传 松约束(最大值等于父)。