Android字体加重过程浅析
aopmeta 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>
配置文件只配置了一个字重,我们打开字体再看下 确实,简体中文只有一个字重
# 国内厂商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);
}