Android LayoutInflater Factory 源

作者: 未来的理想 | 来源:发表于2018-07-29 15:45 被阅读22次
    本文概述

    在上一篇文章《Android LayoutInflater 源码解析》中我们说到 View 的 inflate 中有一个方法 createViewFromTag,会首先尝试通过 Factory 来 CreateView。

        View view;
        if (mFactory2 != null) {
            // ① 有mFactory2,则调用mFactory2的onCreateView方法
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            // ② 有mFactory,则调用mFactory的onCreateView方法
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }
    

    正常情况下这个Factory是空的,那什么时候不为空,以及 LayoutInflater Factory 的具体用法,我们今天就带着这两个问题来详细学习下

    备注:本文基于 Android 8.1.0。

    1、LayoutInflater.Factory 简介

    LayoutInflater.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.

    翻译过来就是:通过 LayoutInflater 创建View时候的一个回调,可以通过LayoutInflater.Factory来改造 XML 中存在的 tag。

    我们来看下这个唯一的方法:

    public abstract View onCreateView (String name, Context context, AttributeSet attrs)
    

    那么我们就明白了,如果我们设置了LayoutInflater Factory ,在LayoutInflater 的 createViewFromTag 方法中就会通过这个 Factory 的 onCreateView 方法来创建 View。

    2、LayoutInflater.Factory 作用

    那怎么理解上述引用的这个改造呢?举个简单的例子:比如你在 XML中 写了一个 TextView标签,然后在 onCreateView 这个回调里 判断如果 name 是 TextView 的话可以变成一个Button,这样的功能可以实现例如批量更换某一个控件等的用途。例子如下:

    布局文件
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.example.liuzhaofutrue.teststart.MainActivity">
    
        <TextView
            android:id="@+id/tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
    </android.support.constraint.ConstraintLayout>
    

    接下来我们在 Java 代码中做修改:

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() {
                @Override
                public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                    if(TextUtils.equals(name,"TextView")){
                        Button button = new Button(MainActivity.this);
                        button.setText("我替换了TextView");
                        button.setAllCaps(false);
                        return button;
                    }
                    return getDelegate().createView(parent, name, context, attrs);
                }
    
                @Override
                public View onCreateView(String name, Context context, AttributeSet attrs) {
                    return null;
                }
            });
    
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    }
    

    可以看到,本来在布局文件中需要展示的是一个 TextView,但是现在却被改造成了一个 Button。

    成功改造为Button

    备注:其实还有一个关系密切的类 LayoutInflater.Factory2 ,与 LayoutInflater.Factory 的区别是

    • LayoutInflater.Factory2 是API 11 被加进来的
    • LayoutInflater.Factory2 继承自 LayoutInflater.Factory
    • 可以对创建 View 的 Parent 进行控制

    3、LayoutInflaterCompat

    刚刚我们说到,LayoutInflater.Factory2 是API 11 被加进来的,那么 LayoutInflaterCompat 就是拿来做兼容的类。我们来看下它最重要的两个方法:

        @Deprecated
        public static void setFactory(
                @NonNull LayoutInflater inflater, @NonNull LayoutInflaterFactory factory) {
            IMPL.setFactory(inflater, factory);
        }
    
        public static void setFactory2(
                @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
            IMPL.setFactory2(inflater, factory);
        }
    

    可以看到 setFactory 已经被标记为过时,更建议使用 setFactory2 方法。

        static final LayoutInflaterCompatBaseImpl IMPL;
        static {
            if (Build.VERSION.SDK_INT >= 21) {
                IMPL = new LayoutInflaterCompatApi21Impl();
            } else {
                IMPL = new LayoutInflaterCompatBaseImpl();
            }
        }
        
        @RequiresApi(21)
        static class LayoutInflaterCompatApi21Impl extends LayoutInflaterCompatBaseImpl {
            @SuppressWarnings("deprecation")
            @Override
            public void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
                inflater.setFactory2(factory != null ? new Factory2Wrapper(factory) : null);
            }
    
            @Override
            public void setFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
                inflater.setFactory2(factory);
            }
        }
    

    这里调用 setFactory 实际上还是调用的 setFactory2 方法,同时将 LayoutInflaterFactory 包裹为 Factory2Wrapper。

    4、LayoutInflater.setFactory 使用注意

    如果我们将LayoutInflater.setFactory 挪到 super.onCreate 的后面可以吗? 程序竟然报错了,我们看下Log:

        Process: com.example.teststart, PID: 24132
        java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.teststart/com.example.teststart.MainActivity}: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
            at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2876)
            at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2941)
         Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
            at android.view.LayoutInflater.setFactory2(LayoutInflater.java:317)
            at com.example.teststart.MainActivity.onCreate(MainActivity.java:18)
            at android.app.Activity.performCreate(Activity.java:6765)
    

    说明是 LayoutInflater 已经被设置了一个 Factory,而我们再设置的时候就会报错。我们跟踪下 LayoutInflater.from(this).setFactory2 方法

        private boolean mFactorySet;
        public void setFactory2(Factory2 factory) {
            if (mFactorySet) {
                throw new IllegalStateException("A factory has already been set on this LayoutInflater");
            }
            if (factory == null) {
                throw new NullPointerException("Given factory can not be null");
            }
            mFactorySet = true;
            if (mFactory == null) {
                mFactory = mFactory2 = factory;
            } else {
                mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
            }
        }
    

    可以通过这个 mFactorySet 变量看出 setFactory2 方法只能被调用一次,重复设置则会抛出异常。那Factory2是被谁设置了呢?

    我们来看下 AppCompatActivity 的 onCreate 方法,

        @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);
        }
    

    其中会调用 delegate.installViewFactory(); 最终会调用到 AppCompatDelegateImplV9 的 installViewFactory方法;

        @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");
                }
            }
        }
    

    可以看到:

    • 如果 layoutInflater.getFactory() 为空,则 AppCompatActivity 会自动设置一个 Factory2,难怪我们在 super.onCreate 之后调用会报错
    • 细心的小伙伴肯定也明白了,为什么我们在 super.onCreate 之前设置 Factory之后,系统再次设置 Factory 的时候不会抛出异常

    备注:聪明的小伙伴肯定能想到使用反射来改变修改 LayoutInflater 中的 mFactorySet 为false就可以在 super.onCreate 之后再次设置 Factory了。

    5、AppCompatActivity 为什么 setFactory

    那么为什么 AppCompatActivity 会自动设置一个 Factory呢?顺着 AppCompatDelegateImplV9 的 installViewFactory方法继续跟踪,走到了 onCreateView 方法,它最终会调用到 AppCompatViewInflater 的 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;
                ......
            }
            return view;
        }
    

    原来 AppCompatActivity 设置 Factory 是为了将一些 widget 自动变成 兼容widget (例如将 TextView 变成 AppCompatTextView)以便于向下兼容新版本中的效果,在高版本中的一些 widget 新特性就是这样在老版本中也能展示的

    那如果我们设置了自己的 Factory 岂不是就避开了系统的兼容?其实系统的兼容我们仍然可以保存下来,因为系统是通过 AppCompatDelegate.onCreateView 方法来实现 widget 兼容的,那我们就可以在设置 Factory 的时候先调用 AppCompatDelegate.onCreateView 方法,再来做我们的处理。

        LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() {
            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        
                // 调用 AppCompatDelegate 的createView方法
                getDelegate().createView(parent, name, context, attrs);
                // 再来执行我们的定制化操作
                return null;
            }
        
            @Override
            public View onCreateView(String name, Context context, AttributeSet attrs) {
                return null;
            }
        });
    

    6、总结

    1. LayoutInflater.Factory的意义:通过 LayoutInflater 创建 View 时候的一个回调,可以通过 LayoutInflater.Factory 来改造或定制创建 View 的过程。
    2. LayoutInflater.setFactory 使用注意:不能在 super.onCreate 之后设置。
    3. AppCompatActivity 为什么 setFactory ?向下兼容新版本中的效果。

    广告时间

    今日头条各Android客户端团队招人火爆进行中,各个级别和应届实习生都需要,业务增长快、日活高、挑战大、待遇给力,各位大佬走过路过千万不要错过!

    本科以上学历、非频繁跳槽(如两年两跳),欢迎加我的微信详聊:KOBE8242011

    欢迎关注

    相关文章

      网友评论

      • 左手木亽:好巧,最近也在温习View相关的. 刚看了下LayoutInflater......

      本文标题:Android LayoutInflater Factory 源

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