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块中会对系统字体进行加载:

[java]  view plain  copy
  1. /* 
  2.  * (non-Javadoc) 
  3.  * 
  4.  * This should only be called once, from the static class initializer block. 
  5.  */  
  6. private static void init() {  
  7.     // Load font config and initialize Minikin state  
  8.     File systemFontConfigLocation = getSystemFontConfigLocation(); // 获得系统字体配置文件的位置  
  9.     File configFilename = new File(systemFontConfigLocation, FONTS_CONFIG); // 创建操作system/etc/fonts.xml的文件对象  
  10.     try {  
  11.         // 解析fonts.xml文件  
  12.         FileInputStream fontsIn = new FileInputStream(configFilename);  
  13.         FontListParser.Config fontConfig = FontListParser.parse(fontsIn);  
  14.   
  15.         Map<String, ByteBuffer> bufferForPath = new HashMap<String, ByteBuffer>();  
  16.   
  17.         List<FontFamily> familyList = new ArrayList<FontFamily>();  
  18.         // Note that the default typeface is always present in the fallback list;  
  19.         // this is an enhancement from pre-Minikin behavior.  
  20.         for (int i = 0; i < fontConfig.families.size(); i++) {  
  21.             FontListParser.Family f = fontConfig.families.get(i);  
  22.             if (i == 0 || f.name == null) {  
  23.                 familyList.add(makeFamilyFromParsed(f, bufferForPath));  
  24.             }  
  25.         }  
  26.         sFallbackFonts = familyList.toArray(new FontFamily[familyList.size()]);  
  27.         setDefault(Typeface.createFromFamilies(sFallbackFonts));  
  28.   
  29.         // 加载系统字体并将其保存在sSystemFontMap中  
  30.         Map<String, Typeface> systemFonts = new HashMap<String, Typeface>();  
  31.         for (int i = 0; i < fontConfig.families.size(); i++) {  
  32.             Typeface typeface;  
  33.             FontListParser.Family f = fontConfig.families.get(i);  
  34.             if (f.name != null) {  
  35.                 if (i == 0) {  
  36.                     // The first entry is the default typeface; no sense in  
  37.                     // duplicating the corresponding FontFamily.  
  38.                     typeface = sDefaultTypeface;  
  39.                 } else {  
  40.                     FontFamily fontFamily = makeFamilyFromParsed(f, bufferForPath);  
  41.                     FontFamily[] families = { fontFamily };  
  42.                     typeface = Typeface.createFromFamiliesWithDefault(families);  
  43.                 }  
  44.   
  45.                 systemFonts.put(f.name, typeface);  
  46.             }  
  47.         }  
  48.         for (FontListParser.Alias alias : fontConfig.aliases) {  
  49.             Typeface base = systemFonts.get(alias.toName);  
  50.             Typeface newFace = base;  
  51.             int weight = alias.weight;  
  52.             if (weight != 400) {  
  53.                 newFace = new Typeface(nativeCreateWeightAlias(base.native_instance, weight));  
  54.             }  
  55.             systemFonts.put(alias.name, newFace);  
  56.         }  
  57.         sSystemFontMap = systemFonts;  
  58.   
  59.     } catch (RuntimeException e) {  
  60.         Log.w(TAG, "Didn't create default family (most likely, non-Minikin build)", e);  
  61.         // TODO: normal in non-Minikin case, remove or make error when Minikin-only  
  62.     } catch (FileNotFoundException e) {  
  63.         Log.e(TAG, "Error opening " + configFilename, e);  
  64.     } catch (IOException e) {  
  65.         Log.e(TAG, "Error reading " + configFilename, e);  
  66.     } catch (XmlPullParserException e) {  
  67.         Log.e(TAG, "XML parse exception for " + configFilename, e);  
  68.     }  
  69. }  
  70.   
  71. // 获得系统字体配置文件的位置  
  72. private static File getSystemFontConfigLocation() {  
  73.     return new File("/system/etc/");  
  74. }  
  75.   
  76. static final String FONTS_CONFIG = "fonts.xml"// 系统字体配置文件名称  

可以看到,在上面的代码中会解析/system/etc/fonts.xml(字体配置文件),并创建对应的Typeface,保存在sSystemFontMap中。fonts.xml其实就是所有的系统字体文件的配置文件,比如其中对中文的配置如下:

[html]  view plain  copy
  1. <!-- 简体中文字体 -->  
  2. <family lang="zh-Hans">  
  3.     <font weight="400" style="normal">NotoSansSC-Regular.otf</font>  
  4. </family>  
  5. <!-- 繁体中文字体 -->  
  6. <family lang="zh-Hant">  
  7.     <font weight="400" style="normal">NotoSansTC-Regular.otf</font>  
  8. </family>  

在手机固件中,它所在的位置为/system/etc/,在android源码中,它所在的位置为frameworks/base/data/fonts/。

为了满足对中文medium字体的显示,我们可以添加一套中文的medium字体,其实,android原生是支持medium字体的,但是只支持英文,这一点可以在配置文件中看到:

[html]  view plain  copy
  1. <alias name="sans-serif-medium" to="sans-serif" weight="500" />  

[html]  view plain  copy
  1. <family name="sans-serif">  
  2.     <font weight="100" style="normal">Roboto-Thin.ttf</font>  
  3.     <font weight="100" style="italic">Roboto-ThinItalic.ttf</font>  
  4.     <font weight="300" style="normal">Roboto-Light.ttf</font>  
  5.     <font weight="300" style="italic">Roboto-LightItalic.ttf</font>  
  6.     <font weight="400" style="normal">Roboto-Regular.ttf</font>  
  7.     <font weight="400" style="italic">Roboto-Italic.ttf</font>  
  8.     <font weight="500" style="normal">Roboto-Medium.ttf</font>  
  9.     <font weight="500" style="italic">Roboto-MediumItalic.ttf</font>  
  10.     <font weight="900" style="normal">Roboto-Black.ttf</font>  
  11.     <font weight="900" style="italic">Roboto-BlackItalic.ttf</font>  
  12.     <font weight="700" style="normal">Roboto-Bold.ttf</font>  
  13.     <font weight="700" style="italic">Roboto-BoldItalic.ttf</font>  
  14. </family>  

可以看到,英文的medium字体使用的是Roboto-Medium.ttf字体,所有当我们给TextView设置
[html]  view plain  copy
  1. android:fontFamily="sans-serif-medium"  

只有英文可以显示medium字体的效果。所有,如果我们让中文也支持medium字体的效果,可以添加一套中文的medium字体,具体如下:

1、在frameworks/base/data/fonts/fonts.xml中

[html]  view plain  copy
  1. <family lang="zh-Hans">  
  2.     <font weight="400" style="normal">NotoSansHans-Regular.otf</font>  
  3.     <!-- 新加的简体中文medium字体 -->  
  4.     <font weight="500" style="normal">NotoSansSC-Medium.otf</font>  
  5. </family>  
  6. <family lang="zh-Hant">  
  7.     <font weight="400" style="normal">NotoSansHant-Regular.otf</font>  
  8.     <!-- 新加的繁体中文medium字体 -->  
  9.     <font weight="500" style="normal">NotoSansTC-Medium.otf</font>  
  10. </family>  

2、在frameworks/base/data/fonts/fonts.mk的最后加入新加的字体文件
[html]  view plain  copy
  1. NotoSansSC-Medium.otf \  
  2. NotoSansTC-Medium.otf \  

3、在frameworks/base/data/fonts/Android.mk的font_src_files最后加入新加的字体文件

[html]  view plain  copy
  1. font_src_files := \  
  2.     Roboto-Bold.ttf \  
  3.     Roboto-Italic.ttf \  
  4.     Roboto-BoldItalic.ttf \  
  5.     Clockopia.ttf \  
  6.     AndroidClock.ttf \  
  7.     AndroidClock_Highlight.ttf \  
  8.     AndroidClock_Solid.ttf \  
  9.     NotoSansSC-Medium.otf \  
  10.     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字体的效果,代码如下:

[java]  view plain  copy
  1. textView.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL));  
  2. textView.getPaint().setStyle(Paint.Style.FILL_AND_STROKE);  
  3. textView.getPaint().setStrokeWidth(1.2f);  

通过修改TextView画笔的描边宽度,我们可以模拟medium字体的效果。

但是上面的方法有一个缺陷,那就是当textview中有emoji表情字符时,会导致emoji表情字符无法显示。所以这种方式只适用于没有emoji表情字符的textview中。


四、添加medium字体开关

添加medium字体后,又有用户反馈说系统字体显示太粗,看起来不太舒服,于是又有了新的需求,在设置中添加一个medium字体的开关,关闭开关之后屏蔽medium字体,打开之后系统可以显示medium字体。额,看起来好像有点棘手了,没办法,只能继续研究。前面分析到系统字体的加载是在Typeface的静态代码块中,那么我们是否可以考虑根据开关的打开情况加载medium字体呢?如果开关打开,就加载medium字体,不打开就不加载medium字体。

我们平时在代码中创建一个Typeface一般是这样创建的:

[java]  view plain  copy
  1. TextView textView = (TextView) findViewById(R.id.textView);  
  2. Typeface font = Typeface.create("sans-serif-medium", Typeface.NORMAL);  
  3. textView.setTypeface(font);  

Typeface的create方法如下:

[java]  view plain  copy
  1.     public static Typeface create(String familyName, int style) {  
  2.         if (sSystemFontMap != null) {  
  3.             return create(sSystemFontMap.get(familyName), style); // 从sSystemFontMap取出对应的字体  
  4.         }  
  5.         return null;  
  6.     }  
  7.   
  8.     public static Typeface create(Typeface family, int style) {  
  9.         if (style < 0 || style > 3) {  
  10.             style = 0;  
  11.         }  
  12.         long ni = 0;  
  13.         if (family != null) {  
  14.             // Return early if we're asked for the same face/style  
  15.             if (family.mStyle == style) {  
  16.                 return family;  
  17.             }  
  18.   
  19.             ni = family.native_instance;  
  20.         }  
  21.   
  22.         Typeface typeface;  
  23.         SparseArray<Typeface> styles = sTypefaceCache.get(ni);  
  24.   
  25.         if (styles != null) {  
  26.             typeface = styles.get(style);  
  27.             if (typeface != null) {  
  28.                 return typeface;  
  29.             }  
  30.         }  
  31.   
  32.         typeface = new Typeface(nativeCreateFromTypeface(ni, style)); // 如果family为null,那么ni=0,会使用默认的字体  
  33.         if (styles == null) {  
  34.             styles = new SparseArray<Typeface>(4);  
  35.             sTypefaceCache.put(ni, styles);  
  36.         }  
  37.         styles.put(style, typeface);  
  38.   
  39.         return typeface;  
  40.     }  

可以看到,其实也就是从sSystemFontMap中保存的字体中找到对应的字体在根据style创建字体并保存再缓存中。如果我们在Typeface加载静态代码块时屏蔽medium字体,那么从sSystemFontMap中取出的medium字体就应该为空,系统就会返回一个正常的字体。故修改代码如下:

[java]  view plain  copy
  1. /* 
  2.  * (non-Javadoc) 
  3.  * 
  4.  * This should only be called once, from the static class initializer block. 
  5.  */  
  6. private static void init() {  
  7.     // Load font config and initialize Minikin state  
  8.     File systemFontConfigLocation = getSystemFontConfigLocation();  
  9.     File configFilename = new File(systemFontConfigLocation, FONTS_CONFIG);  
  10.     try {  
  11.         FileInputStream fontsIn = new FileInputStream(configFilename);  
  12.         FontListParser.Config fontConfig = FontListParser.parse(fontsIn);  
  13.   
  14.         Map<String, ByteBuffer> bufferForPath = new HashMap<String, ByteBuffer>();  
  15.   
  16.         List<FontFamily> familyList = new ArrayList<FontFamily>();  
  17.         // Note that the default typeface is always present in the fallback list;  
  18.         // this is an enhancement from pre-Minikin behavior.  
  19.         for (int i = 0; i < fontConfig.families.size(); i++) {  
  20.             FontListParser.Family f = fontConfig.families.get(i);  
  21.             if (i == 0 || f.name == null) {  
  22.                 familyList.add(makeFamilyFromParsed(f, bufferForPath));  
  23.             }  
  24.         }  
  25.         sFallbackFonts = familyList.toArray(new FontFamily[familyList.size()]);  
  26.         setDefault(Typeface.createFromFamilies(sFallbackFonts));  
  27.   
  28.         //liunian Add {@  
  29.         boolean isUseMediumFont = SystemProperties.get(PROPERTY_FLYME_MEDIUM_FONT, "true").equals("true");  
  30.         // @}  
  31.         Map<String, Typeface> systemFonts = new HashMap<String, Typeface>();  
  32.         for (int i = 0; i < fontConfig.families.size(); i++) {  
  33.             Typeface typeface;  
  34.             FontListParser.Family f = fontConfig.families.get(i);  
  35.             if (f.name != null) {  
  36.                 if (i == 0) {  
  37.                     // The first entry is the default typeface; no sense in  
  38.                     // duplicating the corresponding FontFamily.  
  39.                     typeface = sDefaultTypeface;  
  40.                 } else {  
  41.                     FontFamily fontFamily = makeFamilyFromParsed(f, bufferForPath);  
  42.                     FontFamily[] families = { fontFamily };  
  43.                     typeface = Typeface.createFromFamiliesWithDefault(families);  
  44.                 }  
  45.                 //liunian Add {@  
  46.                 if (isUseMediumFont) {  
  47.                     systemFonts.put(f.name, typeface);  
  48.                 } else {  
  49.                     if (!f.name.contentEquals("sans-serif-medium")) {  
  50.                         systemFonts.put(f.name, typeface);  
  51.                     }  
  52.                 }  
  53.                 // @}  
  54.             }  
  55.         }  
  56.         for (FontListParser.Alias alias : fontConfig.aliases) {  
  57.             Typeface base = systemFonts.get(alias.toName);  
  58.             Typeface newFace = base;  
  59.             int weight = alias.weight;  
  60.             if (weight != 400) {  
  61.                 newFace = new Typeface(nativeCreateWeightAlias(base.native_instance, weight));  
  62.             }  
  63.             //liunian Add {@  
  64.             if (isUseMediumFont) {  
  65.                 systemFonts.put(alias.name, newFace);  
  66.             } else {  
  67.                 if (alias.name != null) {  
  68.                     if (!alias.name.contentEquals("sans-serif-medium")) {  
  69.                         systemFonts.put(alias.name, newFace);  
  70.                     }  
  71.                 }  
  72.             }  
  73.             // @}  
  74.         }  
  75.         sSystemFontMap = systemFonts;  
  76.   
  77.     } catch (RuntimeException e) {  
  78.         Log.w(TAG, "Didn't create default family (most likely, non-Minikin build)", e);  
  79.         // TODO: normal in non-Minikin case, remove or make error when Minikin-only  
  80.     } catch (FileNotFoundException e) {  
  81.         Log.e(TAG, "Error opening " + configFilename, e);  
  82.     } catch (IOException e) {  
  83.         Log.e(TAG, "Error reading " + configFilename, e);  
  84.     } catch (XmlPullParserException e) {  
  85.         Log.e(TAG, "XML parse exception for " + configFilename, e);  
  86.     }  
  87. }  

可以看到,上面代码其实就是判断系统属性值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、重启手机查看字体显示

原文:https://blog.csdn.net/xiao_nian/article/details/60766475
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值