在出現這個框架以前,咱們要將應用字體替換爲自定義字體基本只有三種方式:java
1. 讀取放在Assets目錄下的字ttf文件,再經過 setTypeFace 方法設置,更一般的狀況是經過自定義 View 的方式來實現字體切換,這樣致使 app 中全部切換字體的地方都須要使用自定義 View,當你須要在Button、EditText、CheckBox 和 RadioButton 等繼承自 TextView 的子類中實現自定義字體時又要建立對應的自定義View,這無疑是一種強耦合的寫法,只能適合一些小型項目;android
2. 對當前頁面進行遍歷,遇到繼承自 TextView 的 View 就動態設置 typeface,優勢是能夠一次性替換大量控件的字體,避免寫多個自定義控件的麻煩,缺點也很明顯:git
3. 自定義Application, 在初始化階段將系統的字體經過反射的方式將咱們設置的系統字體替換爲咱們的自定義字體,優勢是避免了一一寫自定義View的麻煩,對應用性能形成的影響也較小,缺點是干涉系統字體在某些狀況下會出現意想不到的問題。github
Calligraphy
這個庫的出現就是以更優雅的方式來解決替換字體時的耦合和性能問題的,項目地址點這裏。app
dependencies { compile 'uk.co.chrisjenx:calligraphy:2.3.0' }
將自定義字體放置在assets/目錄下,之後使用過程當中都將以此路徑做爲相對路徑。固然你也能夠在此路徑下建立子目錄,例如"fonts/"做爲存放字體文件的目錄,在佈局文件中能夠直接使用框架
<TextView fontPath="fonts/MyFont.ttf"/>
在Application的 onCreate 方法中初始化字體配置,若是不設置的話就不會生效ide
@Override public void onCreate() { super.onCreate(); CalligraphyConfig.initDefault(new CalligraphyConfig.Builder() .setDefaultFontPath("fonts/Roboto-RobotoRegular.ttf") .setFontAttrId(R.attr.fontPath) .build() ); //.... }
attachBaseContext()方法本來是由系統來調用的,咱們將自定義的ContextImpl對象做爲參數傳遞到attachBaseContext()方法當中,從而賦值給mBase對象函數
@Override protected void attachBaseContext(Context newBase) { super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase)); }
<TextView android:text="@string/hello_world" android:layout_width="wrap_content" android:layout_height="wrap_content" fontPath="fonts/Roboto-Bold.ttf"/>
注意:常見的IDE(例如Android Studio, IntelliJ)可能會將此標記爲錯誤。你須要在這些View或者ViewGroup中添加 tools:ignore="MissingPrefix" 這一工具域去避免這一問題。你須要添加這一工具域名去啓用"ignore"屬性。工具
Calligraphy功能十分強大,從上面的說明中咱們能夠發現不只支持簡單的TextView,還支持繼承於TextView的一些View,好比Button,EditText,CheckBox之類,還支持有setTypeFace()的自定義view。並且除了從View層面支持外,還包括從style,xml來進行個性化設置字體。
Calligraphy庫中包含了10個類:源碼分析
CalligraphyActivityFactory:提供一個建立view的方法
HasTypeface:給一個標記告訴裏面有須要設置字體的view
ReflectionUtils:用來獲取方法字段,執行方法的Util類
TypefaceUtils:加載asset文件夾字體的Util類
CalligraphyUtils:給view設置字體的Util類
CalligraphyConfig:全局配置類
CalligraphyLayoutInflater:繼承系統本身實現的LayoutInflater,用來建立view
CalligraphyFactory:實現設置字體的地方
CalligraphyTypefaceSpan:Util中須要調用設置字體的類
CalligraphyContextWrapper:hook系統service的類
首先在Application中咱們初始化了 CalligraphyConfig
,運用建造者模式來配置屬性,其中類裏面有一個靜態塊,初始了一些Map,裏面存放的都是繼承於TextView的一些View的 style 屬性。
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(); } } }
在最後使用了CalligraphyUtils中的canAddV7AppCompatViews方法判斷是否能成功初始化AppCompatTextView類
/** * See if the user has added appcompat-v7 with AppCompatViews * * @return true if AppcompatTextView is on the classpath */ 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; }
若是能則將各個繼承於AppCompatTextView的 View 的 style 屬性加入到DEFAULT_STYLES中
/** * AppCompat will inflate special versions of views for Material tinting etc, * this adds those classes to the style lookup map */ private static void addAppCompatViews() { DEFAULT_STYLES.put(android.support.v7.widget.AppCompatTextView.class, android.R.attr.textViewStyle); DEFAULT_STYLES.put(android.support.v7.widget.AppCompatButton.class, android.R.attr.buttonStyle); DEFAULT_STYLES.put(android.support.v7.widget.AppCompatEditText.class, android.R.attr.editTextStyle); DEFAULT_STYLES.put(android.support.v7.widget.AppCompatAutoCompleteTextView.class, android.R.attr.autoCompleteTextViewStyle); DEFAULT_STYLES.put(android.support.v7.widget.AppCompatMultiAutoCompleteTextView.class, android.R.attr.autoCompleteTextViewStyle); DEFAULT_STYLES.put(android.support.v7.widget.AppCompatCheckBox.class, android.R.attr.checkboxStyle); DEFAULT_STYLES.put(android.support.v7.widget.AppCompatRadioButton.class, android.R.attr.radioButtonStyle); DEFAULT_STYLES.put(android.support.v7.widget.AppCompatCheckedTextView.class, android.R.attr.checkedTextViewStyle); }
CalligraphyConfig中配置了字體相關的主要屬性
/** * Is a default font set? */ private final boolean mIsFontSet; /** * The default Font Path if nothing else is setup. */ private final String mFontPath; /** * Default Font Path Attr Id to lookup */ private final int mAttrId; /** * Use Reflection to inject the private factory. */ private final boolean mReflection; /** * Use Reflection to intercept CustomView inflation with the correct Context. */ private final boolean mCustomViewCreation; /** * Use Reflection to try to set typeface for custom views if they has setTypeface method */ private final boolean mCustomViewTypefaceSupport; /** * Class Styles. Build from DEFAULT_STYLES and the builder. */ private final Map<Class<? extends TextView>, Integer> mClassStyleAttributeMap; /** * Collection of custom non-{@code TextView}'s registered for applying typeface during inflation * @see uk.co.chrisjenx.calligraphy.CalligraphyConfig.Builder#addCustomViewWithSetTypeface(Class) */ private final Set<Class<?>> hasTypefaceViews; protected CalligraphyConfig(Builder builder) { mIsFontSet = builder.isFontSet; mFontPath = builder.fontAssetPath; mAttrId = builder.attrId; mReflection = builder.reflection; mCustomViewCreation = builder.customViewCreation; mCustomViewTypefaceSupport = builder.customViewTypefaceSupport; final Map<Class<? extends TextView>, Integer> tempMap = new HashMap<>(DEFAULT_STYLES); tempMap.putAll(builder.mStyleClassMap); mClassStyleAttributeMap = Collections.unmodifiableMap(tempMap); hasTypefaceViews = Collections.unmodifiableSet(builder.mHasTypefaceClasses); }
除了Application須要配置外,還須要在Activity的 attachBaseContext 方法注入用 CalligraphyContextWrapper
包裝後的的ContextImpl,關於attachBaseContext的做用請查看《深刻理解Android中的context》一文
private CalligraphyLayoutInflater mInflater; ... /** * Uses the default configuration from {@link uk.co.chrisjenx.calligraphy.CalligraphyConfig} * * Remember if you are defining default in the * {@link uk.co.chrisjenx.calligraphy.CalligraphyConfig} make sure this is initialised before * the activity is created. * * @param base ContextBase to Wrap. * @return ContextWrapper to pass back to the activity. */ public static ContextWrapper wrap(Context base) { return new CalligraphyContextWrapper(base); } ... @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); }
能夠看到 CalligraphyContextWrapper
裏包含了一個CalligraphyLayoutInflater的屬性,當Activity進行佈局初始化時hook了LAYOUT_INFLATER_SERVICE服務,並將CalligraphyLayoutInflater屬性進行初始化。
繼續跟進CalligraphyLayoutInflater類,能夠看到他的構造方法以下:
protected CalligraphyLayoutInflater(LayoutInflater original, Context newContext, int attributeId, final boolean cloned) { super(original, newContext); mAttributeId = attributeId; mCalligraphyFactory = new CalligraphyFactory(attributeId); setUpLayoutFactories(cloned); }
其中mAttributeId是在Application中初始化CalligraphyConfig時設置的,用來做爲配置字體時的前綴
/** * This defaults to R.attr.fontPath. So only override if you want to use your own attrId. * * @param fontAssetAttrId the custom attribute to look for fonts in assets. * @return this builder. */ public Builder setFontAttrId(int fontAssetAttrId) { this.attrId = fontAssetAttrId; return this; }
最後調用了setUpLayoutFactories(cloned)方法,並傳入 cloned
參數
/** * We don't want to unnecessary create/set our factories if there are none there. We try to be * as lazy as possible. */ 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()); } }
根據版本是否大於11分爲了兩種Factory,其中Factory和Factory2是LayoutInflater內部的兩個接口
public interface Factory2 extends LayoutInflater.Factory { View onCreateView(View var1, String var2, Context var3, AttributeSet var4); } public interface Factory { View onCreateView(String var1, Context var2, AttributeSet var3); }
首次調用會執行 setFactory2(getFactory2()) 方法,咱們能夠看到 CallinggraphyContextWrapper 中重寫了 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); } }
咱們再跟進WrapperFactory2, 能夠看到它是 Factory2 的一個包裝類
@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); } }
構造函數包含兩個參數,其一是實現factory2接口的一個實例,其二是咱們以前初始化的 CalligraphyFactory 實例。
在實現Factory2 接口的兩個方法中,能夠看到咱們最終調用的是 CalligraphyFactory
的 onViewCreated
方法,咱們繼續跟進CalligraphyFactory中的 onCreateView 方法
/** * Handle the created view * * @param view nullable. * @param context shouldn't be null. * @param attrs shouldn't be null. * @return null if null is passed in. */ 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; }
若是該 View沒有被設置過字體,那麼就會調用 onViewCreatedInternal
的方法,並被設置tag
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) { applyFontToToolbar((Toolbar) view); } // 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 的子類,而後若是已經設置過字體就直接跳過,往下走就是 resolveFontPath
方法,依次從xml,style 和 TextAppearance 中獲取字體文件的路徑,若是沒找到則設置爲默認的自定義屬性。最後調用 CalligraphyUtils 中的 applyFontToTextView 方法使字體生效。除了繼承於TextView 的子View 以外,還對ToolBar和 ActionBar作了適配。
/** * Applies a Typeface to a TextView, if deferred,its recommend you don't call this multiple * times, as this adds a TextWatcher. * * Deferring should really only be used on tricky views which get Typeface set by the system at * weird times. * * @param textView Not null, TextView or child of. * @param typeface Not null, Typeface to apply to the TextView. * @param deferred If true we use Typefaces and TextChange listener to make sure font is always * applied, but this sometimes conflicts with other * {@link android.text.Spannable}'s. * @return true if applied otherwise false. * @see #applyFontToTextView(android.widget.TextView, android.graphics.Typeface) */ public static boolean applyFontToTextView(final TextView textView, final Typeface typeface, boolean deferred) { if (textView == null || typeface == null) return false; textView.setPaintFlags(textView.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG); textView.setTypeface(typeface); if (deferred) { textView.setText(applyTypefaceSpan(textView.getText(), typeface), TextView.BufferType.SPANNABLE); textView.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { applyTypefaceSpan(s, typeface); } }); } return true; }
能夠看到在該方法中設置了字體,若是碰到Spannable,還須要延遲處理。
Calligraphy核心實際上就是 自定義LayoutInflater以及其中的Factory來hook住系統構建View的過程,而且替換爲咱們本身的處理方式,由此引伸開來,不管是切換字體仍是皮膚都是同樣的道理。