最后核验: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 和约束范围,不要只调整错误数字附近的某个宽度。

iOS 模拟器复现长标题 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 内的非弹性标题为何可能溢出。
  • 我不会把省略号、裁切或滚动当成所有布局问题的修复。
  • 我已复现错误,并确认默认模式和测试通过。

一手来源