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

相关推荐
maaath2 小时前
【无标题】Flutter for OpenHarmony 的文具手账应用开发实践
flutter·华为·harmonyos
里欧跑得慢2 小时前
Flutter 主题管理:构建一致的用户界面
前端·css·flutter·web
liulian091616 小时前
Flutter for OpenHarmony 跨平台开发:单位转换功能实战指南
flutter
千码君201617 小时前
Trae:一些关于flutter和 go前后端开发构建的分享
android·flutter·gradle·android-studio·trae·vibe code
maaath19 小时前
【maaath】Flutter for OpenHarmony 手表配饰应用实战开发
flutter·华为·harmonyos
maaath19 小时前
【maaath】Flutter for OpenHarmony 跨平台计算器应用开发实践
flutter·华为·harmonyos
maaath1 天前
【maaath】Flutter for OpenHarmony 闹钟时钟应用开发实战
flutter·华为·harmonyos
maaath1 天前
【maaath】Flutter for OpenHarmony 短信管理应用实战
flutter·华为·harmonyos
maaath1 天前
【maaath】Flutter for OpenHarmony打造跨平台便签备忘录应用
flutter·华为·harmonyos