Android字體庫Calligraphy源碼解析
什么是Calligraphy
如果你沒有看過上一篇文章 Android自定義字體實踐 ,那么可以先點一下前置技能,這樣能更好的理解這篇文章。在上一篇文章中雖然已經成功的實現了字體的自定義,以及使用原生 textStyle 而不是自定義的方式來切換字體樣式,但是還是有很多的問題。比如破壞了代碼的統一性,通過一種自定義View的方式來實現字體切換,這樣導致app中所有切換字體的地方都需要使用自定義view,無疑是一種強耦合的寫法,只能適合一些小型項目。 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();
}
}
}</code></pre>
在最后有一個方法判斷能否加入AppCompatView,實際上系統在AppCom中把我們常用的TextView之類的控件都通過Factory轉換成了新的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塊里面來調用 Class.forName ,如果找不到這個類的話就被catch住,將 sAppCompatViewCheck 參數設置為 false。看前面的使用說明里面就知道在這個類里面還能設置默認字體,自定義屬性。
除了Application需要配置外,在Activity中也需要配置,這一點格外重要,整個字體切換都是基于此的。
@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的方法就是下面這個。
@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 參數,繼續往下走
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實際上是 LayoutInflater 內部的一個接口,看看官方的注釋
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);
}
當我們想要自定義操作的時候就可以通過使用Factory的來做事情,實現里面的方法來創建我們需要的view,這里我們以上面的SDK_INK大于11的情況繼續往下看,先判斷是不是 WrapperFactory2 的實例,第一次肯定會走進去來設置這個,也就是調用 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 來將兩個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);
}
}
構造函數中有兩個參數,一個是在重寫的 setFactory2 自帶的Factory,一個是我們自己的 CalligraphyFactory ,在實現 Factory2 接口的兩個方法中,可以看到我們最終調用的是 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都是可以做到的。
功能雖然各式各樣,但是把握核心本質,自然就能在各種需求中游刃有余
參考文獻
- https://github.com/chrisjenx/Calligraphy/issues
- http://willowtreeapps.com/blog/app-development-how-to-get-the-right-layoutinflater
- http://blog.bradcampbell.nz/layoutinflater-factories/
來自:http://www.jianshu.com/p/5d4e6ae8ba4e