Flutter 字体渲染与字重

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 行

}

fontWeightTextStyle 中只是一个可空字段,不参与 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;

}

indexvalue 的区别: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 调用后:

  1. 解析 encoded 数组,提取 fontWeight.index

  2. 结合 fontFamily(如 "PingFang SC")构造 Skia Typeface 请求

  3. Skia 通过 HarfBuzz 进行 shaping 时,将 weight 映射到字体文件的 weight 轴

  4. macOS/iOS :HarfBuzz → CoreText → CTFontCreateWithName 匹配最佳字体

  5. 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 优先使用原版字体 |

推荐的最务实施路线:

  1. 下载阿里巴巴普惠体 3.0 → 配置到 pubspec.yaml → Theme 层全局设置 fontFamily: 'AlibabaPuHuiTi'

  2. 双端所有中文文本自动使用同一套字体,字重、字形、行距完全一致

  3. FontWeight.bold 等所有字重在两端的表现完全相同,不再需要任何校准适配层

  4. 如果还要精简包体积,可以只打包需要的字重档位(通常 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 发布。

相关推荐
恋猫de小郭6 小时前
Flutter 开发怎么做 Agent ?从工程实战详细给你解读下
android·前端·flutter
binbin_527 小时前
Flutter 调用鸿蒙原生组件:MethodChannel 与 PlatformView 的选择和落地
开发语言·深度学习·flutter·harmonyos
GitLqr17 小时前
Flutter 3.44 插件内置 Kotlin (KGP) 双向兼容适配指南
android·flutter·dart
SoaringHeart3 天前
Flutter进阶:基于 EasyRefresh 的下拉刷新封装 n_easy_refresh_mixin.dart
前端·flutter
月光下的丝瓜4 天前
Flutter 国内安装指南
前端·flutter
恋猫de小郭6 天前
Amper 正式转正 Kotlin Toolchain ,Gradle 未来何去何从
android·前端·flutter
张风捷特烈6 天前
Flutter 类库大揭秘#02 | path_provider 各平台实现
前端·flutter
TT_Close7 天前
别劝退了!5秒搞定 Flutter 鸿蒙 FVM 起跑线
flutter·harmonyos·visual studio code
你听得到117 天前
用户说 App 卡,但说不清在哪?我把 Flutter 监控 SDK 升级成了链路观测工作台
前端·flutter·性能优化