Android字体加重过程浅析

11/20/2022 源码

# Android字体加重的窘境

在App的设计稿中UI设计师一般都会使用选择相应的medium字体对字体进行加重而摒弃直接加粗的方式,这对ios开发者来说比较简单,但对Android开发者来说就比较尴尬,Android默认的中文字体,思源黑体并没有包含加重字体,如果简单粗暴的直接加粗又会与设计稿相差较大,至于为什么Android设置了medium字体后只有英文会生效,我们进入源码一探究竟

# 字体的初始化

直接从TextView源码入手

// 找到fontFamily解析位置
case com.android.internal.R.styleable.TextAppearance_fontFamily:
    ...

    // 首次为空,继续查找mFontFamily使用处
    if (attributes.mFontTypeface == null) {
        attributes.mFontFamily = appearance.getString(attr);
    }
    ...
    break;

找到这个方法内部使用了mFontFamily

private void applyTextAppearance(TextAppearanceAttributes attributes) {
    ...
    setTypefaceFromAttrs(attributes.mFontTypeface, attributes.mFontFamily,
            attributes.mTypefaceIndex, attributes.mTextStyle, attributes.mFontWeight);


private void setTypefaceFromAttrs(@Nullable Typeface typeface, @Nullable String familyName,
            @XMLTypefaceAttr int typefaceIndex, @Typeface.Style int style,
            @IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int weight) {
    if (typeface == null && familyName != null) {
        // 这里创建一个普通样式的字体
        final Typeface normalTypeface = Typeface.create(familyName, Typeface.NORMAL);
        resolveStyleAndSetTypeface(normalTypeface, style, weight);
    }
    ...
}

从系统默认的字体库中查找字体

public static Typeface create(String familyName, @Style int style) {
    return create(getSystemDefaultTypeface(familyName), style);
}

// 继续跟进看看sSystemFontMap是如何初始化的
private static Typeface getSystemDefaultTypeface(@NonNull String familyName) {
    Typeface tf = sSystemFontMap.get(familyName);
    return tf == null ? Typeface.DEFAULT : tf;
}

这里我们忽略懒加载的过程,直接找到这个静态方法,setSystemFontMap方法内部会对sSystemFontMap进行赋值

public static void loadPreinstalledSystemFontMap() {
    final FontConfig fontConfig = SystemFonts.getSystemPreinstalledFontConfig();
    final Map<String, FontFamily[]> fallback = SystemFonts.buildSystemFallback(fontConfig);
    final Map<String, Typeface> typefaceMap =
            SystemFonts.buildSystemTypefaces(fontConfig, fallback);
    setSystemFontMap(typefaceMap);
}

// 重点看下SystemFonts的第一个方法,其中找到字体配置的xml文件
public static @NonNull FontConfig getSystemPreinstalledFontConfig() {
    return getSystemFontConfigInternal(FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, null,
            0, 0);
}

到这里我们搞清楚了系统字体由/system/etc/fonts.xml进行配置,/system/fonts/目录进行字体的存储

private static final String FONTS_XML = "/system/etc/fonts.xml";
public static final String SYSTEM_FONT_DIR = "/system/fonts/";
private static final String OEM_XML = "/product/etc/fonts_customization.xml";
public static final String OEM_FONT_DIR = "/product/fonts/";

# 字体配置

打开fonts.xml

<!-- Roboto作为默认字体 -->
<family name="sans-serif">
    <font weight="100" style="normal">Roboto-Regular.ttf
        <axis tag="ital" stylevalue="0" />
        <axis tag="wdth" stylevalue="100" />
        <axis tag="wght" stylevalue="100" />
    </font>
    <font weight="200" style="normal">Roboto-Regular.ttf
        <axis tag="ital" stylevalue="0" />
        <axis tag="wdth" stylevalue="100" />
        <axis tag="wght" stylevalue="200" />
    </font>
    ...
</family>

可以看到默认字体的所有样式Roboto-Regular.ttf字体都包含了,接着我们找一下中文字体

<family lang="zh-Hans">
    <font weight="400" style="normal" index="2" postScriptName="NotoSansCJKjp-Regular">
        NotoSansCJK-Regular.ttc
    </font>
    <font weight="400" style="normal" index="2" fallbackFor="serif"
            postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc
    </font>
</family>

配置文件只配置了一个字重,我们打开字体再看下 fonts.jpg 确实,简体中文只有一个字重

# 国内厂商medium为何生效

前面我们提到Android默认的思源黑体并没有包含加重字体,但在小米华为等国内厂商的手机上却可以正常加重,看了它们的fonts.xml后发现,它们都修改了默认字体,使用了自研的包含有medium加重的中文字体 而当我们在这些手机的设置中将默认字体改回思源黑体后发现,加重效果又消失了

<!-- 默认字体已经改为其他字体 -->
<family name="sans-serif">
    <font weight="100" style="normal">HarmonyOS.ttf
        <axis tag="wght" stylevalue="100" />
    </font>
    <font weight="100" style="italic">HarmonyOS-Italic.ttf
        <axis tag="wght" stylevalue="100" />
    </font>
    <font weight="300" style="normal">HarmonyOS.ttf
        <axis tag="wght" stylevalue="247" />
    </font>
    <font weight="300" style="italic">HarmonyOS-Italic.ttf
        <axis tag="wght" stylevalue="247" />
    </font>
    <font weight="400" style="normal">HarmonyOS.ttf
        <axis tag="wght" stylevalue="400" />
    </font>
    <font weight="400" style="italic">HarmonyOS-Italic.ttf
        <axis tag="wght" stylevalue="400" />
    </font>
    <font weight="500" style="normal">HarmonyOS.ttf
        <axis tag="wght" stylevalue="500" />
    </font>
    <font weight="500" style="italic">HarmonyOS-Italic.ttf
        <axis tag="wght" stylevalue="500" />
    </font>
    <font weight="700" style="normal">HarmonyOS.ttf
        <axis tag="wght" stylevalue="706" />
    </font>
    <font weight="700" style="italic">HarmonyOS-Italic.ttf
        <axis tag="wght" stylevalue="700" />
    </font>
    <font weight="900" style="normal">HarmonyOS.ttf
        <axis tag="wght" stylevalue="844" />
    </font>
    <font weight="900" style="italic">HarmonyOS-Italic.ttf
        <axis tag="wght" stylevalue="844" />
    </font>
</family>

<!-- 中文字体也是 -->
<family lang="zh-Hans">
    <font weight="100" style="normal">HarmonyOSHans.ttf
        <axis tag="wght" stylevalue="100" />
    </font>

# 加粗过程浅析

我们继续看下blob加粗的关键细节 仍然从TextView入手

// blob被存入mTextStyle中
case com.android.internal.R.styleable.TextAppearance_textStyle:
    attributes.mTextStyle = appearance.getInt(attr, attributes.mTextStyle);

继续跟进mTextStyle,进入setTypefaceFromAttrs方法

private void setTypefaceFromAttrs(@Nullable Typeface typeface, @Nullable String familyName,
        @XMLTypefaceAttr int typefaceIndex, @Typeface.Style int style,
        @IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int weight) {
    if (typeface == null && familyName != null) {
        // 上面已经提到过会走这里,weight此时为-1
        final Typeface normalTypeface = Typeface.create(familyName, Typeface.NORMAL);
        resolveStyleAndSetTypeface(normalTypeface, style, weight);
    }
    ...
}

继续进入resolveStyleAndSetTypeface方法

private void resolveStyleAndSetTypeface(@NonNull Typeface typeface, @Typeface.Style int style,
        @IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int weight) {
    if (weight >= 0) {
        ...
    } else {
        // 走这里
        setTypeface(typeface, style);
    }
}

public void setTypeface(@Nullable Typeface tf, @Typeface.Style int style) {
    if (style > 0) {
        if (tf == null) {
            tf = Typeface.defaultFromStyle(style);
        } else {
            // 此时重新根据style创建新的字体了
            tf = Typeface.create(tf, style);
        }

        setTypeface(tf);
        int need = style & ~typefaceStyle;
        //textPaint的FakeBoldText也会被设置为true
        mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
    }
}

public static Typeface create(Typeface family, @Style int style) {
    ...
    Typeface typeface;
    synchronized (sStyledCacheLock) {
        ...
        //最终调用jni层的nativeCreateFromTypeface方法来创建
        typeface = new Typeface(nativeCreateFromTypeface(ni, style));
        styles.put(style, typeface);
    }
    return typeface;
}

我们继续跟进Typeface.cpp文件

// 实际的函数名为Typeface_createFromTypeface
static const JNINativeMethod gTypefaceMethods[] = {
        {"nativeCreateFromTypeface", "(JI)J", (void*)Typeface_createFromTypeface},


static jlong Typeface_createFromTypeface(JNIEnv* env, jobject, jlong familyHandle, jint style) {
    Typeface* family = toTypeface(familyHandle);
    // 调用createRelative方法
    Typeface* face = Typeface::createRelative(family, (Typeface::Style)style);
    ...
}

Typeface* Typeface::createRelative(Typeface* src, Typeface::Style style) {
    const Typeface* resolvedFace = Typeface::resolveDefault(src);
    Typeface* result = new Typeface;
    if (result != nullptr) {
        result->fFontCollection = resolvedFace->fFontCollection;
        result->fBaseWeight = resolvedFace->fBaseWeight;
        result->fAPIStyle = style;
        // style重新计算
        result->fStyle = computeRelativeStyle(result->fBaseWeight, style);
    }
    return result;
}

static minikin::FontStyle computeRelativeStyle(int baseWeight, Typeface::Style relativeStyle) {
    int weight = baseWeight;
    // 最终我们得知当style包含blob时,weight加上300,weight默认是400,blob其实就是使用的weight为700的字体
    if ((relativeStyle & Typeface::kBold) != 0) {
        weight += 300;
    }
    bool italic = (relativeStyle & Typeface::kItalic) != 0;
    return computeMinikinStyle(weight, italic);
}