最后核验:2026-09-09
本篇结论
子组件只能在父级给出的约束内选择尺寸,写了 width: 120 不代表最终一定宽 120。Row 中的普通文本也不会自动得到“剩余宽度”;长标题需要先通过 Expanded 等方式取得有限宽度,再决定换行或截断。
本篇用两组小实验解决同一个问题:如何从父级约束解释实际布局。先比较相同尺寸声明为何得到不同结果,再复现长标题溢出并修正。
学完你能做到
- 用最小/最大宽高描述约束。
- 区分紧约束、松约束和无界约束。
- 解释 SizedBox 的尺寸声明为何可能被父级限制。
- 为 Row 内的长标题分配剩余空间。
- 区分布局修正和文本截断策略。
- 用窄屏、放大字体和真实错误验证布局。
开始前检查
先完成 第 16 课 ,理解 RenderObject 负责布局与绘制。本篇新增:
examples/lesson_17_layout_constraints.dart
app/test/lesson_17_layout_constraints_test.dart
assets/screenshots/17-layout-constraints/
完整示例 使用 app/ 已有的 Flutter 平台工程和依赖,从独立入口运行。本篇实际使用 Flutter 3.47.2、Dart 3.13.2 与 iPhone 17 Pro/iOS 26.5 Simulator 验证,不添加依赖。
默认入口显示正确布局。LESSON17_OVERFLOW 是本课专门保留的错误演示开关,用于重复观察同一问题,不是忽略错误或绕过检查的开关。
01 把父级约束写成范围
Flutter 的盒布局用四个数表达约束:最小宽度、最大宽度、最小高度、最大高度。子盒子返回的尺寸必须符合这些范围。
| 约束示例 | 含义 |
|---|---|
| 宽度 240~240 | 紧约束,宽度只能是 240 |
| 宽度 0~240 | 松约束,允许选择较小宽度,但不能超过 240 |
| 高度 0~无限 | 高度无上界,不等于要求子组件变成无限高 |
“松”与“无界”不是同义词。松约束的最小值为零,最大值仍可以有限。约束术语与有效范围见 BoxConstraints API 。
布局时,父级把约束传给子级,子级在约束内确定尺寸并向上返回,父级再安排子级的位置。具体哪个节点改变了约束,需要沿父子链查看,不能只盯着最里面的 width。流程依据见 Flutter 官方约束教程 。
本篇宽高数字都是 Flutter 布局使用的逻辑像素,不是截图文件的物理像素数量。
02 为什么 width: 120 最后变成 240
第一组实验把小 SizedBox 放进一个固定为 240 × 64 的父盒子:
const SizedBox(
width: 240,
height: 64,
child: SizedBox(
width: 120,
height: 32,
child: ColoredBox(
key: ValueKey('tight-box'),
color: Colors.deepPurple,
),
),
),
在本篇已验证的视口中,外层盒子取得 240 × 64,并向内层施加这个紧约束。内层请求 120 × 32,但父级没有留下其他尺寸选择,因此紫色盒子最终仍是 240 × 64。
SizedBox 的指定尺寸仍需满足父级约束,见 SizedBox API 。如果外层本身也拿不到 240 宽,例如祖先只允许 200 宽,它同样不能保证自己的宽度是 240。因此这里的数字结论有明确的父级空间前提。
03 Center 改变了哪一层约束
第二组保持外框为 240 × 64,在内层盒子之前放一个 Center:
const SizedBox(
width: 240,
height: 64,
child: ColoredBox(
color: Color(0xFFEDE7F6),
child: Center(
child: SizedBox(
width: 120,
height: 32,
child: ColoredBox(
key: ValueKey('loose-box'),
color: Colors.deepPurple,
),
),
),
),
),
浅色外框仍是 240 × 64。Center 让子盒子在不超过外框的范围内选择较小尺寸,并把它居中;因此紫色盒子可以采用 120 × 32。居中和尺寸约束分别承担位置与可用范围的作用,见 Center API 。
这不意味着所有尺寸问题都应该套 Center。当前需求恰好是“保留一个大外框,并把小盒子放在中间”,所以它符合布局意图。
04 Row 为什么让长标题越过右边界
接下来用一条灵感行展示图标、长标题和箭头。IdeaRow 为对照保留了这一行:
if (constrainTitle) Expanded(child: text) else text,
错误模式令 constrainTitle 为 false,把标题作为普通 Row 子项。即使 Row 本身宽度只有 320,它在测量非弹性子项时,也会给它们无界的水平约束。长标题不知道要在哪个宽度内换行,测量结果再加上两个图标和间隔,就超过了整行宽度。
Row 不会自动滚动。调试模式会报告 A RenderFlex overflowed,并标出黄黑条纹。测量顺序与错误原因见 Row API 。
本例的标题已经设置了:
final text = Text(
title,
key: const ValueKey('idea-title'),
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
但 maxLines 和省略号没有替标题创造有限的水平布局空间,因此错误模式仍会溢出。文本溢出显示策略见 Text.overflow API 。
05 用 Expanded 分配有限的剩余宽度
默认修正模式用 Expanded(child: text)。Row 先安排固定图标与间隔,再把剩余宽度交给标题。
本例图标各宽 24,间隔各宽 12,固定部分共宽 72:
| 实际 Row 宽度 | 固定部分 | 标题得到的宽度 |
|---|---|---|
| 240 | 72 | 168 |
| 320 | 72 | 248 |
这是当前只有一个 Expanded、没有额外水平内边距时的结果。多个弹性子项会根据 flex 分配空间,不能把这条减法公式直接套到其他结构上。
Expanded 让子项填满分配到的主轴空间;它需要位于 Row、Column 或 Flex 对应的子树中,本例直接放在 Row 的 children 里。见 Expanded API 。
得到有限宽度后,标题才按最多两行展示,放不下的部分用省略号表达。这里的选择适合摘要行;如果产品要求完整阅读,就应允许更多高度或在详情页展示全文。缩小所有文字和裁掉超出部分都不能代替这个业务选择。
页面里外层 SizedBox(width: 320) 也受可用空间限制。在 320 宽视口扣除左右各 24 的页面内边距后,整行实际最多宽 272,标题宽度相应变成 200。不要把设备宽度直接当作某个嵌套组件的可用宽度。
06 滚动只解决对应方向的问题
完整页面使用竖向 SingleChildScrollView 容纳几段有限的演示内容,避免小屏或放大字体后整体高度超出屏幕。它没有替 Row 内的标题分配水平宽度,因此错误模式在可滚动页面里仍会水平溢出。
竖向滚动容器允许内容沿竖向延伸,内容的最大高度可能无界。不要在其中的 Column 随意加入需要“填满剩余高度”的 Expanded。这里的 Expanded 只在有有限宽度的 Row 中分配横向空间。约束冲突说明见 SingleChildScrollView API 。
本篇只有少量固定内容。大量灵感列表的按需构建、滚动和 Key 留到第 18 课,不把所有列表都写成 ScrollView 加 Column。
07 在 iOS 模拟器复现错误,再运行修正
沿用 第 08 课 的 SDK 变量,从仓库根目录进入 app/。确认并选择已启动的模拟器:
cd app
"$FLUTTER_UPSTREAM_HOME/bin/flutter" devices
IOS_DEVICE_ID="BC0ED930-EAE5-461C-8A3D-D6286FC39DA2"
设备标识取自本次 iPhone 17 Pro/iOS 26.5,读者替换成自己的标识。先显式启动错误演示:
"$FLUTTER_UPSTREAM_HOME/bin/flutter" run \
-d "$IOS_DEVICE_ID" -t ../examples/lesson_17_layout_constraints.dart \
--dart-define=LESSON17_OVERFLOW=true
入口用常量 bool.fromEnvironment 读取编译配置,默认是 false。这是 Dart 编译环境参数,不是读取 .env 文件或系统环境变量,见 bool.fromEnvironment API 。
本次模拟器实际报告的关键内容为:
A RenderFlex overflowed by 140 pixels on the right.
constraints: BoxConstraints(w=320.0, 0.0<=h<=Infinity)
size: Size(320.0, 24.0)
140 是这次字体和视口下的观察值,不是所有设备的固定结果。先看方向,再看出错的 Row 和约束范围,不要只调整错误数字附近的某个宽度。

按 q 退出该次运行,去掉错误参数重新启动:
"$FLUTTER_UPSTREAM_HOME/bin/flutter" run \
-d "$IOS_DEVICE_ID" -t ../examples/lesson_17_layout_constraints.dart
预期紫色尺寸对照清楚可见,灵感标题在行内展示,右侧箭头没有被挤出边界,控制台不再报告这项溢出。修正模式已在同一台 iOS 模拟器实际运行并保存真实截图,本课 Widget 测试同时覆盖尺寸、字体缩放和无异常条件。
错误演示中的黄黑条纹是刻意保留的复现证据,默认模式必须没有该错误。页面无需点击即可观察结果。
Flutter 运行参数见 CLI 参考 。若首次运行需要下载现有包或构建产物,沿用 第 03 课 的可信镜像路径;本课不增加包、不改变 SDK 版本。
08 用测试测量实际尺寸
打开 本课测试 。测试不是只检查 Text 上写了哪个宽度,而是用 tester.getSize() 取得布局完成后的实际尺寸。
7 项测试分别验证:
- 紧约束下为 240 × 64,Center 内为 120 × 32。
- 故意去掉 Expanded 时,确实产生指定的 RenderFlex 溢出错误。
- 行宽 240/320 与字体比例 1/2 的四种组合中,标题宽度等于行宽减 72,箭头仍在边界内,没有布局异常。
- 320 × 568 的逻辑视口与两倍字体下,整页没有布局异常。
字体测试还读取实际 TextScaler,确认放大比例生效。两倍线性缩放只是本课明确验证的条件,不等于覆盖全部系统动态字体设置。测试中使用的 TextScaler.linear 见 TextScaler API 。
错误模式的测试通过 tester.takeException() 检查预期异常,正常模式则要求没有异常。前者是在验证可复现错误,不是把正常模式报错忽略掉。接口见 takeException API 。
在 app/ 执行:
"$FLUTTER_UPSTREAM_HOME/bin/flutter" analyze
"$FLUTTER_UPSTREAM_HOME/bin/flutter" test \
test/lesson_17_layout_constraints_test.dart --reporter expanded
"$FLUTTER_UPSTREAM_HOME/bin/flutter" test --reporter expanded
"$FLUTTER_UPSTREAM_HOME/bin/flutter" build ios --simulator \
-t ../examples/lesson_17_layout_constraints.dart
"$FLUTTER_UPSTREAM_HOME/bin/flutter" build apk --debug \
-t ../examples/lesson_17_layout_constraints.dart
本课 7 项、全项目 23 项测试应通过,构建使用默认修正模式。构建保留 -t,避免只验证到原有 lib/main.dart。
三端差异
- iOS:在 iPhone 17 Pro/iOS 26.5 Simulator 运行并查看错误与修正模式,保存两张真实截图;尺寸、窄屏及放大字体组合由主机 Widget 测试验证。
- Android:完成本课默认入口的 debug APK 回归构建,未把 iOS 截图或主机测试描述为 Android 设备行为验证。
- Track B:未执行 Flutter-OH 构建或运行,不宣称本课已通过鸿蒙兼容验证。
预期结果
- 能从父级约束解释两个紫色盒子的不同尺寸。
- 能复现 Row 溢出,并说明 Expanded 修正的是哪一段约束。
- 能区分“获得有限宽度”和“选择截断方式”。
- 默认布局、测试和对应入口的两端构建通过。
常见问题
width 写了就应该生效吗
它仍需满足父级约束。先确认组件收到的最小和最大宽度,再判断为什么只能选择另一个尺寸。
有省略号为什么还会横向溢出
省略号是文本在有限空间内放不下时的显示策略,不能替 Row 分配水平空间。本例需要先用 Expanded 给标题宽度边界。
Expanded 是否总能解决溢出
不是。如果固定图标和间距已经占满全部宽度,仍需改变布局;主轴无界时也不能照搬“分配剩余空间”。本课只验证指定宽度范围。
为什么没有把错误模式删掉
它是本课明确命名、默认关闭的教学复现路径,测试要求它产生预期错误。读者正常运行和构建时使用修正模式。
完成检查
- 我能用四个边界描述盒约束。
- 我能区分紧、松和无界约束。
- 我知道尺寸声明必须服从父级约束。
- 我能解释 Row 内的非弹性标题为何可能溢出。
- 我不会把省略号、裁切或滚动当成所有布局问题的修复。
- 我已复现错误,并确认默认模式和测试通过。
一手来源
- Flutter 官方:理解布局约束 。
- Flutter API:BoxConstraints 。
- Flutter API:SizedBox 。
- Flutter API:Center 。
- Flutter API:Row 。
- Flutter API:Expanded 。
- Flutter API:Text.overflow 。
- Flutter API:SingleChildScrollView 。
- Dart API:bool.fromEnvironment 。
- Flutter API:TextScaler.linear 。
- Flutter API:WidgetTester.takeException 。
- Flutter 官方:CLI 参考 。