最近公司設計部門提出要將應用的字體全部替換成思源黑體,當時我想到Android 5.0
之后不是系統提供有思源黑體嗎,這個應該非常簡單實現。 但卻在真正實現的時候發現了一些障礙:
1:由于我們的應用已經迭代很多版本,也相對穩定了。項目中的TextView就非常多。但Android API并沒有提供一個合適的API可以在全局的style或者theme一鍵替換成指定的字體。 目前發現只有通過
TextView.setTypeface()
去單個實現2:手機系統里的字體存放在哪個位置,?我怎么樣才能加載到它們。(ps:百度,google一下,里面全是教你如何替換整個系統字體,可能我查找的方式不對,有好的文章可以討論一下)。如果是將字體直接放在程序內部那將大大提升APK的大小,據我查看NotoSansHans這幾個自重隨便一個都有8MB左右.
3: 國內手機廠家非常多,各廠家的資源庫根本不統一。我們應該采用哪種方式動態全局加載統一字體呢,?
首先看到第一個問題:本來以為是配置一下什么的就可以了,但發現卻是個體力活。~ 難道我要把項目里面的所有TextView
,Button
之類的控件全部拿出來設置一個setTypeface()
嗎?這個我堅決不干。當時第一個反應想到就是在view tree
中能不能想點招。
如果不太懂這一塊的朋友可以看下activity 中view的創建過程。
參考 Android應用程序窗口(Activity)的視圖對象(View)的創建過程分析
皇天不負有心人,我在github上找到了一個老外的框架:Calligraphy.
該項目是在的核心部分就是重寫的Context.LayoutInflater
, 由于細節很多在這里就不重點講解框架細節了,我會通過之后的文章在細致的分析Calligraphy
的源碼部分。
我們附上Calligraphy框架API核心代碼:
public class CalligraphyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
CalligraphyConfig.initDefault(new CalligraphyConfig.Builder()
.setDefaultFontPath("fonts/Roboto-ThinItalic.ttf")//指定字體
.setFontAttrId(R.attr.fontPath)
.build()
);
}
}
然后在需要在Activity中或者BaseActivity中實現
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase));
}
就這樣可以達到全局替換,有沒有很方便,很簡單,?
但問題來了, 我們有沒有發現在setDefaultFontPath()
里面的的路徑他居然是fonts/***,?這明顯不是系統路徑呀? 怎么辦?那解決問題最好的辦法看看Calligraphy
的源碼了。
終于,~~ 我在Calligraphy
框架中的TypefaceUtils
找到關鍵代碼:
/**
* A helper loading a custom font.
*
* @param assetManager App's asset manager.
* @param filePath The path of the file.
* @return Return {@link android.graphics.Typeface} or null if the path is invalid.
*/
public static Typeface load(final AssetManager assetManager, final String filePath) {
synchronized (sCachedFonts) {
try {
if (!sCachedFonts.containsKey(filePath)) {
//這一行就是關鍵
final Typeface typeface = Typeface.createFromAsset(assetManager, filePath);
sCachedFonts.put(filePath, typeface);
return typeface;
}
} catch (Exception e) {
Log.w("Calligraphy", "Can't create asset from " + filePath + ". Make sure you have passed in the correct path and file name.", e);
sCachedFonts.put(filePath, null);
return null;
}
return sCachedFonts.get(filePath);
}
}
我們發現,原來fonts/**只是一個相對路徑,根目錄而是我們項目中的assets目錄。如果是這樣,我們必須將第三方字體移植到項目中來,那這樣就遇到了我們第二個問題。系統字體在什么位置,?字體文件這么大。我如果加載到項目的assets目錄中來,apk肯定大到不行。就是我愿意,相信領導應該也不愿意吧!
系統字體的位置很好找到,就在/system/fonts/下,但字體這么大,我一定需要把他copy到項目中來嗎,?
下面附圖:

可以看到,NotoSansHans(思源黑體,漢文)之類的字體真的這么大。。怎么破?隨便一個都8MB左右,這一套子重copy進來那得40多MB。
明顯,如果為了個字體就讓APK文件莫名多出幾十MB,這不現實。
這個時候唯一的辦法就是修改框架。怎么修改呢,?
剛才我們看到如果TypefaceUtils
的load方法,其中關鍵的一句:
public static Typeface load(final AssetManager assetManager, final String filePath) {
synchronized (sCachedFonts) {
try {
if (!sCachedFonts.containsKey(filePath)) {
//這一行就是關鍵
final Typeface typeface = Typeface.createFromAsset(assetManager, filePath);
sCachedFonts.put(filePath, typeface);
return typeface;
}
} catch (Exception e) {
Log.w("Calligraphy", "Can't create asset from " + filePath + ". Make sure you have passed in the correct path and file name.", e);
sCachedFonts.put(filePath, null);
return null;
}
return sCachedFonts.get(filePath);
}
}
我們看到Typeface.createFromAsset(assetManager, filePath);
來創建Typeface, 難道就不能使用別的API來創建Typeface了嗎,?
Typeface :
createFromAsset(AssetManager mgr, String path)
createFromFile(File path)
createFromFile(String path)
createFromFamilies(FontFamily[] families)
createFromFamiliesWithDefault(FontFamily[] families)
哎喲,還不錯哦。~ 居然它有這么多種方式來創建Typeface,不明白為什么作者為啥不在這個地方做個兼容呢?讓開發者有更隨信的設置路徑呢,? 不吐槽了,~咱們試著改造改造。
在demo版本中,我們直接將 :
public static Typeface load(final AssetManager assetManager, final String filePath) {
synchronized (sCachedFonts) {
try {
if (!sCachedFonts.containsKey(filePath)) {
//這一行就是關鍵
final Typeface typeface = Typeface.createFromAsset(assetManager, filePath);
sCachedFonts.put(filePath, typeface);
return typeface;
}
} catch (Exception e) {
Log.w("Calligraphy", "Can't create asset from " + filePath + ". Make sure you have passed in the correct path and file name.", e);
sCachedFonts.put(filePath, null);
return null;
}
return sCachedFonts.get(filePath);
}
}
修改成:
public static Typeface load(final AssetManager assetManager, final String filePath) {
synchronized (sCachedFonts) {
try {
if (!sCachedFonts.containsKey(filePath)) {
//修改完成以后
final Typeface typeface = Typeface.createFromFile(filePath);
sCachedFonts.put(filePath, typeface);
return typeface;
}
} catch (Exception e) {
Log.w("Calligraphy", "Can't create asset from " + filePath + ". Make sure you have passed in the correct path and file name.", e);
sCachedFonts.put(filePath, null);
return null;
}
return sCachedFonts.get(filePath);
}
}
當然,這樣是簡單暴力的,本篇文章只提供一個一個解決思路。由于Calligraphy版權問題,我也不方便將我完全修改的jar包附上。 因為我是在他原基礎上修改框架了。我會在分析源碼的文章中講解如何做好兼容的方式。
就這樣。我們可以任意的加載任意可以訪問到的目錄字體了。有木有很簡單!
到最后一個問題,由于國內手機廠商內部資源沒有一個統一標準甚至在有些機型在5.0之后并沒有NotoSansHans
的字體,既然我們已經實現了可以加載任意目錄下字體,這個時候我想到的是利用服務器下發一套字體文件供我們使用。目前缺點是,在第一次加載程序的時候不會生成自定義字體。只有在第二次加載時才能Load到自定義字體。 這里希望大家給予一些不同的意見,希望一同分享。
一起看下運行效果:
