Android字体库Calligraphy源码解析

vaxy4081 7年前
   <h3><strong>什么是Calligraphy</strong></h3>    <p>如果你没有看过上一篇文章 <a href="/misc/goto?guid=4959717738360370736" rel="nofollow,noindex">Android自定义字体实践</a> ,那么可以先点一下前置技能,这样能更好的理解这篇文章。在上一篇文章中虽然已经成功的实现了字体的自定义,以及使用原生 textStyle 而不是自定义的方式来切换字体样式,但是还是有很多的问题。比如破坏了代码的统一性,通过一种自定义View的方式来实现字体切换,这样导致app中所有切换字体的地方都需要使用自定义view,无疑是一种强耦合的写法,只能适合一些小型项目。 Calligraphy 这个库就是来解决这个耦合的问题的,当然只是用了一些高雅的技巧。</p>    <h3><strong>如何使用Calligraphy</strong></h3>    <p>1.添加依赖</p>    <pre>  <code class="language-java">dependencies {     compile 'uk.co.chrisjenx:calligraphy:2.2.0'  }</code></pre>    <p>2.在 assets 文件下加添加字体文件</p>    <p>3.在Application的 OnCreate 中初始化字体配置,如果不设置的话就不会</p>    <pre>  <code class="language-java">@Override  public void onCreate() {      super.onCreate();      CalligraphyConfig.initDefault(new CalligraphyConfig.Builder()                              .setDefaultFontPath("fonts/Roboto-RobotoRegular.ttf")                              .setFontAttrId(R.attr.fontPath)                              .build()              );      //....  }</code></pre>    <p>4.在Activity中注入Context,重写一个方法</p>    <pre>  <code class="language-java">@Override  protected void attachBaseContext(Context newBase) {      super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase));  }</code></pre>    <h3><strong>总体设计</strong></h3>    <p>这个库十分的强大,从sample中我们可以发现不仅支持简单的TextView,还支持继承于TextView的一些View,比如Button,EditText,CheckBox之类,还支持有setTypeFace()的自定义view。而且除了从View层面支持外,还包括从style,xml来进行个性化设置字体。</p>    <p>Calligraphy的类只有10个,比较精巧~</p>    <p>接口</p>    <p>CalligraphyActivityFactory---提供一个创建view的方法</p>    <p>HasTypeface---给一个标记告诉里面有需要设置字体的view</p>    <p>Util类</p>    <p>ReflectionUtils---用来获取方法字段,执行方法的Util类</p>    <p>TypefaceUtils---加载asset文件夹字体的Util类</p>    <p>CalligraphyUtils---给view设置字体的Util类</p>    <p>其他的</p>    <p>CalligraphyConfig---全局配置类</p>    <p>CalligraphyLayoutInflater---继承系统自己实现的LayoutInflater,用来创建view</p>    <p>CalligraphyFactory---实现设置字体的地方</p>    <p>CalligraphyTypefaceSpan---Util中需要调用设置字体的类</p>    <p>CalligraphyContextWrapper---hook系统service的类</p>    <h3><strong>详细介绍</strong></h3>    <p>为了连贯性,我们按照使用的顺序来依次介绍。</p>    <p>首先在Application中我们初始化了 CalligraphyConfig ,运用建造者模式来配置属性,其中类里面有一个静态块,初始了一些Map,里面存放的都是继承于TextView的一些组件的Style。</p>    <pre>  <code class="language-java">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>    <p>在最后有一个方法判断能否加入AppCompatView,实际上系统在AppCom中把我们常用的TextView之类的控件都通过Factory转换成了新的AppCompatTextView之类的view,这里也是用了一种取巧的办法,</p>    <pre>  <code class="language-java">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;      }</code></pre>    <p>直接在try catch块里面来调用 Class.forName ,如果找不到这个类的话就被catch住,将 sAppCompatViewCheck 参数设置为 false。看前面的使用说明里面就知道在这个类里面还能设置默认字体,自定义属性。</p>    <p>除了Application需要配置外,在Activity中也需要配置,这一点格外重要,整个字体切换都是基于此的。</p>    <pre>  <code class="language-java">@Override      protected void attachBaseContext(Context newBase) {          super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase));      }</code></pre>    <p>attachBaseContext 这个方法是从属于 ContextWrapper 的,Android系统中我们的 Application,Activity,Service其实都是继承于 ContextWrapper,而ContextWrapper则是继承于Context,所以我们的这些类才会有上下文关系。上面这段中我们将当前Activity的Context包装成一个 CalligraphyContextWrapper 的Context,然后设置给 attachBaseContext 这个方法,这样我们后面取到的实际上是包装类的Context 。继续往下看这个包装类,这个类中最重要也是最hack的方法就是下面这个。</p>    <pre>  <code class="language-java">@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);      }</code></pre>    <p>这里面实际上是hook了系统的service,当然只针对 LAYOUT_INFLATER_SERVICE ,也就是LayoutInflater的service。LayoutInflater这个应该都很熟悉了,我们在创建view的时候都用到过这个类,实际上所有的创建view都是调用的这个类,即使有一些我们表面的看不到的方法也是用的这个。比如最常用的 LayoutInflater.from(Context context) 方法</p>    <pre>  <code class="language-java">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;      }</code></pre>    <p>所以我们在系统创建view之前将系统的 LayoutInflater 换成了 CalligraphyLayoutInflater 。继续跟进去, CalligraphyLayoutInflater 继承于系统的 LayoutInflater ,先看构造方法,</p>    <pre>  <code class="language-java">protected CalligraphyLayoutInflater(LayoutInflater original, Context newContext, int attributeId, final boolean cloned) {          super(original, newContext);          mAttributeId = attributeId;          mCalligraphyFactory = new CalligraphyFactory(attributeId);          setUpLayoutFactories(cloned);      }</code></pre>    <p>attributeId 这个是一个自定义的属性,决定我们在XML中配置字体的前缀,如果用默认的那么这里就是默认的,否则就在最开始的Application中配置, CalligraphyFactory 这个类一会再讲,也是十分重要的类,最后就是调用了 setUpLayoutFactories 方法,里面传入了一个 cloned 参数,继续往下走</p>    <pre>  <code class="language-java">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());          }      }</code></pre>    <p>根据版本是否大于11分为了两种Factory,这个Factory实际上是 LayoutInflater 内部的一个接口,看看官方的注释</p>    <pre>  <code class="language-java">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);      }</code></pre>    <p>当我们想要自定义操作的时候就可以通过使用Factory的来做事情,实现里面的方法来创建我们需要的view,这里我们以上面的SDK_INK大于11的情况继续往下看,先判断是不是 WrapperFactory2 的实例,第一次肯定会走进去来设置这个,也就是调用 setFactory2() 方法。</p>    <pre>  <code class="language-java">@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);          }      }</code></pre>    <p>这实际上是一个覆写的方法,并且在里面用 WrapperFactory2 来将两个Factory包装起来,继续跟进去</p>    <pre>  <code class="language-java">@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);          }      }</code></pre>    <p>构造函数中有两个参数,一个是在重写的 setFactory2 自带的Factory,一个是我们自己的 CalligraphyFactory ,在实现 Factory2 接口的两个方法中,可以看到我们最终调用的是 CalligraphyFactory 的 onViewCreated 方法,终于到了关键的地方,继续看这个方法的实现,</p>    <pre>  <code class="language-java">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;      }</code></pre>    <p>使用tag的方式,这里的tag代表的其实是有没有被处理过,也就是有没有被设置过字体,可以看到如果tag为false,那么就会调用 onViewCreatedInternal 的方法。</p>    <pre>  <code class="language-java">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);              }          }        }</code></pre>    <p>代码比较长,整体分析一下,首先是 判断是不是 TextView 的类或者是子类,然后如果已经有 TypeFace也就是字体,那么直接跳过,往下走就是 resolveFontPath 方法,这个主要是从三个方面来提取字体文件, xml , style , TextAppearance ,然后给view设置上自定义的字体。除了正常的view之外,下面还兼容了 ToolBar ,实现了 hasTypeface 接口的view,以及自定义中有 setTypeface 的view。</p>    <p>通过整个方法的调用就完成了自定义字体的设置。</p>    <h3><strong>总结</strong></h3>    <p>整个源码分析到这里差不多脉络都比较清晰了,如果还有不清楚的,可以通读一次源码,自己对照github上的sample进行修改就能理解更深。作者为了兼容不同的场景写的也比较用心,代码也比较多和杂乱,但是核心实际上就是 自定义LayoutInflater以及其中的Factory来hook住系统创建view的过程,并且加上我们自己的处理,只要理解了这个思想,无论是这种字体切换或者是皮肤切换都是一样的道理,比如切换皮肤实际上也就是切换颜色,背景等属性,这些使用Factory都是可以做到的。</p>    <p>功能虽然各式各样,但是把握核心本质,自然就能在各种需求中游刃有余</p>    <p>参考文献</p>    <ul>     <li><a href="/misc/goto?guid=4959717738464580805" rel="nofollow,noindex">https://github.com/chrisjenx/Calligraphy/issues</a></li>     <li><a href="/misc/goto?guid=4959717738556209806" rel="nofollow,noindex">http://willowtreeapps.com/blog/app-development-how-to-get-the-right-layoutinflater</a></li>     <li><a href="/misc/goto?guid=4959671190854117498" rel="nofollow,noindex">http://blog.bradcampbell.nz/layoutinflater-factories/</a></li>    </ul>    <p> </p>    <p>来自:http://www.jianshu.com/p/5d4e6ae8ba4e</p>    <p> </p>