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 只有两个值:maxmin。默认是 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 文字基线对齐 不同字号文字的对齐

这里有个容易混淆的点:stretchcenter 的区别。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 的底层逻辑,大部分问题都能迎刃而解。