在出现这个框架以前,咱们要将应用字体替换为自定义字体基本只有三种方式: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的过程,而且替换为咱们本身的处理方式,由此引伸开来,不管是切换字体仍是皮肤都是同样的道理。