Android -全自动将APP的字体替换系统包含的任意字体


最近公司设计部门提出要将应用的字体全部替换成思源黑体,当时我想到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到项目中来吗,?
下面附图:

font icon
font icon

可以看到,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到自定义字体。 这里希望大家给予一些不同的意见,希望一同分享。

一起看下运行效果:


icon
icon
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容