约束向下,尺寸向上,父定位置
这是从 Android 带过来最顽固的一个错觉。在 Flutter 里,父组件问不到孩子的意愿——布局是单次、自上而下走一遍就完事。整套规则只有三句话,官方甚至把它印在文档最显眼处。学会这三句,你就再也不会对着「为什么我 width: 200 没生效」抓头了。
三句话
1. 约束向下(Constraints go down)。父节点给孩子一个 BoxConstraints——「你的宽必须在这个区间、高必须在那个区间」。
2. 尺寸向上(Sizes go up)。孩子在父给的区间里挑一个尺寸,把 Size 回给父。
3. 父定位置(Parent sets position)。父拿到孩子的尺寸后,决定把它摆在哪个偏移量上。
就这三句。整个 Flutter 布局系统没有第四句。你遇到的每一个布局问题,答案都在这三句里。
关键推论马上就来:既然父只给区间、孩子只能在区间里挑——一个 Widget 没法自己决定大小,它只能在父亲允许的范围内选一个。你写的 width: 200 是一个「愿望」,父亲给的约束才是「法律」。愿望和法律冲突时,法律赢。
BoxConstraints 长什么样
约束就是四个数:
class BoxConstraints {
final double minWidth; // 最小宽
final double maxWidth; // 最大宽(可能是 double.infinity)
final double minHeight;
final double maxHeight;
}
由这四个数派生出三种「性格」,认得它们,布局就懂一半:
| 约束类型 | 特征 | 意思 | 谁给的 |
|---|---|---|---|
| 紧(tight) | min == max | 「你必须正好这么大」 | SizedBox(width, height)、屏幕给根节点 |
| 松(loose) | min == 0 | 「别超过 max 就行,多小随你」 | Center、Align |
| 无界(unbounded) | max == 无穷 | 「你想多大都行」 | Column 主轴、可滚动区(第 12 章的雷) |
亲手跑:真的约束求解器
下面这台真的传播 BoxConstraints:从屏幕的紧约束开始,一层层往下传,每个节点按自己的规则收缩或放松,孩子把尺寸回上来,父亲定位置。右边那条链路是它当场记的账——每一行是「谁收到什么约束(↓),回了什么尺寸(↑)」。
四个场景逐个看,它们覆盖了你日常 90% 会困惑的情况:
场景一:Center 里放一个固定盒子
屏幕给 Center 的是紧约束「你必须是 320×380」。Center 自己占满,但它给孩子的是松约束「0 到 320、0 到 380,随你」。孩子(100×60 的盒子)挑了 100×60 回上去。Center 拿到后执行第三句——把它摆在正中间,偏移 (110, 160)。
注意:Center 做的唯一一件事,就是「把紧约束放松成松约束,然后居中孩子」。它自己不决定大小,它占满父给的空间。
场景二:约束是会传染的
这是最重要的一个场景,请多看两眼。SizedBox(200, 200) 里面放一个想要 999×999 的盒子——结果孩子拿到的是 200×200。
SizedBox(width: 200) 给孩子的是紧约束(min=max=200)。孩子哪怕写了 width: 999,也只能在 [200, 200] 这个区间里挑——只有一个选择,就是 200。父亲的紧约束,会碾过孩子的一切尺寸愿望。
所以下次你 Container(width: 300) 没生效,去看它的父亲:多半是某个父节点给了它紧约束(SizedBox、Expanded、占满的 ListView 项……)。问题永远在上面,不在这个 Container 自己身上。
场景三:Container 的两副面孔
同一个 Container(color: blue):没有孩子时它尽可能大,有孩子时它尽可能小(刚好包住孩子)。很多人把这当成 Container 的「特殊规则」去背——不用背,它是三句话的自然结果:
- Container 自己没有尺寸意见时,就把父给的最大值用满(所以无孩子 → 占满)。
- 有孩子时,它让孩子先量,孩子回上来多大它就多大(所以有孩子 → 收缩)。
Flutter 里很多 Widget 都遵循这个「自己没意见就随大,有孩子就随小」的模式。理解了它,你就不会再被 Container、DecoratedBox 这些的「大小行为」搞糊涂。
场景四:Padding 的完整算法,三行
看 Padding 那条链路,它把三句话演了个完整的来回:
- 收到 320×200 的约束(约束向下之前);
- 四边各扣掉 16,把 288×168 传给孩子(约束向下);
- 孩子回上来一个尺寸,Padding 把它加回 32 当作自己的尺寸(尺寸向上),并把孩子摆在 (16,16)(父定位置)。
整个 RenderPadding 的源码逻辑,就是这三步。你现在能推理它了——这就是「规则少而硬」的好处。
这套设计换来了什么,代价是什么
换来的是速度。因为布局是单次自上而下 + 单次自下而上,每个节点最多被访问两次(下发约束一次、上收尺寸一次),整体是 O(N)。这也是 60fps 的另一半保障(前一半是第 7 章的 O(N) diff)。
代价是表达力受限。「父亲根据孩子的理想尺寸来决定自己怎么布局」这种需求,在这套模型里天然做不到——因为父亲下发约束时,孩子还没量呢。想要这种能力,你得付额外的钱:
IntrinsicHeight / IntrinsicWidth 能实现「让 Row 里所有孩子等高」这种「先问孩子理想尺寸」的效果。但它的做法是在正式布局前,额外多跑一遍「试探性测量」——于是那棵子树被布局了两遍。嵌套几层,就是指数级。
能不用就不用。官方文档专门警告过它的成本。大多数「我想让它们等高」的需求,用 Row + CrossAxisAlignment.stretch,或者干脆固定高度就能解决。真需要时也要控制它的作用范围,别包在大列表外面。
一个立刻能用的调试习惯
既然「布局问题永远在上面的约束」,那排查方法就固定了:
1. 打开 DevTools 的 Layout Explorer(Flutter Inspector 里)。点中出问题的节点,它会显示这个节点收到的 constraints 和算出的 size。
2. 顺着约束往上看。如果它拿到的是紧约束、而你以为能自由伸展,那问题在给它紧约束的那个父节点。
3. 临时用 ColoredBox 或 Container(color:) 染色。Flutter 里布局问题看不见,把可疑节点染上颜色,边界立刻可见。这比在 Android 里开「显示布局边界」还方便,因为你随时可以精准染一个节点。
好消息:Compose 的布局模型和这个几乎一模一样。那边也是「父传 Constraints 下去,子测量后返回尺寸,父在 layout {} 里 place 孩子」——约束向下、尺寸向上、父定位置,一字不差。
唯一的实质差别:Compose 的 Modifier(如 Modifier.width(200.dp))会在测量时修改传给内容的约束——概念上等于 Flutter 往树里插了一个 SizedBox/ConstrainedBox 节点。所以「Modifier 链」和「嵌套布局 Widget」在布局层面是同一件事的两种写法。你在 Compose 里理解的约束传递,直接迁移过来,只是从「链」变成了「树」。
还有一点:Compose 也有对应 Intrinsic 的 IntrinsicSize.Min/Max,成本一样高,一样劝你少用。你的旧经验在这里全都成立。
这一章的一句话
Flutter 布局只有三句话——约束向下、尺寸向上、父定位置——推论是「Widget 不能自己决定大小,只能在父给的约束里挑」;你 90% 的「设了宽高没用」都因为某个父节点下发了紧约束,去上面找它,别盯着这个 Widget 本身。
三句话里最有戏的是第二句「尺寸向上」——当多个孩子要瓜分同一份空间时,它就变成了一场分配游戏。下一章我们把 Row 和 Column 拆开,看 Expanded 怎么两趟分配(正是你熟的 weight),以及那条黄黑警戒条到底是怎么被算出来的。