Android系统字体加载流程
一、背景
视觉同学提了一个需求,要求手机中显示的字体可以支持medium字体,经过分析,android原生的字体库中并没有中文的medium字体,如果使用bold,显示又太粗,为满足需求,需要分析android的系统字体加载流程。
二、分析
android系统字体的加载流程可以参考:http://blog.csdn.net/rjdeng/article/details/48545313
大致的流程是:Android的启动过程中Zygote的Preloading classes会加载frameworks/base下的Typeface.java类并执行其static块,在Typeface的static块中会对系统字体进行加载:
/* * (non-Javadoc) * * This should only be called once, from the static class initializer block. */ private static void init() { // Load font config and initialize Minikin state File systemFontConfigLocation = getSystemFontConfigLocation(); // 获得系统字体配置文件的位置 File configFilename = new File(systemFontConfigLocation, FONTS_CONFIG); // 创建操作system/etc/fonts.xml的文件对象 try { // 解析fonts.xml文件 FileInputStream fontsIn = new FileInputStream(configFilename); FontListParser.Config fontConfig = FontListParser.parse(fontsIn); Map bufferForPath = new HashMap(); List familyList = new ArrayList(); // Note that the default typeface is always present in the fallback list; // this is an enhancement from pre-Minikin behavior. for (int i = 0; i < fontConfig.families.size(); i++) { FontListParser.Family f = fontConfig.families.get(i); if (i == 0 || f.name == null) { familyList.add(makeFamilyFromParsed(f, bufferForPath)); } } sFallbackFonts = familyList.toArray(new FontFamily[familyList.size()]); setDefault(Typeface.createFromFamilies(sFallbackFonts)); // 加载系统字体并将其保存在sSystemFontMap中 Map systemFonts = new HashMap(); for (int i = 0; i < fontConfig.families.size(); i++) { Typeface typeface; FontListParser.Family f = fontConfig.families.get(i); if (f.name != null) { if (i == 0) { // The first entry is the default typeface; no sense in // duplicating the corresponding FontFamily. typeface = sDefaultTypeface; } else { FontFamily fontFamily = makeFamilyFromParsed(f, bufferForPath); FontFamily[] families = { fontFamily }; typeface = Typeface.createFromFamiliesWithDefault(families); } systemFonts.put(f.name, typeface); } } for (FontListParser.Alias alias : fontConfig.aliases) { Typeface base = systemFonts.get(alias.toName); Typeface newFace = base; int weight = alias.weight; if (weight != 400) { newFace = new Typeface(nativeCreateWeightAlias(base.native_instance, weight)); } systemFonts.put(alias.name, newFace); } sSystemFontMap = systemFonts; } catch (RuntimeException e) { Log.w(TAG, "Didn't create default family (most likely, non-Minikin build)", e); // TODO: normal in non-Minikin case, remove or make error when Minikin-only } catch (FileNotFoundException e) { Log.e(TAG, "Error opening " + configFilename, e); } catch (IOException e) { Log.e(TAG, "Error reading " + configFilename, e); } catch (XmlPullParserException e) { Log.e(TAG, "XML parse exception for " + configFilename, e); } } // 获得系统字体配置文件的位置 private static File getSystemFontConfigLocation() { return new File("/system/etc/"); } static final String FONTS_CONFIG = "fonts.xml"; // 系统字体配置文件名称
可以看到,在上面的代码中会解析/system/etc/fonts.xml(字体配置文件),并创建对应的Typeface,保存在sSystemFontMap中。fonts.xml其实就是所有的系统字体文件的配置文件,比如其中对中文的配置如下:
NotoSansSC-Regular.otf NotoSansTC-Regular.otf
在手机固件中,它所在的位置为/system/etc/,在android源码中,它所在的位置为frameworks/base/data/fonts/。
为了满足对中文medium字体的显示,我们可以添加一套中文的medium字体,其实,android原生是支持medium字体的,但是只支持英文,这一点可以在配置文件中看到:
Roboto-Thin.ttf Roboto-ThinItalic.ttf Roboto-Light.ttf Roboto-LightItalic.ttf Roboto-Regular.ttf Roboto-Italic.ttf Roboto-Medium.ttf Roboto-MediumItalic.ttf Roboto-Black.ttf Roboto-BlackItalic.ttf Roboto-Bold.ttf Roboto-BoldItalic.ttf
可以看到,英文的medium字体使用的是Roboto-Medium.ttf字体,所有当我们给TextView设置
android:fontFamily="sans-serif-medium"
只有英文可以显示medium字体的效果。所有,如果我们让中文也支持medium字体的效果,可以添加一套中文的medium字体,具体如下:
1、在frameworks/base/data/fonts/fonts.xml中
NotoSansHans-Regular.otf NotoSansSC-Medium.otf NotoSansHant-Regular.otf NotoSansTC-Medium.otf
2、在frameworks/base/data/fonts/fonts.mk的最后加入新加的字体文件
NotoSansSC-Medium.otf \ NotoSansTC-Medium.otf \
3、在frameworks/base/data/fonts/Android.mk的font_src_files最后加入新加的字体文件
font_src_files := \ Roboto-Bold.ttf \ Roboto-Italic.ttf \ Roboto-BoldItalic.ttf \ Clockopia.ttf \ AndroidClock.ttf \ AndroidClock_Highlight.ttf \ AndroidClock_Solid.ttf \ NotoSansSC-Medium.otf \ NotoSansTC-Medium.otf \
2、3是为了让系统能够将新加的字体编译到固件中
中文Medium字体下载:http://download.csdn.net/detail/xiao_nian/9772888
经过上面的修改,我们就将新的中文medium字体添加到android系统中了,编译固件,给TextView设置fontFamily为sans-serif-medium,中文也会有medium字重的效果,如下:
上面是使用了medium字体的效果,下面是正常的字体。
三、扩展
经过上面的修改,系统已经支持中文的medium字体了,这种方式要添加字体库,而一般的应用是无法修改源码的,经过研究,发现了另外一种简单的办法,同样可以达到medium字体的效果,代码如下:
textView.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL)); textView.getPaint().setStyle(Paint.Style.FILL_AND_STROKE); textView.getPaint().setStrokeWidth(1.2f);
通过修改TextView画笔的描边宽度,我们可以模拟medium字体的效果。
但是上面的方法有一个缺陷,那就是当textview中有emoji表情字符时,会导致emoji表情字符无法显示。所以这种方式只适用于没有emoji表情字符的textview中。
四、添加medium字体开关
添加medium字体后,又有用户反馈说系统字体显示太粗,看起来不太舒服,于是又有了新的需求,在设置中添加一个medium字体的开关,关闭开关之后屏蔽medium字体,打开之后系统可以显示medium字体。额,看起来好像有点棘手了,没办法,只能继续研究。前面分析到系统字体的加载是在Typeface的静态代码块中,那么我们是否可以考虑根据开关的打开情况加载medium字体呢?如果开关打开,就加载medium字体,不打开就不加载medium字体。
我们平时在代码中创建一个Typeface一般是这样创建的:
TextView textView = (TextView) findViewById(R.id.textView); Typeface font = Typeface.create("sans-serif-medium", Typeface.NORMAL); textView.setTypeface(font);
Typeface的create方法如下:
public static Typeface create(String familyName, int style) { if (sSystemFontMap != null) { return create(sSystemFontMap.get(familyName), style); // 从sSystemFontMap取出对应的字体 } return null; } public static Typeface create(Typeface family, int style) { if (style < 0 || style > 3) { style = 0; } long ni = 0; if (family != null) { // Return early if we're asked for the same face/style if (family.mStyle == style) { return family; } ni = family.native_instance; } Typeface typeface; SparseArray styles = sTypefaceCache.get(ni); if (styles != null) { typeface = styles.get(style); if (typeface != null) { return typeface; } } typeface = new Typeface(nativeCreateFromTypeface(ni, style)); // 如果family为null,那么ni=0,会使用默认的字体 if (styles == null) { styles = new SparseArray(4); sTypefaceCache.put(ni, styles); } styles.put(style, typeface); return typeface; }
可以看到,其实也就是从sSystemFontMap中保存的字体中找到对应的字体在根据style创建字体并保存再缓存中。如果我们在Typeface加载静态代码块时屏蔽medium字体,那么从sSystemFontMap中取出的medium字体就应该为空,系统就会返回一个正常的字体。故修改代码如下:
/* * (non-Javadoc) * * This should only be called once, from the static class initializer block. */ private static void init() { // Load font config and initialize Minikin state File systemFontConfigLocation = getSystemFontConfigLocation(); File configFilename = new File(systemFontConfigLocation, FONTS_CONFIG); try { FileInputStream fontsIn = new FileInputStream(configFilename); FontListParser.Config fontConfig = FontListParser.parse(fontsIn); Map bufferForPath = new HashMap(); List familyList = new ArrayList(); // Note that the default typeface is always present in the fallback list; // this is an enhancement from pre-Minikin behavior. for (int i = 0; i < fontConfig.families.size(); i++) { FontListParser.Family f = fontConfig.families.get(i); if (i == 0 || f.name == null) { familyList.add(makeFamilyFromParsed(f, bufferForPath)); } } sFallbackFonts = familyList.toArray(new FontFamily[familyList.size()]); setDefault(Typeface.createFromFamilies(sFallbackFonts)); //liunian Add {@ boolean isUseMediumFont = SystemProperties.get(PROPERTY_FLYME_MEDIUM_FONT, "true").equals("true"); // @} Map systemFonts = new HashMap(); for (int i = 0; i < fontConfig.families.size(); i++) { Typeface typeface; FontListParser.Family f = fontConfig.families.get(i); if (f.name != null) { if (i == 0) { // The first entry is the default typeface; no sense in // duplicating the corresponding FontFamily. typeface = sDefaultTypeface; } else { FontFamily fontFamily = makeFamilyFromParsed(f, bufferForPath); FontFamily[] families = { fontFamily }; typeface = Typeface.createFromFamiliesWithDefault(families); } //liunian Add {@ if (isUseMediumFont) { systemFonts.put(f.name, typeface); } else { if (!f.name.contentEquals("sans-serif-medium")) { systemFonts.put(f.name, typeface); } } // @} } } for (FontListParser.Alias alias : fontConfig.aliases) { Typeface base = systemFonts.get(alias.toName); Typeface newFace = base; int weight = alias.weight; if (weight != 400) { newFace = new Typeface(nativeCreateWeightAlias(base.native_instance, weight)); } //liunian Add {@ if (isUseMediumFont) { systemFonts.put(alias.name, newFace); } else { if (alias.name != null) { if (!alias.name.contentEquals("sans-serif-medium")) { systemFonts.put(alias.name, newFace); } } } // @} } sSystemFontMap = systemFonts; } catch (RuntimeException e) { Log.w(TAG, "Didn't create default family (most likely, non-Minikin build)", e); // TODO: normal in non-Minikin case, remove or make error when Minikin-only } catch (FileNotFoundException e) { Log.e(TAG, "Error opening " + configFilename, e); } catch (IOException e) { Log.e(TAG, "Error reading " + configFilename, e); } catch (XmlPullParserException e) { Log.e(TAG, "XML parse exception for " + configFilename, e); } }
可以看到,上面代码其实就是判断系统属性值PROPERTY_FLYME_MEDIUM_FONT是否为true,如果为true,则加载所有的字体,如果不为true,则不加载FontFamilyName为sans-serif-medium的字体。
然后在设置中添加一个开关,动态修改系统属性值PROPERTY_FLYME_MEDIUM_FONT即可达到目的。
采用这种方式有一个缺点,就是系统必须要重启才能生效,因为只有系统重启了才会重新加载Typeface文件并执行其静态代码块。如果要实时生效,可以考虑在Paint.java中修改其Typeface。
五、测试系统字体的方法
我们在修改系统字体时,并不是每次都需要编译固件才能看到效果,在手机固件中,系统字体文件会被放置到/system/fonts/目录下,而配置文件会放在/system/etc/目录下,我们可以直接修改后push到手机中。
-
目录
android字体库主要在以下两个目录
frameworks/base/data/fonts
external/noto-fonts
android6.0相对于android5.0目录结构有所调整。
具体可以网上查询相关资料。
测试方法
我们修改了字体,不用每次都编译固件测试,可以直接将对应的修改push到手机中。
android字体文件在手机中的路径:system/fonts/
android字体的配置文件在手机中的路径:system/etc/fonts.xml
我们可以先将对应的字体和配置文件pull到本地,修改完成后在push到对应的目录即可,具体步骤如下:
1、找到一台userdebug或eng的手机
2、将需要修改的字体库push到本地(比如缅甸字体)
3、修改字体库或者配置文件
4、将修改的字体库或者配置文件push到手机中
5、重启手机查看字体显示
更多相关文章
- Android开发 了解android系统的架构
- Qualcomm 高通芯片组与Android音频系统缺陷测评分析
- Android(安卓)WebView 开发详解(三)
- (转)演化理解 Android(安卓)异步加载图片
- android系统分区大小设置的经验值
- Android学习笔记(一)概述
- android sdcard存储方案(基于wrapfs文件系统)
- Android加载动态库不成功处理方法
- Android成功刷到beagle board ^_^