最近公司设计部门提出要将应用的字体全部替换成思源黑体,当时我想到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到自定义字体。 这里希望大家给予一些不同的意见,希望一同分享。
一起看下运行效果: