Flutter Text 为什么“看起来不垂直居中”(baseline/metrics/line-height)

现象

ListWheelScrollView(滚轮选择器)里,数字会出现“看起来不垂直居中”的情况:中心数字与上下两行数字的间距不一致,即使外层已经 Center(...) 了。

一个典型片段(来自 lib/ui/pages/widgets/number_wheel_picker.dart):

return Center(
  child: Text(
    value.toString().padLeft(2, '0'),
    textAlign: TextAlign.center, // 只管水平,不管竖直
    style: TextStyle(
      fontSize: fontSize,        // 动态字号插值
      color: color,
      fontWeight: weight,
    ),
  ),
);

textAlign: center 只影响水平对齐;竖直方向是由文本布局系统决定的。


根因:Flutter 文本布局以 baseline + 字体度量为核心

1) RenderObject 的 baseline 计算链路(可直接查官方源码实现)

RenderParagraph.computeDistanceToActualBaseline(框架层):

// RenderParagraph.computeDistanceToActualBaseline
return _textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic);

来源:https://api.flutter.dev/flutter/rendering/RenderParagraph/computeDistanceToActualBaseline.html

TextPainter.computeDistanceToActualBaseline(绘制层):

// TextPainter.computeDistanceToActualBaseline
return _layoutCache!.layout.getDistanceToBaseline(baseline);

来源:https://api.flutter.dev/flutter/painting/TextPainter/computeDistanceToActualBaseline.html

这条链路说明:段落的竖直度量是由 text layout 计算出来的 baseline/metrics 决定的,并不是“把 glyph 的视觉几何中心放到盒子中心”。

2) 默认 line height 来自字体 metrics,而不是 fontSize

TextStyle.height 文档明确说明:

  • heightkTextHeightNone(或未设置)时,行高由字体 metrics 决定;
  • height 被设置时,行高精确等于 fontSize * height

来源:https://api.flutter.dev/flutter/painting/TextStyle/height.html

因此:即使两个 Text 组件都在 Center 中,字形在行盒中的“上下留白”并不必然对称(取决于字体 ascent/descent/leading),在滚轮的透视/缩放下这种差异会被放大。


为什么 height / strutStyle 能“看起来更居中”

1) TextStyle(height: 1.0):把行高锁到 EM-square,减少字体自带 metrics 漂移

当你显式设置 height: 1.0 时,行高会变成 fontSize * 1.0。它不等于“默认高度”,而是用 EM-square 来定义一个更可控的行盒。

这能减少的不是 baseline 本身,而是:

  • 行盒高度随字体 metrics 波动(leading/ascent/descent)的不确定性;
  • 不同字号/不同 run 在同一段落里导致的“行盒变化”。

滚轮里我们常做字号插值(中心大、两侧小),如果不固定行高,就更容易出现“上一行顶端被裁 1~2dp”或“上下间距不一致”的视觉错觉。

2) StrutStyle(forceStrutHeight: true):强制所有行使用统一的 strut 度量

StrutStyle 是 paragraph 级别的最小行高约束。文档写得很明确:

when true, all lines will be laid out with the height of the strut.
All line and run-specific metrics will be ignored/overridden.

来源:https://api.flutter.dev/flutter/painting/StrutStyle-class.html

在滚轮里如果你想要“机械等距”的观感,forceStrutHeight: true 的效果通常是:

  • 每一行的 ascent/descent 以 strut 为准;
  • 即便字号变化,行盒也更稳定;
  • glyph 在行盒中的位置波动更小,从而视觉中心更稳定

3) 为什么这能改善“居中”

“不居中”本质上是行盒内部的 glyph 分布 + 滚轮透视共同造成的。height/strut 并不会改变“baseline 体系”,但会改变:

  • 行盒高度(line box height)
  • ascent/descent 的分配
  • leading 的分配

当这些维度被固定后,glyph 的视觉中心更接近行盒中心,你就会感觉“居中变好了”,上下间距也更一致。


实战建议(滚轮/数字选择器场景)

  1. 优先保证足够的 itemExtent:itemExtent 太小,即使有 strut 也容易裁切。
  2. 固定行高TextStyle(height: 1.0) 通常是第一步。
  3. 需要强一致时再上 strut:尤其是字号插值/多字体时。
  4. 不要把 textAlign 当成竖直对齐工具:它只管水平方向。

附:你项目里的关联文件

  • lib/ui/pages/widgets/number_wheel_picker.dart
  • lib/ui/pages/pillow_device_setting/PillowDeviceSettingPage.dart