3、Row 与 Column 的弹性布局:Flex 因子、MainAxisSize、CrossAxisAlignment 的底层逻辑
说实话,Flutter 的布局体系里,Row 和 Column 是最基础、也最容易被低估的两个组件。很多人用它们只是简单排一排子组件,但一旦遇到复杂的自适应场景,就开始各种套 Expanded、加 SizedBox,最后代码又臭又长。
我个人习惯是,先把 Row 和 Column 的底层逻辑吃透。你想想看,它们本质上就是一个单方向的 Flexbox。主轴方向怎么分配空间?交叉轴方向怎么对齐?这些搞明白了,布局代码能精简一半。
3.1 Flex 因子:空间分配的「权重」
Flex 因子,说白了就是告诉 Row 或 Column:「剩下的空间,我该拿多少」。这个「剩下的空间」指的是主轴方向减去所有非弹性子组件后的余量。
举个例子:
Row(
children: [
Container(width: 100, color: Colors.red),
Expanded(flex: 2, child: Container(color: Colors.blue)),
Expanded(flex: 1, child: Container(color: Colors.green)),
],
)
这里红色容器固定 100 像素。剩下的宽度,蓝色拿 2/3,绿色拿 1/3。嗯,就这么简单。
核心要点:
- flex 默认值是 1,不是 0。很多人以为不写 flex 就是 0,其实 Expanded 默认 flex: 1
- flex 可以是任意正数,不一定是整数。0.5、2.5 都行
- 多个 Expanded 按 flex 比例分配剩余空间
我在项目中遇到过一个问题:三个按钮平分一行,但中间按钮内容特别多。如果直接用 Expanded,内容会被截断。后来我改用 Flexible 配合 fit: FlexFit.loose,让子组件可以按自身大小展示,超出部分再滚动。这个坑踩得值。
3.2 MainAxisSize:主轴尺寸的「伸缩开关」
MainAxisSize 只有两个值:max 和 min。默认是 max,也就是 Row 或 Column 会撑满主轴方向可用空间。
但有时候我们不想让它撑满。比如一个按钮组,希望它只占实际内容的宽度:
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(onPressed: () {}, child: Text('确定')),
SizedBox(width: 8),
ElevatedButton(onPressed: () {}, child: Text('取消')),
],
)
这样 Row 的宽度就等于两个按钮加间距的总和。不会像默认那样占满屏幕宽度。
我的建议:
如果你发现 Row 或 Column 莫名其妙地撑满了父容器,先检查 mainAxisSize 是不是 max。很多时候我们只需要 min,但忘了改。
为什么会这样?因为 Flutter 的布局是「约束向下传递,尺寸向上汇报」。父容器给 Row 一个最大宽度约束,Row 默认会取最大值。改成 min 后,它只取子组件需要的尺寸。
3.3 CrossAxisAlignment:交叉轴的对齐「魔法」
交叉轴方向,Row 是垂直方向,Column 是水平方向。CrossAxisAlignment 控制的就是这个方向的对齐方式。
常用的值有:
| 值 | 说明 | 适用场景 |
|---|---|---|
start |
顶部/左对齐 | 默认行为,文字基线对齐 |
center |
居中对齐 | 图标与文字垂直居中 |
end |
底部/右对齐 | 底部导航栏 |
stretch |
拉伸填满交叉轴 | 等高的卡片列表 |
baseline |
文字基线对齐 | 不同字号文字的对齐 |
这里有个容易混淆的点:stretch 和 center 的区别。stretch 会让子组件强制拉伸到交叉轴的最大尺寸,而 center 只是居中,子组件保持自身大小。
我曾经在做一个表单页面时,用了 CrossAxisAlignment.stretch,结果所有输入框都被拉成了同样的高度。嗯,那正是我想要的——等高的输入框看起来更整齐。但如果某个输入框需要特殊高度,就得单独用 Container 包裹并设置 height。
注意:
CrossAxisAlignment.baseline 只对 Row 有效,Column 不支持。而且使用 baseline 时,子组件必须实现 TextBaseline 属性,否则会报错。
3.4 三个属性的协同工作
这三个属性不是孤立的。它们共同决定了 Row 和 Column 的布局行为。
举个例子:
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Icons.star, size: 40),
Text('评分 4.8', style: TextStyle(fontSize: 20)),
],
)
这里 mainAxisSize 是 min,所以 Row 只占图标加文字加间距的宽度。crossAxisAlignment 是 center,所以图标和文字在垂直方向居中对齐。如果改成 start,图标会偏上,文字会偏下,看起来就不协调了。
你想想看,如果我把 mainAxisSize 改成 max,这个 Row 就会撑满屏幕宽度,但图标和文字仍然在交叉轴居中。很多时候我们只需要改一个属性,就能达到完全不同的效果。
3.5 避坑指南
最后分享几个我踩过的坑:
- Expanded 嵌套问题:不要在 Expanded 里再套 Expanded,除非你很清楚自己在做什么。否则布局会变得不可预测。
- Flexible 与 Expanded 的区别:Expanded 强制子组件填满剩余空间,Flexible 允许子组件小于剩余空间。我一般用 Expanded 做等分,用 Flexible 做自适应。
- 交叉轴溢出:如果 Row 的子组件高度不一致,又没有设置 crossAxisAlignment,默认是 start,可能导致底部对齐不整齐。用 center 或 stretch 可以解决。
我曾经在做一个聊天界面时,消息气泡用 Row 排列,头像和文字高度不一致。默认 start 对齐导致头像偏上,文字偏下,看起来很奇怪。改成 crossAxisAlignment: start 后,又发现文字底部被截断了。最后用 crossAxisAlignment: center 配合 padding 微调,才达到理想效果。
嗯,布局这东西,说白了就是不断试错的过程。但只要你理解了 Flex 因子、MainAxisSize 和 CrossAxisAlignment 的底层逻辑,大部分问题都能迎刃而解。