美文网首页Android之旅
LayoutInflaterFactory app内全局更换字体

LayoutInflaterFactory app内全局更换字体

作者: h2coder | 来源:发表于2018-03-07 23:55 被阅读80次

LayoutInflaterFactory app内全局更换字体

效果图.png
  • LayoutInflater 大家肯定很熟悉,布局填充器。但是LayoutInflaterFactory我们缺很少用,今天就来学习一下,LayoutInflaterFactory~

  • LayoutInfater,我们平时最常用就是inflate方法,其实它还有2个方法

    1. setFactory()
    2. setFactory2()
  • 2个方法,作用其实是一样的,只是setFactory2是SDK11后加入的,所以要兼容以前的版本的话,我们可以用v4包中的LayoutInflaterCompat。

LayoutInflaterFactory,它的作用是什么?简单来讲就是,我们在布局中写的控件,在反射构造完后在设置到View树之前,先过一把我们写的工厂类。我们可以对它一些操作,甚至替换掉~

  • 先来写一段代码~
Activity...

@Override
   protected void onCreate(Bundle savedInstanceState) {
       createTextTypeface();
       LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory() {
           @Override
           public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
               //动态替换成带字体的TextView
               if (name.equals("TextView")) {
                   return new Button(MainActivity.this, attrs);
               }
               return view;
           }
       });
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
   }
  • 在Activity的onCreate调用时,调用父类super.onCreate()之前,安装我们的工厂类,重写onCreateView方法,在这个方法回传了name,attrs等信息,这个name是什么呢,其实就是控件在布局上的名称,例如我们在布局上写了一个TextView,这个的name就是TextView。其实我们在布局上面写官方控件的时候,为什么我们的自定义控件要写全包名,而系统控件时却不用呢?其实在Inflate内部去Xml解析时,会先加上一个“android.view.”的前缀拼接后尝试去反射,所以我们就不用全类名了,而像RecyclerView,自定义控件这些就需要我们去写全类名了,因为系统里面没有内置他们的前缀~

  • 所以这里的name,如果是系统控件,则直接使用布局中的名字,而其他控件则判断使用全类名。而attrs,就是使用的属性和对应的值。

  • 上述代码,判断如果是TextView我们就偷偷返回了一个Button,将属性也传递进去,本来要显示的TextView就变了一个Button了。是不是很简单呢。

其实还有没有问题呢?

  • 其实在5.0开始,谷歌为了让低版本支持MD,就占用了这个接口,像TextView就有AppCompatTextView,Button就有AppCompatButton,依次类推,那么多控件,如果要更换,还补得换死人喔,但是有了这个工厂,就能一键式全更换啦。其实在AppCompateActivity就已经做了这一步了。
@Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       //创建代理
       final AppCompatDelegate delegate = getDelegate();
       //这里开始替换
       delegate.installViewFactory();
       delegate.onCreate(savedInstanceState);
       if (delegate.applyDayNight() && mThemeId != 0) {
           // If DayNight has been applied, we need to re-apply the theme for
           // the changes to take effect. On API 23+, we should bypass
           // setTheme(), which will no-op if the theme ID is identical to the
           // current theme ID.
           if (Build.VERSION.SDK_INT >= 23) {
               onApplyThemeResource(getTheme(), mThemeId, false);
           } else {
               setTheme(mThemeId);
           }
       }
       super.onCreate(savedInstanceState);
   }

   @Override
   public void installViewFactory() {
       LayoutInflater layoutInflater = LayoutInflater.from(mContext);
       if (layoutInflater.getFactory() == null) {
           LayoutInflaterCompat.setFactory2(layoutInflater, this);
       } else {
           if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
               Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                       + " so we can not install AppCompat's");
           }
       }
   }
  • 我们看到,在onCreate()方法,调用了installViewFactory()方法,其实就是调用了LayoutInflater.setFactory(),这里有个非空判断,如果之前已经安装了,则不安装了,并打印一句log,如果是这样的话,AppCompatActivity的替换Compat系列控件就无法替换了,那怎么兼容这一块呢?

  • 上述的操作都是由一个AppCompatDelegate来进行的,一开始先getDelegate()。

@NonNull
   public AppCompatDelegate getDelegate() {
       if (mDelegate == null) {
           mDelegate = AppCompatDelegate.create(this, this);
       }
       return mDelegate;
   }
  • 没有创建就创建一个AppCompatDelegate
private static AppCompatDelegate create(Context context, Window window,
           AppCompatCallback callback) {
       if (Build.VERSION.SDK_INT >= 24) {
           return new AppCompatDelegateImplN(context, window, callback);
       } else if (Build.VERSION.SDK_INT >= 23) {
           return new AppCompatDelegateImplV23(context, window, callback);
       } else if (Build.VERSION.SDK_INT >= 14) {
           return new AppCompatDelegateImplV14(context, window, callback);
       } else if (Build.VERSION.SDK_INT >= 11) {
           return new AppCompatDelegateImplV11(context, window, callback);
       } else {
           return new AppCompatDelegateImplV9(context, window, callback);
       }
   }
  • 这里就是根据当前运行的版本号去创建不同版本的代理实现,并且高版本的代理是继承低版本的,通过复写来达到兼容,例如5.0的阴影,也是一样的做法,低版本的实现类直接是空实现。
class AppCompatDelegateImplV11 extends AppCompatDelegateImplV9 {
}
  • 创建完代理后,调用delegate.installViewFactory();在installViewFactory()方法中,安装了工厂给LayoutInflate,其实就是自身,回调View的操作是onCreateView,所以我们来看它的onCreateView()
@Override
   public View createView(View parent, final String name, @NonNull Context context,
           @NonNull AttributeSet attrs) {
       if (mAppCompatViewInflater == null) {
           mAppCompatViewInflater = new AppCompatViewInflater();
       }

       boolean inheritContext = false;
       if (IS_PRE_LOLLIPOP) {
           inheritContext = (attrs instanceof XmlPullParser)
                   // If we have a XmlPullParser, we can detect where we are in the layout
                   ? ((XmlPullParser) attrs).getDepth() > 1
                   // Otherwise we have to use the old heuristic
                   : shouldInheritContext((ViewParent) parent);
       }

       //替换并返回
       return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
               IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
               true, /* Read read app:theme as a fallback at all times for legacy reasons */
               VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
       );
   }
  • 最终返回的是mAppCompatViewInflater.createView(),继续跟踪
public final View createView(View parent, final String name, @NonNull Context context,
           @NonNull AttributeSet attrs, boolean inheritContext,
           boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
       ...省略部分代码

       View view = null;

       // We need to 'inject' our tint aware Views in place of the standard framework versions
       switch (name) {
           case "TextView":
               view = new AppCompatTextView(context, attrs);
               break;
           case "ImageView":
               view = new AppCompatImageView(context, attrs);
               break;
           case "Button":
               view = new AppCompatButton(context, attrs);
               break;
           case "EditText":
               view = new AppCompatEditText(context, attrs);
               break;
           case "Spinner":
               view = new AppCompatSpinner(context, attrs);
               break;
           case "ImageButton":
               view = new AppCompatImageButton(context, attrs);
               break;
           case "CheckBox":
               view = new AppCompatCheckBox(context, attrs);
               break;
           case "RadioButton":
               view = new AppCompatRadioButton(context, attrs);
               break;
           case "CheckedTextView":
               view = new AppCompatCheckedTextView(context, attrs);
               break;
           case "AutoCompleteTextView":
               view = new AppCompatAutoCompleteTextView(context, attrs);
               break;
           case "MultiAutoCompleteTextView":
               view = new AppCompatMultiAutoCompleteTextView(context, attrs);
               break;
           case "RatingBar":
               view = new AppCompatRatingBar(context, attrs);
               break;
           case "SeekBar":
               view = new AppCompatSeekBar(context, attrs);
               break;
       }
       省略部分代码...
       return view;
   }
  • 原来是在这里判断系统控件,去替换AppCompat系列的组件,那么我们只要手动调用这个onCreateView去替换掉,不需要使用AppCompatActivity的install()。这个方法在哪呢?在AppCompatViewInflater这个类,这个类可以由AppCompatActivity的getDelegate().createView()来调用,所以,我们写的时候就可以这样:
LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory() {
           @Override
           public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
               //这里先做自己的替换操作
               if (name.equals("TextView")) {
                   return new xxx();
               }
               //为了让AppCompat的控件替换到,所以调用AppCompat的替换进行替换
               View view = getDelegate().createView(parent, name, context, attrs);
               return view;
           }
       });

用途

  • 既然可以使用工厂来进行替换原生控件,那么我们的给全局的TextView都加上字体就很容易啦
  1. 先将我们的字体放在src-main-assets文件夹,没有则需要自己新建,这个大家都懂

  2. 创建Typeface对象,给TextView设置setTypeface

public class MainActivity extends AppCompatActivity {
   private Typeface mTypeface;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       createTextTypeface();
       LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory() {
           @Override
           public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
               //为了让AppCompat的控件替换到,所以调用AppCompat的替换进行替换
               View view = getDelegate().createView(parent, name, context, attrs);
               if (view != null && view instanceof TextView) {
                   //这里给每个TextView都设置上字体
                   ((TextView) view).setTypeface(mTypeface);
               }
               return view;
           }
       });
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
   }

   //创建字体
   private void createTextTypeface() {
       mTypeface = Typeface.createFromAsset(getAssets(), "QingXinKaiTi.ttf");
   }
}
  1. 也可以自定义TextView去调用字体设置,替换TextView喔
LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory() {
           @Override
           public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
               //动态替换成带字体的TextView
               if (name.equals("TextView")) {
                   return new FontTextView(MainActivity.this, attrs);
               }
               //为了让AppCompat的控件替换到,所以调用AppCompat的替换进行替换
               View view = getDelegate().createView(parent, name, context, attrs);
               if (view != null && view instanceof TextView) {
                   ((TextView) view).setTypeface(mTypeface);
               }
               return view;
           }
       });
  1. 最后将这句话放到Activity的基类的onCreate(),这样就大功告成啦~

有些继承了官方控件,在项目中不好替换时,就可以这样动态替换啦,例如我们的通用滚动库,就是给每个滚动控件都写一个子类,实现接口,再用工厂进行偷梁换柱~

  1. github地址

相关文章

网友评论

    本文标题:LayoutInflaterFactory app内全局更换字体

    本文链接:https://www.haomeiwen.com/subject/sbcefftx.html