LayoutInflater.setFactory学习及进阶

相信大家对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的用法及

推荐阅读更多精彩内容