Calligraphy

如何使用Calligraphy

1.添加依赖

dependencies {
   compile 'uk.co.chrisjenx:calligraphy:2.2.0'
}

2.在 assets 文件下加添加字体文件

3.在Application的 OnCreate 中初始化字体配置

@Override
public void onCreate() {
    super.onCreate();
    CalligraphyConfig.initDefault(new CalligraphyConfig.Builder()
                            .setDefaultFontPath("fonts/Roboto-RobotoRegular.ttf")
                            .setFontAttrId(R.attr.fontPath)
                            .build()
            );
    //....
}

4.在Activity中注入Context,重写一个方法

@Override
protected void attachBaseContext(Context newBase) {
    super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase));
}

总体设计

这个库十分的强大,从sample中我们可以发现不仅支持简单的TextView,还支持继承于TextView的一些View,比如Button,EditText,CheckBox之类,还支持有setTypeFace()的自定义view。而且除了从View层面支持外,还包括从style,xml来进行个性化设置字体。

Calligraphy的类只有10个,比较精巧~

接口

CalligraphyActivityFactory---提供一个创建view的方法

HasTypeface---给一个标记告诉里面有需要设置字体的view

Util类

ReflectionUtils---用来获取方法字段,执行方法的Util类

TypefaceUtils---加载asset文件夹字体的Util类

CalligraphyUtils---给view设置字体的Util类

其他的

CalligraphyConfig---全局配置类

CalligraphyLayoutInflater---继承系统自己实现的LayoutInflater,用来创建view

CalligraphyFactory---实现设置字体的地方

CalligraphyTypefaceSpan---Util中需要调用设置字体的类

CalligraphyContextWrapper---hook系统service的类

详细介绍

为了连贯性,我们按照使用的顺序来依次介绍。

首先在Application中我们初始化了 CalligraphyConfig ,运用建造者模式来配置属性,其中类里面有一个静态块,初始了一些Map,里面存放的都是继承于TextView的一些组件的Style。

private static final Map<Class<? extends TextView>, Integer> DEFAULT_STYLES = new HashMap<>();

    static {
        {
            DEFAULT_STYLES.put(TextView.class, android.R.attr.textViewStyle);
            DEFAULT_STYLES.put(Button.class, android.R.attr.buttonStyle);
            DEFAULT_STYLES.put(EditText.class, android.R.attr.editTextStyle);
            DEFAULT_STYLES.put(AutoCompleteTextView.class, android.R.attr.autoCompleteTextViewStyle);
            DEFAULT_STYLES.put(MultiAutoCompleteTextView.class, android.R.attr.autoCompleteTextViewStyle);
            DEFAULT_STYLES.put(CheckBox.class, android.R.attr.checkboxStyle);
            DEFAULT_STYLES.put(RadioButton.class, android.R.attr.radioButtonStyle);
            DEFAULT_STYLES.put(ToggleButton.class, android.R.attr.buttonStyleToggle);
            if (CalligraphyUtils.canAddV7AppCompatViews()) {
                addAppCompatViews();
            }
        }
    }

在最后有一个方法判断能否加入AppCompatView,实际上系统在AppCom中把我们常用的TextView之类的控件都通过Factory转换成了新的AppCompatTextView之类的view,这里也是用了一种取巧的办法,
直接在try catch块里面来调用 Class.forName ,如果找不到这个类的话就被catch住,将 sAppCompatViewCheck 参数设置为 false。看前面的使用说明里面就知道在这个类里面还能设置默认字体,自定义属性。

除了Application需要配置外,在Activity中也需要配置,这一点格外重要,整个字体切换都是基于此的。

//使用自定义的CalligraphyContextWrapper类
@Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase));
    }

attachBaseContext 这个方法是从属于 ContextWrapper 的,Android系统中我们的 Application,Activity,Service其实都是继承于 ContextWrapper,而ContextWrapper则是继承于Context,所以我们的这些类才会有上下文关系。上面这段中我们将当前Activity的Context包装成一个 CalligraphyContextWrapper 的Context,然后设置给 attachBaseContext 这个方法,这样我们后面取到的实际上是包装类的Context 。继续往下看这个包装类,这个类中最重要也是最hack的方法就是下面这个。

//hook方法getSystemService并返回自定义的CalligraphyLayoutInflater类
@Override
    public Object getSystemService(String name) {
        if (LAYOUT_INFLATER_SERVICE.equals(name)) {
            if (mInflater == null) {
                mInflater = new CalligraphyLayoutInflater(LayoutInflater.from(getBaseContext()), this, mAttributeId, false);
            }
            return mInflater;
        }
        return super.getSystemService(name);
    }

这里面实际上是hook了系统的service,当然只针对 LAYOUT_INFLATER_SERVICE ,也就是LayoutInflater的service。LayoutInflater这个应该都很熟悉了,我们在创建view的时候都用到过这个类,实际上所有的创建view都是调用的这个类,即使有一些我们表面的看不到的方法也是用的这个。比如最常用的 LayoutInflater.from(Context context) 方法

public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

所以我们在系统创建view之前将系统的 LayoutInflater 换成了 CalligraphyLayoutInflater 。继续跟进去, CalligraphyLayoutInflater 继承于系统的 LayoutInflater ,先看构造方法,

protected CalligraphyLayoutInflater(LayoutInflater original, Context newContext, int attributeId, final boolean cloned) {
        super(original, newContext);
        mAttributeId = attributeId;
        mCalligraphyFactory = new CalligraphyFactory(attributeId);
        setUpLayoutFactories(cloned);
    }

attributeId 这个是一个自定义的属性,决定我们在XML中配置字体的前缀,如果用默认的那么这里就是默认的,否则就在最开始的Application中配置, CalligraphyFactory 这个类一会再讲,也是十分重要的类,最后就是调用了 setUpLayoutFactories方法,里面传入了一个 cloned 参数,继续往下走

执行Activity的oncreate方法

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        }

其实际调用AppCompatActivity内的oncreate方法


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        if (delegate.applyDayNight() && mThemeId != 0) {
            // If DayNight has been applied, we need to re-apply the theme for
            // the changes to take effect. On API 23+, we should bypass
            // setTheme(), which will no-op if the theme ID is identical to the
            // current theme ID.
            if (Build.VERSION.SDK_INT >= 23) {
                onApplyThemeResource(getTheme(), mThemeId, false);
            } else {
                setTheme(mThemeId);
            }
        }
        super.onCreate(savedInstanceState);
    }

关键部分执行installViewFactory()方法

 @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory(layoutInflater, this);
        } else {
            if (!(LayoutInflaterCompat.getFactory(layoutInflater)
                    instanceof AppCompatDelegateImplV7)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }

此处用来设置处理View的类为AppCompatDelegateImplV7也实现了LayoutInflatFactory
当调用到inflate方法的时候将会执行

 @Override
    public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
        setPrivateFactoryInternal();
        return super.inflate(parser, root, attachToRoot);
    }

setPrivateFactoryInternal方法

private void setPrivateFactoryInternal() {
        // Already tried to set the factory.
        if (mSetPrivateFactory) return;
        // Reflection (Or Old Device) skip.
        if (!CalligraphyConfig.get().isReflection()) return;
        // Skip if not attached to an activity.
        if (!(getContext() instanceof Factory2)) {
            mSetPrivateFactory = true;
            return;
        }

        final Method setPrivateFactoryMethod = ReflectionUtils
                .getMethod(LayoutInflater.class, "setPrivateFactory");

        if (setPrivateFactoryMethod != null) {
            ReflectionUtils.invokeMethod(this,
                    setPrivateFactoryMethod,
                    new PrivateWrapperFactory2((Factory2) getContext(), this, mCalligraphyFactory));
        }
        mSetPrivateFactory = true;
    }

该方法是通过反射的方法设置factory为PrivateWrapperFactory2
和 setFactory 相关的函数一共有三个

public void setFactory(LayoutInflater.Factory factory) {
    if (mFactorySet) {
        throw new IllegalStateException("A factory has already been set on this LayoutInflater");
    }
    if (factory == null) {
        throw new NullPointerException("Given factory can not be null");
    }
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = factory;
    } else {
        mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
    }
}

public void setFactory2(Factory2 factory) {
    if (mFactorySet) {
        throw new IllegalStateException("A factory has already been set on this LayoutInflater");
    }
    if (factory == null) {
        throw new NullPointerException("Given factory can not be null");
    }
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = mFactory2 = factory;
    } else {
        mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
    }
}
/**
 * @hide for use by framework
 */
public void setPrivateFactory(Factory2 factory) {
    if (mPrivateFactory == null) {
        mPrivateFactory = factory;
    } else {
        mPrivateFactory = new FactoryMerger(factory, factory, mPrivateFactory, mPrivateFactory);
    }
}

此处调用的是第三个

private static class FactoryMerger implements Factory2 {
    private final Factory mF1, mF2;
    private final Factory2 mF12, mF22;

    FactoryMerger(Factory f1, Factory2 f12, Factory f2, Factory2 f22) {
        mF1 = f1;
        mF2 = f2;
        mF12 = f12;
        mF22 = f22;
    }

    public View onCreateView(String name, Context context, AttributeSet attrs) {
        View v = mF1.onCreateView(name, context, attrs);
        if (v != null) return v;
        return mF2.onCreateView(name, context, attrs);
    }

    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        View v = mF12 != null ? mF12.onCreateView(parent, name, context, attrs)
                : mF1.onCreateView(name, context, attrs);
        if (v != null) return v;
        return mF22 != null ? mF22.onCreateView(parent, name, context, attrs)
                : mF2.onCreateView(name, context, attrs);
    }
}

FactoryMerger 实现了 Factory2 接口,构造函数有四个参数,前两个是新的 Factory ,后两个参数是旧的 Factory ,回调中先调用新的 Factory ,如果返回为空,则调用旧的 Factory,如果不为空则直接返回

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
    private static class WrapperFactory2 implements Factory2 {
        protected final Factory2 mFactory2;
        protected final CalligraphyFactory mCalligraphyFactory;

        public WrapperFactory2(Factory2 factory2, CalligraphyFactory calligraphyFactory) {
            mFactory2 = factory2;
            mCalligraphyFactory = calligraphyFactory;
        }

        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return mCalligraphyFactory.onViewCreated(
                    mFactory2.onCreateView(name, context, attrs),
                    context, attrs);
        }

        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            return mCalligraphyFactory.onViewCreated(
                    mFactory2.onCreateView(parent, name, context, attrs),
                    context, attrs);
        }
    }

可以看到我们最终调用的是 CalligraphyFactory 的 onViewCreated 方法,终于到了关键的地方,继续看这个方法的实现,

public View onViewCreated(View view, Context context, AttributeSet attrs) {
        if (view != null && view.getTag(R.id.calligraphy_tag_id) != Boolean.TRUE) {
            onViewCreatedInternal(view, context, attrs);
            view.setTag(R.id.calligraphy_tag_id, Boolean.TRUE);
        }
        return view;
    }

使用tag的方式,这里的tag代表的其实是有没有被处理过,也就是有没有被设置过字体,可以看到如果tag为false,那么就会调用 onViewCreatedInternal 的方法。

void onViewCreatedInternal(View view, final Context context, AttributeSet attrs) {
        if (view instanceof TextView) {
            // Fast path the setting of TextView's font, means if we do some delayed setting of font,
            // which has already been set by use we skip this TextView (mainly for inflating custom,
            // TextView's inside the Toolbar/ActionBar).

            if (TypefaceUtils.isLoaded(((TextView) view).getTypeface())) {
                return;
            }
            // Try to get typeface attribute value
            // Since we're not using namespace it's a little bit tricky

            // Check xml attrs, style attrs and text appearance for font path
            String textViewFont = resolveFontPath(context, attrs);

            // Try theme attributes
            if (TextUtils.isEmpty(textViewFont)) {
                final int[] styleForTextView = getStyleForTextView((TextView) view);
                if (styleForTextView[1] != -1)
                    textViewFont = CalligraphyUtils.pullFontPathFromTheme(context, styleForTextView[0], styleForTextView[1], mAttributeId);
                else
                    textViewFont = CalligraphyUtils.pullFontPathFromTheme(context, styleForTextView[0], mAttributeId);
            }

            // Still need to defer the Native action bar, appcompat-v7:21+ uses the Toolbar underneath. But won't match these anyway.
            final boolean deferred = matchesResourceIdName(view, ACTION_BAR_TITLE) || matchesResourceIdName(view, ACTION_BAR_SUBTITLE);

            CalligraphyUtils.applyFontToTextView(context, (TextView) view, CalligraphyConfig.get(), textViewFont, deferred);
        }

        // AppCompat API21+ The ActionBar doesn't inflate default Title/SubTitle, we need to scan the
        // Toolbar(Which underlies the ActionBar) for its children.
        if (CalligraphyUtils.canCheckForV7Toolbar() && view instanceof android.support.v7.widget.Toolbar) {
            final Toolbar toolbar = (Toolbar) view;
            toolbar.getViewTreeObserver().addOnGlobalLayoutListener(new ToolbarLayoutListener(this, context, toolbar));
        }

        // Try to set typeface for custom views using interface method or via reflection if available
        if (view instanceof HasTypeface) {
            Typeface typeface = getDefaultTypeface(context, resolveFontPath(context, attrs));
            if (typeface != null) {
                ((HasTypeface) view).setTypeface(typeface);
            }
        } else if (CalligraphyConfig.get().isCustomViewTypefaceSupport() && CalligraphyConfig.get().isCustomViewHasTypeface(view)) {
            final Method setTypeface = ReflectionUtils.getMethod(view.getClass(), "setTypeface");
            String fontPath = resolveFontPath(context, attrs);
            Typeface typeface = getDefaultTypeface(context, fontPath);
            if (setTypeface != null && typeface != null) {
                ReflectionUtils.invokeMethod(view, setTypeface, typeface);
            }
        }

    }

代码比较长,整体分析一下,首先是 判断是不是 TextView 的类或者是子类,然后如果已经有 TypeFace也就是字体,那么直接跳过,往下走就是 resolveFontPath 方法,这个主要是从三个方面来提取字体文件, xml , style , TextAppearance ,然后给view设置上自定义的字体。除了正常的view之外,下面还兼容了 ToolBar ,实现了 hasTypeface 接口的view,以及自定义中有 setTypeface 的view。

通过整个方法的调用就完成了自定义字体的设置。

总结

整个源码分析到这里差不多脉络都比较清晰了,如果还有不清楚的,可以通读一次源码,自己对照github上的sample进行修改就能理解更深。作者为了兼容不同的场景写的也比较用心,代码也比较多和杂乱,但是核心实际上就是 自定义LayoutInflater以及其中的Factory来hook住系统创建view的过程,并且加上我们自己的处理,只要理解了这个思想,无论是这种字体切换或者是皮肤切换都是一样的道理,比如切换皮肤实际上也就是切换颜色,背景等属性,这些使用Factory都是可以做到的。

功能虽然各式各样,但是把握核心本质,自然就能在各种需求中游刃有余~

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

推荐阅读更多精彩内容