【Flutter Web】Text 自动刷新中文,CustomPainter 为什么不行?

66 阅读3分钟

在开发 Flutter Web 插件时,我遇到了一些有趣的现象:

  • Text:初始乱码 → 字体加载完成后自动变正常

  • CustomPainter:初始乱码 → 字体加载完成后 仍然乱码

image-20251127172929563.png

为什么两者表现不同?

Text 会自动监听字体变更并重新 layout;CustomPainter需要手动处理。

一、核心结论

✔ Text 会自动刷新

因为 RenderParagraph 混入了 RelayoutWhenSystemFontsChangeMixin,会在字体变更事件到达时自动:

  1. 清理 TextPainter 缓存
  2. 重新 layout
  3. 重新绘制

所以字体加载完成后,Text 会自动从乱码变成正常文字。


✘ CustomPainter 不会刷新

CustomPainter 不属于 RenderObject 体系,不会监听系统字体事件。 字体加载完成后,它内部的 TextPainter 仍然使用旧的 layoutCache → 继续渲染乱码。

要想生效,必须手动监听字体变更并重绘。

二、解决方法

适用于RenderObject(混入)

让你的 RenderObject 继承 Mixin:

class RenderCustom extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
  @override
  void systemFontsDidChange() {
    super.systemFontsDidChange();
    _textPainter.markNeedsLayout();
    markNeedsLayout();
  }
}

✔ 自动监听字体变更 ✔ 清理 TextPainter 缓存 ✔ 重新 layout / paint


适用于 CustomPainter(手动监听)

PaintingBinding.instance.systemFonts.addListener(() {
  painter.markNeedsLayout(); // 清缓存
  repaint();                 // 触发重绘
});

必须同时:

  1. markNeedsLayout() 清理 TextPainter 缓存
  2. repaint()markNeedsPaint() 才能真正刷新画布

否则仍然不会更新。

最终效果:demo

中文简介

三、Text 为什么能自动刷新?

1. Text → RichText → RenderParagraph

RenderParagraph 使用:

with RelayoutWhenSystemFontsChangeMixin

Mixin 在 attach 时注册监听:

PaintingBinding.instance.systemFonts.addListener(_scheduleSystemFontsUpdate);

字体变更后会调用:

void systemFontsDidChange() {
  _textPainter.markNeedsLayout();
}

即:

清除 layoutCache → 触发重新 layout → 文本刷新成功。

四、系统字体变更事件从哪里来?

当字体下载完毕:

sendFontChangeMessage(); // 发送 fontsChange 事件

Framework 层收到事件后:

_systemFonts.notifyListeners();

→ 所有监听字体变化的 RenderObject 会收到通知。

→ 但是 CustomPainter 没监听,所以什么都不会发生。

五、最终总结

组件会不会监听字体变更会不会自动刷新为什么
Text / RichText✔ 会✔ 会RenderParagraph + RelayoutWhenSystemFontsChangeMixin
CustomPainter✘ 不会✘ 不会不属于 RenderObject,未注册监听

要想让 CustomPainter 显示中文不乱码,你必须主动:

  • 监听 systemFonts
  • 清理 TextPainter 缓存
  • 触发 repaint

否则永远不会更新。

六、相关问题:字体加载触发 TextPainter paint 断言

在调试自定义绘制时,我遇到一个典型断言:

══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞════════════════════════════════════
Assertion failed:
.../text_painter.dart:1333:14
debugSize == size
is not true

本质原因

  • 修改 text 会将 _rebuildParagraphForPaint = true,表示需要在 paint 期间重建段落。
  • 但此时 layout() 已使用“乱码字体”完成过一次布局,并把该尺寸作为 debugSize(旧尺寸) 缓存。
  • 字体随后加载成功,正确字体的排版尺寸发生变化,此时的 size(新尺寸) 与旧尺寸不一致。
  • paint 阶段会执行以下断言检查缓存尺寸是否与当前布局尺寸一致:
assert(debugSize == size);
  • 由于 debugSize ≠ size,就会在 paint 期间触发断言。

修改 text 后,重新 layout 行不行?

TextPainter.layout() 内部有缓存判断:

final cachedLayout = _layoutCache;
if (cachedLayout != null &&
    cachedLayout._resizeToFit(minWidth, maxWidth, textWidthBasis)) {
  return;  // 缓存有效,直接跳过,不重新 layout
}

只要旧缓存满足当前约束,layout() 会直接返回,不会真正重新计算布局。 因此,layout 是否生效取决于缓存是否被清理


设置 text 时的行为

  • 如果新 text 与旧 text 尺寸发生变化 → TextPainter 会自动清理 layout 缓存。
  • 如果只是样式修改(颜色、字体粗细等不影响尺寸的属性) → 缓存不会被清理,调用 layout() 无效

手动清除缓存

对于第二种情况,必须显式清除缓存:

textPainter.markNeedsLayout();

只有清掉 _layoutCache,下一次 layout() 才会真正重新计算段落,否则 paint 阶段仍会使用旧尺寸。

实际解决方式

由于我只是修改了text的样式,在这种情况下layout也无效,等待系统字体变更后清空cache。这里直接忽略错误,不阻断绘制流程

try {
  tPainter.paint(
    canvas,
    Offset(0, mainHeight + layout.style.translationLineGap),
  );
} catch (_) {
  // 避免字体加载完成瞬间触发的 debugSize == size 断言
}

字体加载完成后会再次触发绘制,此时尺寸已被重新计算,不会再触发断言。