在开发 Flutter Web 插件时,我遇到了一些有趣的现象:
-
Text:初始乱码 → 字体加载完成后自动变正常
-
CustomPainter:初始乱码 → 字体加载完成后 仍然乱码
为什么两者表现不同?
Text 会自动监听字体变更并重新 layout;CustomPainter需要手动处理。
一、核心结论
✔ Text 会自动刷新
因为 RenderParagraph 混入了 RelayoutWhenSystemFontsChangeMixin,会在字体变更事件到达时自动:
- 清理 TextPainter 缓存
- 重新 layout
- 重新绘制
所以字体加载完成后,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(); // 触发重绘
});
必须同时:
markNeedsLayout()清理 TextPainter 缓存repaint()或markNeedsPaint()才能真正刷新画布
否则仍然不会更新。
中文简介
三、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 断言
}
字体加载完成后会再次触发绘制,此时尺寸已被重新计算,不会再触发断言。