3.2 布局系统:约束向下、尺寸向上


3.2 布局系统:约束向下、尺寸向上

本节摘要:Flutter 布局的全部规则可以压缩成一句话:约束从父到子传递,尺寸从子到父汇报,父级最终决定子级的位置。本节用这条规则拆解 Row、Column、Expanded、Stack 的行为,解掉"列表放进 Column 报错""Expanded 里包不出高度"等高频问题,并现场搭出轻记账首页。读完你将能预测任何组合的排布结果,而不是靠改代码碰运气。

一条规则解释所有布局行为

上一节的 build 产出了配置树,接下来布局工序接手。每个布局组件(Row、Column、Padding 等)做布局时只有一份交互协议:

  1. 父级给子级一份约束(Constraint):最小与最大宽度、最小与最大高度;
  2. 子级在这份约束里决定自己的尺寸,可能还需要问自己的孩子(递归);
  3. 子级把最终尺寸汇报给父级,父级按自己的对齐规则放置它。

三个角色缺一不可:约束是"你最多能多大",尺寸是"我实际多大",位置是父级说了算。记住"汇报的尺寸不一定被尊重"——Center 里的孩子说想要 400 像素宽,但父级约束最大 300,那它就是 300。

这套协议带来 Flutter 与传统 UI 框架的关键差异:没有"自然大小"的绝对概念。同一个 Text,放进无限宽的行里是一行流式排布,放进固定宽的格子里就自动换行截断——它的尺寸始终是"在给定约束下算出来的",脱离约束谈大小没有意义。初学者的布局挫败感,多半源于用旧框架的直觉(控件自带理想尺寸)来套这套协议。

图 7 约束向下、尺寸向上的完整回合

图 7 约束向下、尺寸向上的完整回合

Row 与 Column 的排班规则

Row 与 Column 是同一个 Flex 布局的横竖两用版,规则一并说。一排孩子的宽度分配分三步:

  1. 不弹性的先量:普通孩子(Text、SizedBox)按"约束向下、尺寸向上"各报各的;
  2. 弹性的分账:总约束减去不弹性孩子的占用(含间距),剩余空间按 flex 值比例分给 Expanded 与 Flexible——Expanded 强制填满分到的份额,Flexible 允许孩子比份额小;
  3. 超了就溢出:不弹性孩子的申报总和超过总约束,直接溢出(屏幕上出现黄黑条纹),Row 的主轴上不自动换行。

主轴与纵轴的对齐用两组参数:mainAxisAlignment 管排列方向(起头、居中、两端、均分),crossAxisAlignment 管横向对齐(Row 里是上下对齐)。一个新手高频困惑"Row 里的孩子怎么高度不一致",答案就在纵轴默认值 crossAxisAlignment.center 之外——想要等高就 crossAxisStretch,想顶部对齐就 start。

轻记账首页的金额卡片行是标准示例:

Row( children: [ const Text('本月支出', style: TextStyle(fontSize: 14)), const SizedBox(width: 8), // 固定占位,不参与分账 Expanded( // 分走剩余全部宽度 child: Text('¥3,280.50', style: Theme.of(context).textTheme.headlineMedium, overflow: TextOverflow.ellipsis), ), IconButton( onPressed: () {}, icon: const Icon(Icons.visibility), ), ], )

无界约束:报错与解法都背下来

约束协议里有一个特殊状况:无界约束(最大值为无穷)。滚动视口(ListView 的主轴)给孩子的就是无界约束——"你要多高给多高,我会滚"。

由它引发的报错是初学者的第一大坑:Column 里直接放 ListView,或者 ListView 里再放 ListView(主轴同向)。Column 传给 ListView 的是"高度至少撑满、至多不限"?不对——准确地说,Column 在无界父级里本身就没有确定高度,ListView 收到无界高度约束后无法决定自己多高,于是抛出异常,大意是"可滚动组件收到了无界的高度约束"。

解法按场景三选一:

// 场景一:Column 里要滚动的区域占满剩余高度 → Expanded Column( children: [ const HeaderCard(), Expanded(child: ListView.builder(itemBuilder: (_, i) => Text('第 $i 条账单'))), ], ) // 场景二:只想让它按内容多高就多高 → shrinkWrap,代价是失去懒加载,长列表别用 ListView.builder(shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), ...) // 场景三:给一个确定的窗口高度 → SizedBox 定高 SizedBox(height: 240, child: ListView(...))

顺带把另一个高频报错说掉:Row 里塞 TextField 报"布局溢出"或约束异常,是因为输入框想"要多宽要多宽",而 Row 的剩余空间算不出来。给它 Expanded 或固定宽度,报错即消。记住排查口诀:溢出与约束异常,先问"它的父级给它的约束是什么",一层一层往上问。

Stack:叠加与定位

矩形排布讲完,还有一类"叠罗汉"需求:图片上压文字、背景上浮按钮。Stack 是唯一的多子级非 Flex 布局:孩子按声明顺序叠放,先声明的在底下;尺寸取非定位子级中的最大者(或受约束截断);Positioned 子级脱离常规排布,按给定偏移钉在 Stack 坐标系里。

Stack( children: [ Container(height: 160, color: Theme.of(context).colorScheme.primaryContainer), const Positioned(left: 16, bottom: 16, child: Text('九月光看板')), Positioned( right: 12, top: 12, child: Badge(label: const Text('3'), child: const Icon(Icons.notifications)), ), ], )

规则一句话:常规孩子参与决定 Stack 大小,Positioned 孩子只管自己钉在哪。把整个首页背景图放 Stack 底层、内容层用普通子级排列,是轻记账多个页面的通用骨架。

本节要点回顾

  • 布局协议三步:约束向下、尺寸向上、父级放置;尺寸永远在约束内解出,无绝对自然大小;
  • Flex 分账顺序:先量不弹性的,剩余空间按 flex 比例分给弹性孩子,总和超限即溢出条纹;
  • 无界约束坑位:可滚动组件收到无界约束必炸,解法是 Expanded 定区域、shrinkWrap 按内容、SizedBox 定窗口;
  • Row 里放输入框或无界宽度孩子,给 Expanded 或定宽即可消错;
  • Stack 叠加:常规子级决定大小,Positioned 只管定位。

布局讲的是"静止的形状"。界面还要"动"——下一节进入动画系统,看隐式与显式两套工艺各自适用什么场合。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U