Android字體庫Calligraphy源碼解析

什么是Calligraphy

如果你沒有看過上一篇文章 Android自定義字體實(shí)踐 ,那么可以先點(diǎn)一下前置技能,這樣能更好的理解這篇文章。在上一篇文章中雖然已經(jīng)成功的實(shí)現(xiàn)了字體的自定義,以及使用原生 textStyle 而不是自定義的方式來切換字體樣式,但是還是有很多的問題。比如破壞了代碼的統(tǒng)一性,通過一種自定義View的方式來實(shí)現(xiàn)字體切換,這樣導(dǎo)致app中所有切換字體的地方都需要使用自定義view,無疑是一種強(qiáng)耦合的寫法,只能適合一些小型項(xiàng)目。Calligraphy 這個(gè)庫就是來解決這個(gè)耦合的問題的,當(dāng)然只是用了一些高雅的技巧。

如何使用Calligraphy

1.添加依賴

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

2.在 assets文件下加添加字體文件
3.在Application的 OnCreate 中初始化字體配置,如果不設(shè)置的話就不會(huì)

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

4.在Activity中注入Context,重寫一個(gè)方法

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

總體設(shè)計(jì)

這個(gè)庫十分的強(qiáng)大,從sample中我們可以發(fā)現(xiàn)不僅支持簡(jiǎn)單的TextView,還支持繼承于TextView的一些View,比如Button,EditText,CheckBox之類,還支持有setTypeFace()的自定義view。而且除了從View層面支持外,還包括從style,xml來進(jìn)行個(gè)性化設(shè)置字體。
Calligraphy的類只有10個(gè),比較精巧~
接口
CalligraphyActivityFactory---提供一個(gè)創(chuàng)建view的方法
HasTypeface---給一個(gè)標(biāo)記告訴里面有需要設(shè)置字體的view
Util類
ReflectionUtils---用來獲取方法字段,執(zhí)行方法的Util類
TypefaceUtils---加載asset文件夾字體的Util類
CalligraphyUtils---給view設(shè)置字體的Util類
其他的
CalligraphyConfig---全局配置類
CalligraphyLayoutInflater---繼承系統(tǒng)自己實(shí)現(xiàn)的LayoutInflater,用來創(chuàng)建view
CalligraphyFactory---實(shí)現(xiàn)設(shè)置字體的地方
CalligraphyTypefaceSpan---Util中需要調(diào)用設(shè)置字體的類
CalligraphyContextWrapper---hook系統(tǒng)service的類

詳細(xì)介紹

為了連貫性,我們按照使用的順序來依次介紹。
首先在Application中我們初始化了 CalligraphyConfig,運(yùn)用建造者模式來配置屬性,其中類里面有一個(gè)靜態(tài)塊,初始了一些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();
            }
        }
    }

在最后有一個(gè)方法判斷能否加入AppCompatView,實(shí)際上系統(tǒng)在AppCom中把我們常用的TextView之類的控件都通過Factory轉(zhuǎn)換成了新的AppCompatTextView之類的view,這里也是用了一種取巧的辦法,

    static boolean canAddV7AppCompatViews() {
        if (sAppCompatViewCheck == null) {
            try {
                Class.forName("android.support.v7.widget.AppCompatTextView");
                sAppCompatViewCheck = Boolean.TRUE;
            } catch (ClassNotFoundException e) {
                sAppCompatViewCheck = Boolean.FALSE;
            }
        }
        return sAppCompatViewCheck;
    }

直接在try catch塊里面來調(diào)用 Class.forName,如果找不到這個(gè)類的話就被catch住,將 sAppCompatViewCheck 參數(shù)設(shè)置為 false??辞懊娴氖褂谜f明里面就知道在這個(gè)類里面還能設(shè)置默認(rèn)字體,自定義屬性。
除了Application需要配置外,在Activity中也需要配置,這一點(diǎn)格外重要,整個(gè)字體切換都是基于此的。

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

attachBaseContext 這個(gè)方法是從屬于 ContextWrapper 的,Android系統(tǒng)中我們的 Application,Activity,Service其實(shí)都是繼承于 ContextWrapper,而ContextWrapper則是繼承于Context,所以我們的這些類才會(huì)有上下文關(guān)系。上面這段中我們將當(dāng)前Activity的Context包裝成一個(gè) CalligraphyContextWrapper 的Context,然后設(shè)置給 attachBaseContext 這個(gè)方法,這樣我們后面取到的實(shí)際上是包裝類的Context 。繼續(xù)往下看這個(gè)包裝類,這個(gè)類中最重要也是最hack的方法就是下面這個(gè)。

    @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);
    }

這里面實(shí)際上是hook了系統(tǒng)的service,當(dāng)然只針對(duì) LAYOUT_INFLATER_SERVICE,也就是LayoutInflater的service。LayoutInflater這個(gè)應(yīng)該都很熟悉了,我們?cè)趧?chuàng)建view的時(shí)候都用到過這個(gè)類,實(shí)際上所有的創(chuàng)建view都是調(diào)用的這個(gè)類,即使有一些我們表面的看不到的方法也是用的這個(gè)。比如最常用的 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;
    }

所以我們?cè)谙到y(tǒng)創(chuàng)建view之前將系統(tǒng)的 LayoutInflater 換成了 CalligraphyLayoutInflater 。繼續(xù)跟進(jìn)去,CalligraphyLayoutInflater 繼承于系統(tǒng)的 LayoutInflater ,先看構(gòu)造方法,

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

attributeId 這個(gè)是一個(gè)自定義的屬性,決定我們?cè)赬ML中配置字體的前綴,如果用默認(rèn)的那么這里就是默認(rèn)的,否則就在最開始的Application中配置,CalligraphyFactory 這個(gè)類一會(huì)再講,也是十分重要的類,最后就是調(diào)用了 setUpLayoutFactories 方法,里面?zhèn)魅肓艘粋€(gè) cloned 參數(shù),繼續(xù)往下走

    private void setUpLayoutFactories(boolean cloned) {
        if (cloned) return;
        // If we are HC+ we get and set Factory2 otherwise we just wrap Factory1
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            if (getFactory2() != null && !(getFactory2() instanceof WrapperFactory2)) {
                // Sets both Factory/Factory2
                setFactory2(getFactory2());
            }
        }
        // We can do this as setFactory2 is used for both methods.
        if (getFactory() != null && !(getFactory() instanceof WrapperFactory)) {
            setFactory(getFactory());
        }
    }

根據(jù)版本是否大于11分為了兩種Factory,這個(gè)Factory實(shí)際上是 LayoutInflater 內(nèi)部的一個(gè)接口,看看官方的注釋

    public interface Factory {
        /**
         * Hook you can supply that is called when inflating from a LayoutInflater.
         * You can use this to customize the tag names available in your XML
         * layout files.
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         * 
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        public View onCreateView(String name, Context context, AttributeSet attrs);
    }

當(dāng)我們想要自定義操作的時(shí)候就可以通過使用Factory的來做事情,實(shí)現(xiàn)里面的方法來創(chuàng)建我們需要的view,這里我們以上面的SDK_INK大于11的情況繼續(xù)往下看,先判斷是不是 WrapperFactory2 的實(shí)例,第一次肯定會(huì)走進(jìn)去來設(shè)置這個(gè),也就是調(diào)用 setFactory2() 方法。

    @Override
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public void setFactory2(Factory2 factory2) {
        // Only set our factory and wrap calls to the Factory2 trying to be set!
        if (!(factory2 instanceof WrapperFactory2)) {
    //            LayoutInflaterCompat.setFactory(this, new WrapperFactory2(factory2, mCalligraphyFactory));
            super.setFactory2(new WrapperFactory2(factory2, mCalligraphyFactory));
        } else {
            super.setFactory2(factory2);
        }
    }

這實(shí)際上是一個(gè)覆寫的方法,并且在里面用 WrapperFactory2 來將兩個(gè)Factory包裝起來,繼續(xù)跟進(jìn)去

    @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);
        }
    }

構(gòu)造函數(shù)中有兩個(gè)參數(shù),一個(gè)是在重寫的 setFactory2 自帶的Factory,一個(gè)是我們自己的 CalligraphyFactory ,在實(shí)現(xiàn) Factory2 接口的兩個(gè)方法中,可以看到我們最終調(diào)用的是 CalligraphyFactoryonViewCreated 方法,終于到了關(guān)鍵的地方,繼續(xù)看這個(gè)方法的實(shí)現(xiàn),

    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代表的其實(shí)是有沒有被處理過,也就是有沒有被設(shè)置過字體,可以看到如果tag為false,那么就會(huì)調(diào)用 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 的類或者是子類,然后如果已經(jīng)有 TypeFace也就是字體,那么直接跳過,往下走就是 resolveFontPath 方法,這個(gè)主要是從三個(gè)方面來提取字體文件,xml,style,TextAppearance,然后給view設(shè)置上自定義的字體。除了正常的view之外,下面還兼容了 ToolBar ,實(shí)現(xiàn)了 hasTypeface 接口的view,以及自定義中有 setTypeface 的view。
通過整個(gè)方法的調(diào)用就完成了自定義字體的設(shè)置。

總結(jié)

整個(gè)源碼分析到這里差不多脈絡(luò)都比較清晰了,如果還有不清楚的,可以通讀一次源碼,自己對(duì)照github上的sample進(jìn)行修改就能理解更深。作者為了兼容不同的場(chǎng)景寫的也比較用心,代碼也比較多和雜亂,但是核心實(shí)際上就是 自定義LayoutInflater以及其中的Factory來hook住系統(tǒng)創(chuàng)建view的過程,并且加上我們自己的處理,只要理解了這個(gè)思想,無論是這種字體切換或者是皮膚切換都是一樣的道理,比如切換皮膚實(shí)際上也就是切換顏色,背景等屬性,這些使用Factory都是可以做到的。
功能雖然各式各樣,但是把握核心本質(zhì),自然就能在各種需求中游刃有余~

參考文獻(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 7,295評(píng)論 0 17
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,765評(píng)論 25 709
  • 如何使用Calligraphy 1.添加依賴 2.在 assets 文件下加添加字體文件 3.在Applicati...
    桑享閱讀 2,776評(píng)論 1 3
  • 現(xiàn)在是去往深圳西的火車上,離下車還有2個(gè)小時(shí),來安靜得一個(gè)人寫寫東西吧。標(biāo)題在路上的意思不僅僅是我在去深圳西的路上...
    聞舒閱讀 369評(píng)論 1 0
  • (2017-04-30-周日 17:29:04) No purchase was found for your a...
    菜五閱讀 289評(píng)論 0 0

友情鏈接更多精彩內(nèi)容