Flutter 字体渲染与字重控制原理
基于 Flutter 3.27.4 源码分析,深入理解字体从 Widget 到引擎渲染的全链路,以及 PingFang SC 在写中文时如何区分字重。
一、核心结论
Flutter 框架本身不感知具体字体(如 PingFang SC)的内部结构 ------它只负责将 fontWeight 的抽象值(100-900)通过 dart:ui 编码后传递给 C++ 引擎,由引擎调用平台 API(CoreText / HarfBuzz / DirectWrite)进行实际的字体匹配。
PingFang SC 字重区分的关键在于 CoreText 的字重映射 :由于 PingFang SC 仅有 6 档静态字重文件(最重到 Semibold),设置任何超过 w600 的字重最终都只会落到 Semibold 上。这导致中文加粗(FontWeight.bold)在 iOS 上比英文 SF Pro 的 Bold 轻一个级别。
二、整体架构:四层渲染管线
vbnet
┌─────────────────────────────────────────────────────┐
│ Widget 层 │
│ Text → RichText → RenderParagraph │
│ (packages/flutter/lib/src/widgets/text.dart) │
├─────────────────────────────────────────────────────┤
│ Painting 层 (Framework Dart) │
│ TextPainter → TextStyle → TextSpan.build() │
│ (packages/flutter/lib/src/painting/) │
├─────────────────────────────────────────────────────┤
│ dart:ui 层 (Engine Dart Binding) │
│ ParagraphBuilder → _NativeParagraphBuilder.pushStyle│
│ (bin/cache/pkg/sky_engine/lib/ui/text.dart) │
├─────────────────────────────────────────────────────┤
│ Engine 层 (C++) │
│ Skia/Impeller → HarfBuzz → CoreText/DirectWrite │
│ 平台字体匹配 │
└─────────────────────────────────────────────────────┘
各层职责说明
| 层级 | 核心类/文件 | 对字重的处理 |
|------|-----------|------------|
| Widget 层 | Text, RichText, RenderParagraph | 样式继承、无障碍加粗叠加 |
| Painting 层 | TextStyle.fontWeight, TextPainter | 存储字重抽象值,编码到 Int32List |
| dart:ui 层 | FontWeight, ParagraphBuilder.pushStyle | 通过 @Native FFI 调用 C++ 引擎 |
| Engine 层 | Skia/Impeller + HarfBuzz + 平台 API | 将抽象字重映射到具体字体文件的 weight 轴 |
三、各层级源码逐层分析
3.1 Widget 层:样式继承与传递
文件 : packages/flutter/lib/src/widgets/text.dart
Text widget 的 build 方法(第 659 行):
dart
Widget build(BuildContext context) {
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
TextStyle? effectiveTextStyle = style;
if (style == null || style!.inherit) {
effectiveTextStyle = defaultTextStyle.style.merge(style); // 合并继承样式
}
if (MediaQuery.boldTextOf(context)) {
// 无障碍"加粗文本"设置 → 强制叠加 FontWeight.bold
effectiveTextStyle = effectiveTextStyle!.merge(const TextStyle(fontWeight: FontWeight.bold));
}
// ... 最终构建 RichText 或 _SelectableTextContainer
result = RichText(
text: TextSpan(
style: effectiveTextStyle,
text: data,
),
// ... textAlign, textDirection, softWrap 等参数透传
);
}
关键点:
-
若用户未设
fontWeight,优先从DefaultTextStyle继承 -
MediaQuery.boldTextOf(context)在系统无障碍开启"加粗文本"时自动强制加粗 -
Text.rich构造函数的TextSpan也可以独立指定fontWeight,用于富文本
3.2 Painting 层:TextStyle 与字重存储
3.2.1 TextStyle.fontWeight 字段
文件 : packages/flutter/lib/src/painting/text_style.dart
dart
class TextStyle with Diagnosticable {
/// The typeface thickness to use when painting the text (e.g., bold).
final FontWeight? fontWeight; // 第 627 行
}
fontWeight 在 TextStyle 中只是一个可空字段,不参与 merge 时若 inherit: true 会从祖先样式继承。
3.2.2 编码过程:encodeTextStyle
文件 : packages/flutter/lib/src/painting/text_style.dart(第 1563 行)
框架的 TextStyle 在传递给 dart:ui 前需要进行编码:
dart
Int32List _encodeTextStyle(...) {
final Int32List result = Int32List(9);
// ...
if (fontWeight != null) {
result[0] |= 1 << 5; // 第 5 位标记 fontWeigh 非空
result[5] = fontWeight.index; // 存储 index 值 (0~8)
}
// ...
return result;
}
编码布局(与 C++ ParagraphBuilder::pushStyle 对齐):
| 数组元素 | 含义 | 字重相关 |
|---------|------|---------|
| result[0] | 位掩码,标记哪些字段非空 | Bit 5 = fontWeight |
| result[5] | fontWeight.index | 0=w100 ... 8=w900 |
3.2.3 TextSpan.build → ParagraphBuilder.pushStyle
文件 : packages/flutter/lib/src/painting/text_span.dart(第 275 行)
dart
void build(ui.ParagraphBuilder builder, {TextScaler textScaler, ...}) {
if (hasStyle) {
builder.pushStyle(style!.getTextStyle(textScaler: textScaler));
// ↑ 将 Framework TextStyle → dart:ui TextStyle
// fontWeight 直接透传
}
if (text != null) {
builder.addText(text!);
}
// 递归处理子节点...
if (hasStyle) {
builder.pop();
}
}
文件 : packages/flutter/lib/src/painting/text_style.dart(第 1301 行)
dart
ui.TextStyle getTextStyle({...}) {
return ui.TextStyle(
fontWeight: fontWeight, // 直接透传给 dart:ui 层
fontStyle: fontStyle,
fontFamily: fontFamily,
fontSize: textScaler.scale(this.fontSize ?? 10.0),
// ...
);
}
3.3 dart:ui 层:FontWeight 类定义与原生 Bridge
3.3.1 FontWeight 类
文件 : bin/cache/pkg/sky_engine/lib/ui/text.dart(第 57 行)
dart
class FontWeight {
const FontWeight._(this.index, this.value);
final int index; // 0..8,用于编码和插值
final int value; // 100..900,对应 CSS font-weight 规范
static const FontWeight w100 = FontWeight._(0, 100); // Thin
static const FontWeight w200 = FontWeight._(1, 200); // ExtraLight
static const FontWeight w300 = FontWeight._(2, 300); // Light
static const FontWeight w400 = FontWeight._(3, 400); // Normal / regular
static const FontWeight w500 = FontWeight._(4, 500); // Medium
static const FontWeight w600 = FontWeight._(5, 600); // Semi-bold
static const FontWeight w700 = FontWeight._(6, 700); // Bold
static const FontWeight w800 = FontWeight._(7, 800); // Extra-bold
static const FontWeight w900 = FontWeight._(8, 900); // Black
static const FontWeight normal = w400;
static const FontWeight bold = w700;
}
index和value的区别:index(0-8)用于 Dart 侧编码和lerp插值,value(100-900)遵守 CSS 规范,传递给引擎用于字体匹配。
3.3.2 NativeParagraphBuilder.pushStyle --- 进入 C++ 引擎的关口
文件 : bin/cache/pkg/sky_engine/lib/ui/text.dart(第 3536 行)
dart
@Native<Void Function(Pointer<Void>, Handle, Handle, Double, Double, Double, Double, Double, Handle, Handle, Handle, Handle, Handle, Handle, Handle, Handle)>
external void _pushStyle(
Int32List encoded, // 包含 fontWeight.index
List<Object?> fontFamilies,
double fontSize,
double letterSpacing,
double wordSpacing,
double height,
double decorationThickness,
String locale,
List<Object?>? backgroundObjects,
ByteData? backgroundData,
List<Object?>? foregroundObjects,
ByteData? foregroundData,
ByteData shadowsData,
ByteData? fontFeaturesData,
ByteData? fontVariationsData,
);
encoded 的第 5 个元素(fontWeight.index)被随同 fontFamilies(如 ["PingFang SC"])一起通过 FFI 传给 C++ 引擎。
3.4 Engine 层:平台字体匹配(C++)
引擎端(Flutter Engine 的 paragraph_builder.cc)收到 pushStyle 调用后:
-
解析
encoded数组,提取fontWeight.index -
结合
fontFamily(如"PingFang SC")构造 Skia Typeface 请求 -
Skia 通过 HarfBuzz 进行 shaping 时,将 weight 映射到字体文件的 weight 轴
-
macOS/iOS :HarfBuzz → CoreText →
CTFontCreateWithName匹配最佳字体 -
Android/Linux:HarfBuzz → FontConfig / DirectWrite 匹配
四、PingFang SC 字重区分深度分析
4.1 Apple 平台字体文件分布
PingFang SC 作为 Apple 系统默认中文字体,由多个独立静态字体文件组成(不是可变字体):
| FontWeight | Flutter 常量 | CSS Weight | 实际匹配的 ttc/otf 文件 |
|------------|-----------------|-----------|---------------------------|
| w100 | FontWeight.w100 | 100 | PingFangSC-Ultralight.otf |
| w200 | FontWeight.w200 | 200 | PingFangSC-Thin.otf |
| w300 | FontWeight.w300 | 300 | PingFangSC-Light.otf |
| w400 | normal | 400 | PingFangSC-Regular.otf |
| w500 | w500 | 500 | PingFangSC-Medium.otf |
| w600 | w600 | 600 | PingFangSC-Semibold.otf |
| w700 | bold / w700 | 700 | ⚠️ Fallback 到 Semibold (w600) |
| w800 | w800 | 800 | ⚠️ Fallback 到 Semibold (w600) |
| w900 | w900 | 900 | ⚠️ Fallback 到 Semibold (w600) |
4.2 英文 vs 中文字重对比
| 特性 | 英文 (SF Pro) | 中文 (PingFang SC) |
|------|--------------|-------------------|
| 字体类型 | 可变字体 (Variable Font) | 静态字体合集 |
| 支持 wght 轴 | ✅ 是,值域 1-1000 | ❌ 否 |
| FontWeight.bold | 真正 Bold (w700) 字重 | ⚠️ 仅 Semibold (w600) 级别 |
| FontVariation.weight() | ✅ 平滑调节任意字重 | ❌ 不支持 |
| 最大字重 | w900 (Black) | w600 (Semibold) |
4.3 CoreText 匹配逻辑
当 Flutter Engine 调用 CoreText 的字体匹配 API 时(伪代码):
c
// CoreText 根据 CSS weight 匹配字重的逻辑
CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes(
(CFDictionaryRef)@{
(id)kCTFontFamilyNameAttribute: @"PingFang SC",
(id)kCTFontWeightTrait: @(weightValue),
}
);
CTFontRef font = CTFontCreateWithFontDescriptor(descriptor, size, NULL);
CoreText 的 kCTFontWeightTrait 取值范围为 -1.0(Ultralight) 到 1.0(Black),Flutter 传递的 CSS weight(100-900)会被 CoreText 内部映射到最近的可用字体文件:
| CSS Weight 区间 | CoreText map | 匹配的字体文件 |
|-----------------|-------------|----------------------|
| 0-174 | -0.7~-0.4 | PingFangSC-Ultralight |
| 175-274 | -0.4~-0.3 | PingFangSC-Thin |
| 275-374 | -0.3~-0.2 | PingFangSC-Light |
| 375-474 | 0.0 | PingFangSC-Regular |
| 475-574 | 0.23 | PingFangSC-Medium |
| 575-1000 | 0.3~1.0 | PingFangSC-Semibold |
| | | |
这就是为什么 FontWeight.bold (w700) 在 PingFang SC 上只显示 Semibold 粗细的根本原因------字体文件中没有真正的 Bold、ExtraBold、Black 字重,CoreText 无法生成比 Semibold 更粗的汉字。
五、Material Design CJK 字重处理
5.1 ScriptCategory 机制
文件 : packages/flutter/lib/src/material/typography.dart
Material Design 根据脚本类型使用不同的几何主题(字号、字重、基线):
dart
enum ScriptCategory {
englishLike, // 拉丁字母
dense, // 中/日/韩(CJK)--- 密集脚本
tall, // 印地/泰语 --- 高瘦脚本
}
在 Typography.geometryThemeFor(第 298 行):
dart
TextTheme geometryThemeFor(ScriptCategory scriptCategory) {
return switch (scriptCategory) {
ScriptCategory.englishLike => englishLike,
ScriptCategory.dense => dense, // 中文走这里
ScriptCategory.tall => tall,
};
}
5.2 dense 主题的字重定义
dart
static const TextTheme dense2018 = TextTheme(
displayLarge: TextStyle(debugLabel: 'dense displayLarge 2018',
fontSize: 96.0, fontWeight: FontWeight.w100), // 极细
displayMedium: TextStyle(debugLabel: 'dense displayMedium 2018',
fontSize: 60.0, fontWeight: FontWeight.w100),
titleLarge: TextStyle(debugLabel: 'dense titleLarge 2018',
fontSize: 21.0, fontWeight: FontWeight.w500),
bodyLarge: TextStyle(debugLabel: 'dense bodyLarge 2018',
fontSize: 17.0, fontWeight: FontWeight.w400),
// ...
);
关键点:
-
**没有指定
fontFamily**→ 使用系统默认中文字体(iOS = PingFang SC, Android = Noto Sans CJK) -
字重选用
w100/w400/w500,避开了 PingFang SC 不支持的w700+ -
textBaseline: TextBaseline.ideographic--- CJK 使用表意基线
5.3 中文实际使用的字重映射(Material 默认)
| Material Text Style | dense 字重 | iOS 实际显示 | Android 实际显示 |
|--------------------|-----------|------------|-----------------|
| displayLarge | w100 | Ultralight | Thin |
| titleLarge | w500 | Medium | Medium |
| bodyLarge | w400 | Regular | Regular |
| labelLarge | w500 | Medium | Medium |
六、平台对比
6.1 各平台默认中文字体
| 平台 | 默认中文系统字体 | 字体类型 | 支持字重范围 | 可变字体支持 |
|---------|----------------|--------------|--------------------|------------|
| iOS | PingFang SC | 静态字体合集 | 100-600 (无 Bold) | ❌ |
| macOS | PingFang SC | 静态字体合集 | 100-600 (无 Bold) | ❌ |
| Android | Noto Sans CJK | 可变字体 | 100-900 全支持 | ✅ wght 轴 |
| Web | 系统默认 | 取决于字体 | 取决于字体 | 取决于字体 |
6.2 双端统一到 PingFang SC 视觉效果的方案
核心目标:让 Android 和 iOS 的中文文本都呈现出 PingFang SC 的视觉效果。
难点:PingFang SC 是 Apple 系统专有字体,不可再分发。Android 上无法直接使用系统自带的 PingFang SC。同时 PingFang SC 的笔画比 Noto Sans CJK 更细------同样标称 w400,PingFang SC Regular 肉眼看起来就比 Noto Sans CJK Regular 细一些。
方案一:双端统一使用视觉匹配 PingFang SC 的开源字体(推荐)
最好的替代品是阿里巴巴普惠体 3.0 (Alibaba PuHuiTi) ,它是中文界公认最接近 PingFang SC 视觉风格的开源字体(阿里巴巴官方出品,免费可商用)。同时,OPPO Sans 风格也相当接近。
| 字体 | 与 PingFang SC 相似度 | 字重档位 | 精简后体积 | License |
|------|---------------------|---------|-----------|---------|
| 阿里巴巴普惠体 3.0 | ⭐⭐⭐⭐⭐ 极高 | 6 档 (w300-w800) | ~2-4MB | 免费可商用 |
| OPPO Sans | ⭐⭐⭐⭐ 高 | 5 档 | ~2-3MB | 免费可商用 |
| Noto Sans CJK SC | ⭐⭐⭐ 中等(偏几何感) | 7 档 (w100-w900) | ~3-5MB | Apache 2.0 |
| 思源黑体 SC | ⭐⭐⭐ 中等(与 Noto 同源) | 7 档 | ~3-5MB | SIL OpenFont |
流程 :下载字体文件(.ttf/.otf) → 放入
fonts/目录 →pubspec.yaml配置 → 全局应用。
dart
// ─── 1. pubspec.yaml ───
flutter:
fonts:
- family: AlibabaPuHuiTi
fonts:
- asset: fonts/AlibabaPuHuiTi-3-45-Light.ttf
weight: 300
- asset: fonts/AlibabaPuHuiTi-3-55-Regular.ttf
weight: 400
- asset: fonts/AlibabaPuHuiTi-3-65-Medium.ttf
weight: 500
- asset: fonts/AlibabaPuHuiTi-3-75-SemiBold.ttf
weight: 600
- asset: fonts/AlibabaPuHuiTi-3-85-Bold.ttf
weight: 700
- asset: fonts/AlibabaPuHuiTi-3-95-ExtraBold.ttf
weight: 800
// ─── 2. Theme 全局配置(双端效果完全一致) ───
MaterialApp(
theme: ThemeData(
textTheme: Typography.material2021().black.apply(
fontFamily: 'AlibabaPuHuiTi',
),
),
)
字重映射对照(确保双端一致):
| Flutter 写法 | 字体文件 | 视觉效果 |
|------------------|------------------------------|--------------|
| FontWeight.w300 | AlibabaPuHuiTi-3-45-Light | 极细 |
| FontWeight.w400 | AlibabaPuHuiTi-3-55-Regular | 正文 |
| FontWeight.w500 | AlibabaPuHuiTi-3-65-Medium | 中等强调 |
| FontWeight.w600 | AlibabaPuHuiTi-3-75-SemiBold | 半粗 |
| FontWeight.w700 | AlibabaPuHuiTi-3-85-Bold | ✅ 真正有 Bold |
| FontWeight.w800 | AlibabaPuHuiTi-3-95-ExtraBold| 特粗 |
与 PingFang SC 不同,阿里巴巴普惠体 有真正的 Bold(w700) 字重文件 ,所以
FontWeight.bold在双端表现一致,不会出现 PingFang SC 上 w700 降级到 Semibold 的问题。
方案二:Android 端利用可变字体微调来模拟 PingFang SC(零包体积)
如果不想添加任何额外字体文件(零包体积),可以利用 Android Noto Sans CJK 是可变字体 的特性,用 FontVariation.weight 精确调节字重值,使 Android 上每个字重档位的视觉粗细与 iOS PingFang SC 保持一致。
核心问题:同样标称 w400,PingFang SC 比 Noto Sans CJK 细。 通过肉眼对比,得出近似映射:
| 语义 | Flutter 标准写法 | PingFang SC 实际字重 | Android Noto Sans CJK 的字重调节值 | 说明 |
|-----|----------------|--------------------|-------------------------------|------|
| 极细 | w100 | Ultralight | FontVariation.weight(80) | Noto 调得更轻匹配 Ultralight |
| 特细 | w200 | Thin | FontVariation.weight(180) | 调轻 |
| 细体 | w300 | Light | FontVariation.weight(280) | 调轻 |
| 正文 | w400 | Regular | FontVariation.weight(**370**) | 关键!比 Noto 默认轻 |
| 中等 | w500 | Medium | FontVariation.weight(**480**) | 略轻 |
| 半粗 | w600 | Semibold | FontVariation.weight(**580**) | 略轻 |
| 粗体 | w700 | ⚠️ Semibold (降级) | FontVariation.weight(**650**) | 收敛到与 iOS 一致的 Semibold |
| 特粗 | w800 | ⚠️ Semibold (降级) | FontVariation.weight(**700**) | 收敛 |
| 最粗 | w900 | ⚠️ Semibold (降级) | FontVariation.weight(**750**) | 收敛 |
dart
// ─── 完整实现:双端统一到 PingFang SC 视觉效果(零包体积) ───
class PingFangCompat {
/// iOS 使用系统 PingFang SC,Android 用 Noto Sans CJK 可变字体微调
static TextStyle textStyle({
required FontWeight fontWeight,
double? fontSize,
Color? color,
// ... 其他 TextStyle 参数
}) {
if (defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS) {
// iOS/macOS:直接使用系统 PingFang SC + 字重上限校准
return TextStyle(
fontFamily: 'PingFang SC',
fontWeight: _capForPingFang(fontWeight),
fontSize: fontSize,
color: color,
);
} else {
// Android:用 Noto Sans CJK + FontVariation 模拟 PingFang SC 视觉
return TextStyle(
fontFamily: 'sans-serif', // 系统默认中文 = Noto Sans CJK
fontWeight: fontWeight, // 保持标准字重声明
fontSize: fontSize,
color: color,
fontVariations: [
FontVariation.weight(_pingFangMappedWeight(fontWeight)),
],
);
}
}
/// PingFang SC 字重上限:超过 w600 全部降级到 Semibold
static FontWeight _capForPingFang(FontWeight w) {
return w.index > FontWeight.w600.index ? FontWeight.w600 : w;
}
/// Android 上通过可变字体微调,模拟 PingFang SC 的视觉粗细
static double _pingFangMappedWeight(FontWeight w) {
return switch (w) {
FontWeight.w100 => 80,
FontWeight.w200 => 180,
FontWeight.w300 => 280,
FontWeight.w400 => 370, // ← 核心:比 Noto 默认轻
FontWeight.w500 => 480,
FontWeight.w600 => 580,
FontWeight.w700 => 650, // ← 收敛到 Semibold,匹配 iOS
FontWeight.w800 => 700,
FontWeight.w900 => 750,
_ => w.value.toDouble(),
};
}
}
// ─── 使用方式 ───
Text(
'中文文本,双端都是 PingFang SC 视觉效果',
style: PingFangCompat.textStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: Colors.black,
),
)
// ─── 也可以封装统一组件 ───
class PingFangText extends StatelessWidget {
const PingFangText(
this.data, {
super.key,
this.fontWeight = FontWeight.w400,
this.fontSize,
this.color,
this.textAlign,
this.maxLines,
});
final String data;
final FontWeight fontWeight;
final double? fontSize;
final Color? color;
final TextAlign? textAlign;
final int? maxLines;
@override
Widget build(BuildContext context) {
return Text(
data,
style: PingFangCompat.textStyle(
fontWeight: fontWeight,
fontSize: fontSize,
color: color,
),
textAlign: textAlign,
maxLines: maxLines,
);
}
}
方案三:双端统一使用阿里巴巴普惠体(最推荐 --- 完美一致)
方案二虽然零包体积,但 FontVariation.weight 的映射值需要人工调优,且仅 Android 10+ 支持。最可靠的方案是在双端使用同一套字体文件(阿里巴巴普惠体):
dart
// ─── 封装统一 PingFang 风格文本 ───
class PingFangStyleText extends StatelessWidget {
const PingFangStyleText(
this.data, {
super.key,
this.style,
this.textAlign,
this.maxLines,
this.overflow,
});
final String data;
final TextStyle? style;
final TextAlign? textAlign;
final int? maxLines;
final TextOverflow? overflow;
@override
Widget build(BuildContext context) {
return Text(
data,
style: (style ?? const TextStyle()).copyWith(
fontFamily: 'AlibabaPuHuiTi', // 双端统一
),
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
);
}
}
// ─── 使用 ───
// iOS 效果:AlibabaPuHuiTi-Medium
// Android 效果:AlibabaPuHuiTi-Medium(完全一致)
PingFangStyleText(
'双端完全一致的 PingFang 风格文本',
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16),
)
方案四:iOS 用系统 PingFang SC,Android 用 Alibaba PuHuiTi
如果希望 iOS 始终使用真正的系统 PingFang SC(而不是替代字体),Android 使用视觉匹配的阿里巴巴普惠体:
dart
class PlatformPingFangText extends StatelessWidget {
const PlatformPingFangText(
this.data, {
super.key,
this.style,
this.textAlign,
this.maxLines,
});
final String data;
final TextStyle? style;
final TextAlign? textAlign;
final int? maxLines;
static String get _fontFamily {
if (defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS) {
return 'PingFang SC'; // Apple 平台用真正的 PingFang SC
}
return 'AlibabaPuHuiTi'; // Android 用视觉匹配的开源字体
}
@override
Widget build(BuildContext context) {
return Text(
data,
style: (style ?? const TextStyle()).copyWith(
fontFamily: _fontFamily,
fontWeight: _calibrateWeight(style?.fontWeight),
),
textAlign: textAlign,
maxLines: maxLines,
);
}
/// 字重校准(避免 PingFang SC 上 w700 无效)
FontWeight? _calibrateWeight(FontWeight? w) {
if (w == null) return null;
if (_fontFamily == 'PingFang SC' && w.index > FontWeight.w600.index) {
return FontWeight.w600;
}
return w;
}
}
方案对比与选型建议
| 方案 | 双端视觉效果 | 包体积 | 实施难度 | 适用场景 |
|-----|------------|-------|---------|---------|
| 方案一:阿里巴巴普惠体 (双端统一字体) | ✅ 完全一致 | ⚠️ ~2-4MB | ⭐ 低 | 最推荐,一劳永逸 |
| 方案二:可变字体微调(零包体积) | ✅ 接近一致(需调参) | ✅ 无 | ⭐⭐⭐ 中 | 包体积敏感、仅 Android 10+ |
| 方案三:AlibabaPuHuiTi 封装组件 | ✅ 完全一致 | ⚠️ ~2-4MB | ⭐ 低 | 效果最可靠 |
| 方案四:PingFang+iOS + PuHuiTi+Android | ✅ 基本一致 | ⚠️ ~2-4MB | ⭐⭐ 低中 | iOS 优先使用原版字体 |
推荐的最务实施路线:
-
下载阿里巴巴普惠体 3.0 → 配置到
pubspec.yaml→ Theme 层全局设置fontFamily: 'AlibabaPuHuiTi' -
双端所有中文文本自动使用同一套字体,字重、字形、行距完全一致
-
FontWeight.bold等所有字重在两端的表现完全相同,不再需要任何校准适配层 -
如果还要精简包体积,可以只打包需要的字重档位(通常 Regular + Medium + Semibold + Bold 即可)
七、完整数据流汇总
css
开发者写代码:
Text('你好', style: TextStyle(fontFamily: 'PingFang SC', fontWeight: FontWeight.w500))
│
▼
Framework Widget 层 (text.dart:659)
→ DefaultTextStyle.of(context) + merge(style)
→ 得到 effectiveTextStyle: {fontFamily: 'PingFang SC', fontWeight: w500}
│
▼
Framework Painting 层 (text_style.dart:1301)
→ effectiveTextStyle.getTextStyle()
→ ui.TextStyle(fontWeight: FontWeight.w500)
│
▼
Framework TextSpan (text_span.dart:283)
→ builder.pushStyle(textStyle)
│
▼
dart:ui ParagraphBuilder (text.dart:3536)
→ _encodeTextStyle: result[5] = fontWeight.index // = 4
→ _pushStyle(encoded, fontFamilies: ["PingFang SC"], ...)
│
▼ @Native FFI
Engine C++ (paragraph_builder.cc)
解析 encoded → weight = 500
│
▼
Skia Typeface Matching
→ familyName = "PingFang SC", weight = 500
│
▼
┌─── macOS/iOS ─────────────────┐
│ CoreText CTFontCreateWithName │
│ → PingFangSC-Medium.otf │
└──────────────────────────────┘
│
▼
HarfBuzz Shaping
→ 用 PingFangSC-Medium 字体的 glyph 数据
→ 输出字形位置和轮廓
│
▼
Skia/Impeller Rendering
→ canvas.drawParagraph()
→ 光栅化到屏幕上
八、FontWeight 插值与动画
文件 : bin/cache/pkg/sky_engine/lib/ui/text.dart(第 128 行)
dart
static FontWeight? lerp(FontWeight? a, FontWeight? b, double t) {
if (a == null && b == null) return null;
return values[_lerpInt(
(a ?? normal).index,
(b ?? normal).index,
t,
).round().clamp(0, 8)];
}
-
基于 index(0-8)而非 value(100-900)进行插值
-
结果是离散的,只会在 9 个标准字重间跳变
-
若需平滑字重动画,应使用 可变字体的
FontVariation.weight(仅支持可变字体的平台)
dart
// 平滑字重动画(仅可变字体支持)
TextStyle(
fontVariations: [FontVariation.weight(700.0)],
)
九、参考资料
| 源码文件 | 路径 |
|---------|------|
| Text Widget | packages/flutter/lib/src/widgets/text.dart |
| RichText / RenderParagraph | packages/flutter/lib/src/widgets/basic.dart |
| RenderParagraph | packages/flutter/lib/src/rendering/paragraph.dart |
| TextStyle (框架) | packages/flutter/lib/src/painting/text_style.dart |
| TextPainter | packages/flutter/lib/src/painting/text_painter.dart |
| TextSpan | packages/flutter/lib/src/painting/text_span.dart |
| TextStyle / FontWeight (dart:ui) | bin/cache/pkg/sky_engine/lib/ui/text.dart |
| Material Typography | packages/flutter/lib/src/material/typography.dart |
分析基于 Flutter 3.27.4 (d8a9f9a52e),2025-01-31 发布。