美文网首页Android
LayoutInflater.setFactory学习及进阶

LayoutInflater.setFactory学习及进阶

作者: zizi192 | 来源:发表于2018-11-27 10:06 被阅读0次

    相信大家对LayoutInflater都不陌生,它经常被用来根据xml生成View。比较熟悉的方法包括:

    • LayoutInflater.from(Context context)
    • inflate(@LayoutRes int resource, @Nullable ViewGroup root)
    • inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

    构造方法源码如下,可见LayoutInflater.from(Context context)等同于context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)。

        /**
         * Obtains the LayoutInflater from the given 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;
        }
    

    除了上述方法,我今天想介绍的是相对不常用的两个方法。

    • setFactory(Factory factory)
    • setFactory2(Factory2 factory)

    这两个方法基本功能一致。系统通过Factory提供了一种hook的方法,方便开发者拦截LayoutInflater创建View的过程。应用场景包括1)在XML布局中自定义标签名称;2)全局替换系统控件为自定义View; 3)替换app中字体;4)全局换肤等。

    Factory与Factory2的区别

    二者都是LayoutInflater类内部定义的接口。Factory2继承自Factory接口,在API 11(HONEYCOMB)中引入的。Factory2比Factory多增加了一个onCreateView(View parent, String name, Context context, AttributeSet attrs),该方法多了一个parent,用来存放构建出的View。

    Android在v4包中提供了LayoutInflaterCompat来帮助完成兼容性的操作。

    • setFactory(
      @NonNull LayoutInflater inflater, @NonNull LayoutInflaterFactory factory)

      在API Level 26.1.0中被标记为Deprecated,官方推荐使用setFactory2方法
    • setFactory2(
      @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory)

    Factory接口的定义如下,该接口只有一个onCreateView方法。

        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.
             *
             * <p>
             * Note that it is good practice to prefix these custom names with your
             * package (i.e., com.coolcompany.apps) to avoid conflicts with system
             * names.
             *
             * @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);
        }
    

    Factory2接口的源码定义如下。

    public interface Factory2 extends Factory {
            /**
             * Version of {@link #onCreateView(String, Context, AttributeSet)}
             * that also supplies the parent that the view created view will be
             * placed in.
             *
             * @param parent The parent that the created view will be placed
             * in; <em>note that this may be null</em>.
             * @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(View parent, String name, Context context, AttributeSet attrs);
        }
    

    AppCompatActivity中系统Factory实现

    Activity常用的基类包括Activity,FragmentActivity和AppCompatActivity,关于它们三者的区别,可以参考我的文章Activity、FragmentActivity和AppCompatActivity的区别

    其中AppCompatActivity在v7包中引入,查看其源码,其中onCreate方法设置了一个AppCompatDelegate。

       @Override
       protected void onCreate(@Nullable Bundle savedInstanceState) {
            final AppCompatDelegate delegate = getDelegate();
            delegate.installViewFactory();
            delegate.onCreate(savedInstanceState);
            ...
            super.onCreate(savedInstanceState);
        }
    

    AppCompatDelegate是一个抽象基类,其对象实例根据手机sdk版本来初始化,具体可参考源码。其中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");
                }
            }
        }
    

    上述代码可知,如果AppCompatActivity未在onCreate之前设置LayoutInflater的Factory,则AppCompatActivity会尝试设置一个Factory2,其中Factory2在AppCompatDelegate的具体子类代码中实现。注意,在API Level 26及以后,LayoutInflaterCompat.setFactory被标记为Deprecated,故我参考的v27的源码中使用的是LayoutInflaterCompat.setFactory2。

    根据Activity、FragmentActivity和AppCompatActivity的区别,官方提供的AppCompatDelegate子类实现,如AppCompatDelegateImplN。帮助我们实现了AppCompat风格组件的向下兼容,利用AppCompatDelegateImplN提供的Factory2将TextView等组件替换为AppCompatTextView,这样就可以使用一些新的属性,如autoSizeMinTextSize。

    Activity中setFactory的兼容性问题

    上面也提到过,通过setFactory或setFactory2可以实现一些特殊功能,如全局自定义View替换,应用换肤等。但是需要注意兼容性问题,保证AppCompat风格组件的正确替换。

    注意,需要在调用super.onCreate(savedInstanceState)之前进行LayoutInflaterCompat.setFactory2的设置。否则setFactory并不能进行重复设置,会导致后设置的Factory失效。

    探究AppCompatDelegateImplN中Factory2接口的具体实现。

        /**
         * From {@link LayoutInflater.Factory2}.
         */
        @Override
        public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            // First let the Activity's Factory try and inflate the view
            final View view = callActivityOnCreateView(parent, name, context, attrs);
            if (view != null) {
                return view;
            }
    
            // If the Factory didn't handle it, let our createView() method try
            return createView(parent, name, context, attrs);
        }
    

    可知最终是调用AppCompatDelegate实例中的createView方法进行AppCompat组件的绘制。故兼容写法如下:

    public class MainActivity extends AppCompatActivity
    {
       private static final String TAG = "MainActivity";
    
       if (typeface == null)
       {
           typeface = Typeface.createFromAsset(getAssets(), "x x.ttf");
       }
    
       @Override
       protected void onCreate(Bundle savedInstanceState)
       {
           LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory()
           {
               @Override
               public View onCreateView(View parent, String name, Context context, AttributeSet attrs)
               {
                    //你可以在这里直接new自定义View
    
                    //你可以在这里将系统类替换为自定义View
    
                    //appcompat 创建view代码
                   AppCompatDelegate delegate = getDelegate();
                   View view = delegate.createView(parent, name, context, attrs);
    
                   //替换字体示例
                   if ( view!= null && (view instanceof TextView))
                   {
                       ((TextView) view).setTypeface(typeface);
                   }
    
                   return view;
               }
           });
           super.onCreate(savedInstanceState);
           setContentView(R.layout.activity_main);
       }
    
    

    Acitivity中setContentView的调用流程

    以最常用的setContentView(@LayoutRes int layoutResID)方法为起点,跟踪view的绘制流程

        public void setContentView(@LayoutRes int layoutResID) {
            getWindow().setContentView(layoutResID);
            initWindowDecorActionBar();
        }
    

    关于Activity中的window实例创建,相信大家都有所了解。PhoneWindow是抽象基类window的具体实现,且该类内部持有一个DecorView对象,也即Activity界面的根View。

    PhoneWindow的setContentView方法如下:

        @Override
        public void setContentView(int layoutResID) {
            // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
            // decor, when theme attributes and the like are crystalized. Do not check the feature
            // before this happens.
            if (mContentParent == null) {
                installDecor();
            } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
                mContentParent.removeAllViews();
            }
    
            if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
                final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                        getContext());
                transitionTo(newScene);
            } else {
                mLayoutInflater.inflate(layoutResID, mContentParent);
            }
            mContentParent.requestApplyInsets();
            final Callback cb = getCallback();
            if (cb != null && !isDestroyed()) {
                cb.onContentChanged();
            }
            mContentParentExplicitlySet = true;
        }
    

    阅读代码,可看到关键的调用语句mLayoutInflater.inflate(layoutResID, mContentParent),将资源文件构建成View树,并添加到mContentParent视图中。其中mLayoutInflater是在PhoneWindow的构造函数中得到实例对象的LayoutInflater.from(context)。可以多次调用setContentView()来显示界面,每次绘制之前会调用removeAllViews来移除原有页面。

    PhoneWindow类的setContentView(View view)方法和setContentView(View view, ViewGroup.LayoutParams params)方法源码,原理同上!

    最终结合LayoutInflater的infalte方法,参考Android LayoutInflater源码解析,真正创建view的方法是LayoutInflater的createViewFromTag方法。会依次调用mFactory2、mFactory和mPrivateFactory三者之一的onCreateView方法去创建一个View。如果不存在Factory,则调用LayoutInflater自身的onCreateView或者createView来实例化View。

    根据上面的流程可知,可通过setFactory或setFactory2来拦截view的创建过程,进行一些特殊的操作。

    Activity中onCreateView方法

    Activity对象实现了LayoutInfalter.Factory2接口,提供了onCreateView方法的缺省实现。在Activity的attach方法中,为Window的LayoutInflater设置了mPrivateFactory对象。也可以通过重新Activity的onCreateView方法进行特定的操作。但是拦截时机晚于LayoutInfalter的setFactory和setFactory2方法。

    根据AppCompatActivity的学习也可知,在未对AppCompatActivity设置Factory或Factory2时,系统通过AppCompatDelegate自动设置了Factory2实例。故一般的换肤方案都是通过setFactory或setFactory2实现对view创建过程的侵入。


    参考文章:
    Android 探究 LayoutInflater setFactory
    Android应用setContentView与LayoutInflater加载解析机制源码分析
    https://github.com/hongyangAndroid/ChangeSkin
    侵入性低扩展性强的Android换肤框架XSkinLoader的用法及

    相关文章

      网友评论

        本文标题:LayoutInflater.setFactory学习及进阶

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