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的用法及

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容