当前位置: 首页 > news >正文

Flutter 字体生效原理解析

一、说明

Flutter作为跨平台UI框架,其核心能力在于实现一套代码的多端一致性渲染。字体文本渲染作为保障应用UI视觉统一和文案展示规范的基础能力,在开发中具有关键作用。开发人员通常通过Dart层的TextStyle和全局ThemeData配置字体家族、字重和样式,即可实现Android/iOS多平台的正常文本展示。

二、字体生效原理

我们在创建 MaterialApp 的时候会传入 ThemeData,示例:

MaterialApp( theme: ThemeData( fontFamily: "CustomFont", useMaterial3: true, ), );

在构造函数中,如果你配置了自己的字体则会用自己的,如果没有配置,Flutter 会为你配置一套主题字体。

theme_data.dart

我们再看一下 typography.dart 干了什么

根据不同的平台配置不同的字体

Android 默认主题

iOS 默认主题

所以在 Framework 层其实已经做了兜底策略,你如果不手动设置的话,它会配置一套系统的默认字体。

源码中提到 iOS 的字体使用了 San Francisco 的字体主题。

A Material Design text theme with dark glyphs based on San Francisco.

这儿的 CupertinoSystemDisplay 和 CupertinoSystemText 名称是怎样去找到系统的 SF 字体的呢?

首先,Flutter 引擎层会通过不同的字体管理器去获取不同的字体。

引擎层源码:font_collection.cc

std::vector<sk_sp<SkTypeface>> FontCollection::findTypefaces(const std::vector<SkString>& familyNames, SkFontStyle fontStyle, const std::optional<FontArguments>& fontArgs) { std::vector<sk_sp<SkTypeface>> typefaces; for (const SkString& familyName : familyNames) { // 匹配字体 sk_sp<SkTypeface> match = matchTypeface(familyName, fontStyle); if (match && fontArgs) { match = fontArgs->CloneTypeface(match); } if (match) { typefaces.emplace_back(std::move(match)); } } return typefaces; } // 查找字体 sk_sp<SkTypeface> FontCollection::matchTypeface(const SkString& familyName, SkFontStyle fontStyle) { for (const auto& manager : this->getFontManagerOrder()) { // 匹配字体 sk_sp<SkFontStyleSet> set(manager->matchFamily(familyName.c_str())); // 没找到字体找下一个 if (!set || set->count() == 0) { continue; } // 如果找到了就匹配样式:字重、宽度、斜体等 sk_sp<SkTypeface> match(set->matchStyle(fontStyle)); if (match) { return match; } } return nullptr; } // 获取字体管理器数组 std::vector<sk_sp<SkFontMgr>> FontCollection::getFontManagerOrder() const { std::vector<sk_sp<SkFontMgr>> order; if (fDynamicFontManager) { order.push_back(fDynamicFontManager); } if (fAssetFontManager) { order.push_back(fAssetFontManager); } if (fTestFontManager) { order.push_back(fTestFontManager); } if (fDefaultFontManager && fEnableFontFallback) { order.push_back(fDefaultFontManager); } return order; }

flutter 会维护 4 种不同的字体管理器,查找字体的时候会根据顺序依次查找:

  1. DynamicFontManager :系统特定字体(如iOS的SF Pro Display)和动态注册的字体。
  2. AssetFontManager :pubspec注册的应用字体(自定义字体)
  3. TestFontManager :仅用于测试
  4. DefaultFontManager :平台默认字体(如iOS的SF Pro Text)

当使用 CupertinoSystemText 时 :

  1. 先在动态字体管理器中查找,找不到
  2. 再在资源字体管理器中查找,找不到
  3. 最终在默认字体管理器(CoreText)中找到SF Pro Text
  4. CoreText根据获取到 SF Pro Text

CupertinoSystemDisplay 对应的 SF Pro Display 对应的加载方式,我们继续查看源码:

FontCollection::RegisterFonts( const std::shared_ptr<AssetManager>& asset_manager) { #if FML_OS_MACOSX || FML_OS_IOS RegisterSystemFonts(*dynamic_font_manager_); #endif

进一步查看 platform_mac.mm

// Apple system font larger than size 29 returns SFProDisplay typeface. static const CGFloat kSFProDisplayBreakPoint = 29; // Font name represents the "SF Pro Display" system font on Apple platforms. static const std::string kSFProDisplayName = "CupertinoSystemDisplay"; void RegisterSystemFonts(const DynamicFontManager& dynamic_font_manager) { auto register_weighted_font = [&dynamic_font_manager](const int weight) { sk_sp<SkTypeface> large_system_font_weighted = SkMakeTypefaceFromCTFont(MatchSystemUIFont(weight, kSFProDisplayBreakPoint)); if (large_system_font_weighted) { dynamic_font_manager.font_provider().RegisterTypeface(large_system_font_weighted, kSFProDisplayName); } }; for (int i = 0; i < 8; i++) { const int font_weight = i * 100; register_weighted_font(font_weight); } // The value 780 returns a font weight of 800. register_weighted_font(780); // The value of 810 returns a font weight of 900. register_weighted_font(810); }

使用 MatchSystemUIFont 函数通过CoreText的 CTFontCreateUIFontForLanguage 获取系统字体。

由于 CoreText 是 Apple 开发的闭源系统库,用于文本渲染和排版。Apple 只提供该函数的声明(头文件) 和 二进制库 ,不开放实现源码,我们只能看到这一层。

指定字体大小为 kSFProDisplayBreakPoint (29)确保获取的是SF Pro Display字体。

通过 dynamic_font_manager.font_provider().RegisterTypeface 将获取到的SF Pro Display字体注册为 CupertinoSystemDisplay 名称,这样Flutter框架就可以通过这个名称使用SF字体。

而安卓主题注释信息中有提到

A Material Design text theme with dark glyphs based on Roboto.

它是基于 robot 字体的主题。

以上的两种字体只是英文的,为什么没有提到中文呢?

进一步查看 text_style.dart 中的说明

/// The fallback order is:

///

/// * [fontFamily]

/// * [fontFamilyFallback] in order of first to last.

/// * System fallback fonts which will vary depending on platform. Flutter

通过智能字体回退机制来解决找不到字体的问题。回退顺序为:

  1. fontFamily (主要字体 - Roboto)
  2. fontFamilyFallback (自定义回退字体列表 - Android 默认为空)
  3. System fallback fonts (系统回退字体 - 关键!)

我们从源码中可以看到底层并未设置 fontFamilyFallback,所以它主要是通过系统层的回退机制去查找的。

安卓系统的回退机制为:Roboto (主要字体) → Noto Sans CJK (中文) → Noto Color Emoji (表情) → 其他系统字体 iOS 的回退机制为:San Francisco → PingFang SC → 系统默认中文字体

举例:Flutter 在安卓端显示混合文本:"Hello 你好!🌍"

Text("Hello 你好!🌍")

字符级别的字体查找:

  1. 'H' → Roboto ✅ (使用 Roboto)
  2. 'e' → Roboto ✅ (使用 Roboto)
  3. 'l' → Roboto ✅ (使用 Roboto)
  4. 'l' → Roboto ✅ (使用 Roboto)
  5. 'o' → Roboto ✅ (使用 Roboto)
  6. ' ' → Roboto ✅ (使用 Roboto)
  7. '你' → Roboto ❌ → 系统回退 → Noto Sans CJK ✅ (使用 Noto Sans CJK)
  8. '好' → Roboto ❌ → 系统回退 → Noto Sans CJK ✅ (使用 Noto Sans CJK)
  9. '!' → Roboto ✅ (使用 Roboto)
  10. '🌍' → Roboto ❌ → 系统回退 → Noto Color Emoji ✅ (使用 Noto Color Emoji)

所以,在不手动设置 Flutter 字体的情况下,其使用的是系统默认的字体。

三、字重生效原理

Flutter 会先找到字体后再去尝试查找字重。

// 查找字体 sk_sp<SkTypeface> FontCollection::matchTypeface(const SkString& familyName, SkFontStyle fontStyle) { for (const auto& manager : this->getFontManagerOrder()) { // 匹配字体 sk_sp<SkFontStyleSet> set(manager->matchFamily(familyName.c_str())); // 没找到字体找下一个 if (!set || set->count() == 0) { continue; } // 如果找到了就匹配样式:字重、宽度、斜体等 sk_sp<SkTypeface> match(set->matchStyle(fontStyle)); if (match) { return match; } } return nullptr; }

matchStyle 部分又做了什么呢?

typeface_font_asset_provider.cc

sk_sp<SkTypeface> TypefaceFontStyleSet::matchStyle(const SkFontStyle& pattern) { return matchStyleCSS3(pattern); }

SKFontMgr.cpp

/** * Width has the greatest priority. * If the value of pattern.width is 5 (normal) or less, * narrower width values are checked first, then wider values. * If the value of pattern.width is greater than 5 (normal), * wider values are checked first, followed by narrower values. * * Italic/Oblique has the next highest priority. * If italic requested and there is some italic font, use it. * If oblique requested and there is some oblique font, use it. * If italic requested and there is some oblique font, use it. * If oblique requested and there is some italic font, use it. * * Exact match. * If pattern.weight < 400, weights below pattern.weight are checked * in descending order followed by weights above pattern.weight * in ascending order until a match is found. * If pattern.weight > 500, weights above pattern.weight are checked * in ascending order followed by weights below pattern.weight * in descending order until a match is found. * If pattern.weight is 400, 500 is checked first * and then the rule for pattern.weight < 400 is used. * If pattern.weight is 500, 400 is checked first * and then the rule for pattern.weight < 400 is used. */ sk_sp<SkTypeface> SkFontStyleSet::matchStyleCSS3(const SkFontStyle& pattern) { int count = this->count(); if (0 == count) { return nullptr; } struct Score { int score; int index; Score& operator +=(int rhs) { this->score += rhs; return *this; } Score& operator <<=(int rhs) { this->score <<= rhs; return *this; } bool operator <(const Score& that) { return this->score < that.score; } }; Score maxScore = { 0, 0 }; for (int i = 0; i < count; ++i) { SkFontStyle current; this->getStyle(i, &current, nullptr); Score currentScore = { 0, i }; // CSS weight / SkFontStyle::Weight // The 'closer' to the target weight, the higher the score. // 1000 is the 'heaviest' recognized weight if (pattern.weight() == current.weight()) { currentScore += 1000; // less than 400 prefer lighter weights } else if (pattern.weight() < 400) { if (current.weight() <= pattern.weight()) { currentScore += 1000 - pattern.weight() + current.weight(); } else { currentScore += 1000 - current.weight(); } // between 400 and 500 prefer heavier up to 500, then lighter weights } else if (pattern.weight() <= 500) { if (current.weight() >= pattern.weight() && current.weight() <= 500) { currentScore += 1000 + pattern.weight() - current.weight(); } else if (current.weight() <= pattern.weight()) { currentScore += 500 + current.weight(); } else { currentScore += 1000 - current.weight(); } // greater than 500 prefer heavier weights } else if (pattern.weight() > 500) { if (current.weight() > pattern.weight()) { currentScore += 1000 + pattern.weight() - current.weight(); } else { currentScore += current.weight(); } } if (maxScore < currentScore) { maxScore = currentScore; } } return this->createTypeface(maxScore.index); }

这个时候会按照 CSS Fonts Module Level 3 规范中的规则去匹配字重,匹配算法遵循标准:

  • 评分系统 :为每种字体样式计算一个匹配得分
  • 字重匹配 :根据目标字重与当前字体字重的差异计算得分

按照上面的逻辑来看,如果某个字体只有一种字重的话,应该是不管设置多大的字重,都只会使用自带的字重,比如 thin 的字重是 300,应该是将字重设置为 100 到 900,它都只会使用 300 的字重。我们搞个 demo 实践一下:

从 demo 中发现 2 个问题:

  1. 如果设置的字重比它自身的字重小的话,以字体的实际字重兜底。举例:thin 是 300 的字重,你给它设置 100 的时候它也是 300。
  2. 当大到一定程度,它会自动加粗,但加粗的效果好像不如真实的。举例:thin 虽然只有 300 的字重,但是从 600 开始变得粗了,但它这儿的 600 不如真实的 600 字重粗。

问题:为什么会造成这样的情况呢?

skia/modules/skparagraph/src/OneLineShaper.cpp

在 matchResolvedFonts 的调用中我们发现这样一段逻辑:

// Apply fake bold and/or italic settings to the font if the typeface's attributes do not match the intended font style. int wantedWeight = block.fStyle.getFontStyle().weight(); bool fakeBold = wantedWeight >= 600 && wantedWeight - font.getTypeface()->fontStyle().weight() >= 200; font.setEmbolden(fakeBold);

判断逻辑为:

目标字重大于等于 600,并且与找到的字重相差大于等于200,才会触发模拟加粗,否则直接用找到的字重去绘制。 到这儿也就能解释上面的 demo 中为什么会有加粗的效果了。

底层是怎样模拟加粗的呢?

skia/src/ports/SkFontHost_FreeType.cpp #ifndef SK_OUTLINE_EMBOLDEN_DIVISOR #ifdef __ANDROID__ #define SK_OUTLINE_EMBOLDEN_DIVISOR 34 #else #define SK_OUTLINE_EMBOLDEN_DIVISOR 24 #endif #endif // 加粗强度 const FT_Pos strength = FT_MulFix(face->units_per_EM, face->size->metrics.y_scale) / SK_OUTLINE_EMBOLDEN_DIVISOR; // 应用合成加粗 return 0 == FT_Outline_Embolden(&glyph->outline, strength);
  • FT_Pos :FreeType 的位置类型,通常是 32 位整数,使用 16.16 定点数格式(高 16 位整数部分,低 16 位小数部分)
  • face->units_per_EM :字体的设计大小,通常固定为 1000 或 2048,单位是字体设计单位
  • face->size->metrics.y_scale :当前字体大小的 Y 轴缩放因子,将字体设计单位转换为设备像素
  • FT_MulFix(a, b) :FreeType 提供的定点数乘法函数
  • SK_OUTLINE_EMBOLDEN_DIVISOR :控制加粗强度的除数,根据平台不同为 24 或 34
    • 小字体 :加粗效果适中,避免笔画粘连
    • 大字体 :加粗效果明显,保持视觉一致性
    • 跨平台 :根据不同平台的显示特性调整强度

举例:

// 示例 1:12pt 字体,units_per_EM = 1000,y_scale = 0.012 strength = FT_MulFix(1000, 0.012 * 65536) / 24 = (1000 * 786.432) / 24 = 786432 / 24 = 32768 (约等于 0.5 像素) // 示例 2:24pt 字体,units_per_EM = 1000,y_scale = 0.024 strength = FT_MulFix(1000, 0.024 * 65536) / 24 = (1000 * 1572.864) / 24 = 1572864 / 24 = 65536 (约等于 1 像素)

所以,字体越大,加粗效果越明显。

所以,字重的整体匹配逻辑如下:

四、总结

Flutter作为跨平台框架,通过统一的字体文本渲染机制确保多端UI一致性。开发人员可通过TextStyle和ThemeData配置字体家族、字重等属性,框架默认提供Android的Roboto和iOS的San Francisco作为兜底字体。

字体加载流程依赖引擎层的动态管理器、资源管理器等四类管理器按序查找。iOS通过CoreText闭源库获取系统字体,例如CupertinoSystemDisplay对应SF Pro Display,安卓则基于Roboto主题。

对于中文等非默认字体,Flutter采用智能回退机制:优先匹配主字体,失败后依次尝试自定义回退列表和系统回退字体(如安卓的Noto Sans CJK)。这种字符级匹配策略保障了混合文本(如英文+中文+表情)的正确渲染。

http://www.jsqmd.com/news/769311/

相关文章:

  • 品牌擦片机制造商哪家好 - mypinpai
  • 喜茶代金券回收哪里好 怎么操作更省心 - 畅回收小程序
  • 企业如何利用多模型聚合平台优化AI应用开发成本与效率
  • 别再被web-view盖住了!用uniapp的cover-view给小程序网页加个授权弹窗(附完整代码)
  • 你的Python脚本吃掉了多少内存?用psutil进行程序性能分析与资源泄漏排查实战
  • 解决方案:PvZ Toolkit如何通过内存注入技术重塑植物大战僵尸的游戏体验?
  • 深入ZynqMP启动流程:从BootROM到Linux桌面,一张图看懂Petalinux每个文件的作用
  • 2026年贵阳装修公司排名完全指南:从预算透明到品质交付的深度横评 - 年度推荐企业名录
  • Step3.5 Flash 大模型技术深度解析:稀疏 MoE、混合注意力与 MTP 的高效推理革命
  • 选购酒店床上用品,哪个品牌好? - mypinpai
  • 2026年贵阳装修公司排名|闭口合同+VR设计+环保承诺的靠谱整装公司怎么选 - 年度推荐企业名录
  • 【微波辐射】基于matlab模拟综合孔径微波辐射成像仿真,含校正前后傅氏反演图像 Y阵型反演图像
  • TensorFlow模型快速部署:基于Gradio的AI演示界面构建指南
  • 免费解锁电脑性能的完整指南:Universal x86 Tuning Utility终极教程
  • 卡梅德生物技术快报:微生物基因敲入工程化构建甘露醇高产菌株
  • 2026年郫都区西装定制哪家靠谱?琪诺服装口碑佳 - mypinpai
  • 年省超200万!除垢剂实战案例深度解析 - 速递信息
  • **零基础小白用 GitHub 和 OpenCode 写代码入门教程(超详细保姆级)**
  • SARAH技术解析:实时自适应动作生成的突破与应用
  • 告别复制粘贴!用STM32CubeMX HAL库驱动ESP8266的保姆级避坑指南
  • 玫瑰痤疮可用防晒霜推荐来了,这4款温和防晒修护力拉满 - 全网最美
  • 远程命令执行系统架构设计:从Agent模型到gRPC安全通信实践
  • MakeFile编译管理工具
  • Go function - 有关function我能告诉你的一切
  • 神经网络参数化缩放(µP)原理与实践指南
  • Claude Code 免费使用指南:free-claude-code 代理方案全解析(2026)
  • 2026年贵阳装修公司排名指南:预算透明+环保可信的五大靠谱品牌深度横评 - 年度推荐企业名录
  • 模型量化鲁棒性优化:学习率调度与权重平均技术
  • dnSpy配置管理实战:从个人工作流到团队协作的进阶指南
  • 蓝牙5.3到底升级了啥?手把手教你为IoT设备选型避坑