美文网首页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