Flutter 字体字生效原理解析

一、说明

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

二、字体生效原理

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

Dart 复制代码
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

cpp 复制代码
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 对应的加载方式,我们继续查看源码:

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

进一步查看 platform_mac.mm

cpp 复制代码
// 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 你好!🌍"

Dart 复制代码
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 会先找到字体后再去尝试查找字重。

cpp 复制代码
// 查找字体
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

cpp 复制代码
sk_sp<SkTypeface> TypefaceFontStyleSet::matchStyle(const SkFontStyle& pattern) {
  return matchStyleCSS3(pattern);
}

SKFontMgr.cpp

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 的调用中我们发现这样一段逻辑:

cpp 复制代码
// 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 中为什么会有加粗的效果了。

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

cpp 复制代码
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
    • 小字体 :加粗效果适中,避免笔画粘连
    • 大字体 :加粗效果明显,保持视觉一致性
    • 跨平台 :根据不同平台的显示特性调整强度

举例:

cpp 复制代码
// 示例 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)。这种字符级匹配策略保障了混合文本(如英文+中文+表情)的正确渲染。

相关推荐
JIngles1233 小时前
flutter避免对widget图片作重复刷新(含实际图片发生变化或不发生变化)
flutter
雾沉川7 小时前
Flutter 入门开发环境完整搭建教程
学习·flutter
MemoriKu8 小时前
Flutter 本地 AI 相册工程收口:从屏幕常亮、标签体系到照片属性后台队列
大数据·人工智能·python·flutter·elasticsearch·搜索引擎·数据库架构
Prowler_925610 小时前
创新项目实训博客(十一):大模型智能标题生成与多级降维兜底策略
人工智能·flutter·aigc
不良使11 小时前
鸿蒙PC迁移_LocalSend 迁移到鸿蒙 PC:一次 Flutter + Rust + 三方库适配的完整记录
flutter·rust·harmonyos
恋猫de小郭12 小时前
由于 iOS 26 的键盘变化,Flutter 又要重构键盘区域逻辑
android·前端·flutter
风华圆舞1 天前
在 Flutter 鸿蒙项目里接入文本转语音的完整思路
flutter·华为·harmonyos
勤劳打代码1 天前
翻江倒海——滚动布局下拉视图管理
flutter·前端框架·开源
spmcor1 天前
Flutter 学习笔记 (6):路由与导航 —— 从基础 push/pop 到 go_router
flutter
风华圆舞2 天前
在 Flutter 鸿蒙项目里接入语音识别的完整思路
flutter·语音识别·harmonyos