Android中LayoutInflater详解

前言

身为Android开发者,我们知道在用RecycleView在onCreateViewHolder方法里通常是加载Item的布局文件,如下:

    @NonNull
    @Override
    public CustomerAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.adapter_row_item,parent,false);
        return new ViewHolder(v);
    }

发现

LayoutInflater.from(parent.getContext()).inflate(R.layout.adapter_row_item,parent,false);

RecycleView是通过LayoutInflater.from的方式将Item的根布局视图渲染出来。

LayoutInflater是什么?

这个LayoutInflater到底是什么呢?查看官方api,官方说明如下:


LayoutInflater官方说明

大概意识是:将布局xml文件实例化到对应的视图对象中,前提是这个视图对象中没有添加过这个xml布局。使用Activity.getLayoutInflate()或者Context.getSystemService(Class)来检索这个已经正在运行设备上的实例。
要为自己的视图创建带有LayoutInflate.Factory的新LayoutInflate,你可以使用cloneInContext(Context)来复制现有的ViewFactory,然后调用其上的setFactory(LayoutInflate.Factory)。
由于性能原因,视图膨胀严重依赖于构建时对XML文件的预处理。因此,目前不可能运行时将LayoutInflate与XmlPullParser一起在普通XML文件上使用;它只能从编译资源返回的XmlPullParser中使用。
这个类的实例必须使用LayoutInflate.class的Context.getSystemService(Class)或者Context.LAYOUT_INFLATER_SERVICE的Context.getSystemService(String)获得。
 
其实这个类LayoutInflater是Android开发中经常遇到的,因为我们所写的XML文件布局中都是通过LayoutInflater.from inflate成具体的View对象。但是大家会疑问,没有看到呀?我们平时加载布局不都是通过Activity的setContentView()来完成加载布局的吗?通过查看setConentView可以发现首先调用getWindow拿到window对象,而创造window对象的时候就初始化了LayoutInflate对象,最后调用inflate方法使用pull解析方式解析XML最后返回View。
 
通过上面分析可知,LayoutInflater这个类作用类似findViewById(),但是LayoutInflater是用来找layout/下的xml文件进行实例化,LayoutInflater是一个用于加载布局的系统服务,就是用来实例化和布局XML文件对应的View对象,但是它不能直接使用,需要通过getLayoutInflate()方法或者getSystemService()方法获得和当前Context绑定的实例。
获取LayoutInflater实例的三个方法:

LayoutInflater inflater = LayoutInflater.from(this);  
LayoutInflater inflater = getLayoutInflater();  
LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); 

LayoutInflater的inflate方法源码解析

inflate的重载方法有四个,依次:

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
        return inflate(parser, root, root != null);
    }
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            //mConstructorArgs 是长度为2的Object 数组
            Context lastContext = (Context) mConstructorArgs[0];
            //第1个放了context对象
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                final String name = parser.getName();

                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    //根据tag节点来创建View
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        //创建LayoutParams布局
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            //设置布局参数
                            temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    // Inflate all children under temp against its context.
                    //解析temp的子View
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        //如果root不为空,并且attachToRoot为true
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(parser.getPositionDescription()
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            // 如果view被添加到root则返回root 后再返回xml解析出来的view
            return result;
        }
    }

可以发现前面三个方法都是调用了第四个方法,那么重点看第四个方法即可:
先看官方的注释:

/**
     * Inflate a new view hierarchy from the specified XML node. Throws
     * {@link InflateException} if there is an error.
     * <p>
     * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance
     * reasons, view inflation relies heavily on pre-processing of XML files
     * that is done at build time. Therefore, it is not currently possible to
     * use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
     *
     * @param parser XML dom node containing the description of the view
     *        hierarchy.
     * @param root Optional view to be the parent of the generated hierarchy (if
     *        <em>attachToRoot</em> is true), or else simply an object that
     *        provides a set of LayoutParams values for root of the returned
     *        hierarchy (if <em>attachToRoot</em> is false.)
     * @param attachToRoot Whether the inflated hierarchy should be attached to
     *        the root parameter? If false, root is only used to create the
     *        correct subclass of LayoutParams for the root view in the XML.
     * @return The root View of the inflated hierarchy. If root was supplied and
     *         attachToRoot is true, this is root; otherwise it is the root of
     *         the inflated XML file.
     */

第一个参数XmlPullParser parser:这个是用来解析XML布局,布局解析器。
第二个参数ViewGroup root:这个是可选的参数,可以传入null,如果attachToRoot的值为true,返回的root是生成视图的父类,如果attachToRoot的值为false,root是一个对象,为返回的视图提供一系列的LayoutParams参数。
第三个参数boolean attachToRoot:这个参数决定生成的视图是否添加到root上,如果这个值为false,root仅仅用来为xml布局生成正确的布局参数。
返回参数是一个View类型:如果root参数传入不是null,并且attachToRoot的值是true,则返回的值是root参数,否则返回值就是XML布局的根节点视图。
下面提供一个XML布局,我们根据源码一行一行走:

<?xml version="1.0" encoding="utf-8"?>
<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.recycleviewdemo.MainActivity">

    <Button
        android:id="@+id/btn_update"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycleview"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@id/btn_update"
        app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>
final AttributeSet attrs = Xml.asAttributeSet(parser);

生成的attrs是XML布局根节点的参数集合

View result = root;

View类型的result外面传递进来的root参数,而inflate方法最后返回的是result

final View temp = createViewFromTag(root, name, inflaterContext, attrs);

生成XML布局的根节点temp,XML布局就是上面,调用inflate方法将布局转为View时,那么temp就是ConstraintLayout。

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

如果传递root不为null,就执行上面代码,根据attrs生成XML根布局的布局参数params,如果attachToRoot为false,表示XML布局生成的View不添加到root上,执行setLayoutParams(params),即为根节点temp设置布局参数。

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

如果root不为空,并且参数attachToRoot为true,XML布局生成的View就会添加到root,执行root.addView(temp,params)。

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }

如果root为空,或者attachToRoot为false,意思是没有为XML布局设置父布局或者不想挂载在root上,那么result就是temp,也就是XML布局的根节点,那么上面XML的根节点就是ConstraintLayout。
最后就是返回result了。

简单例子

通过上面的分析,应该有一个大概了解,下面通过几个小例子来实践加深理解:
MainActivity界面布局如下:

<?xml version="1.0" encoding="utf-8"?>
<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"
    android:id="@+id/cl_main"
    tools:context="com.example.recycleviewdemo.MainActivity">


</android.support.constraint.ConstraintLayout>

resource指定的布局如下:
adapter_row_item.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:background="@color/colorAccent">


    <TextView
        android:id="@+id/textview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/recycleview_text"/>
</FrameLayout>

View inflate( int resource, ViewGroup root, boolean attachToRoot)

1.root不为null,attachToRoot为true

也就是想把adapter_row_item.xml布局添加到MainActivity布局中:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ConstraintLayout cl_main = findViewById(R.id.cl_main);
        LayoutInflater inflater = getLayoutInflater();
        inflater.inflate(R.layout.adapter_row_item,cl_main,true);

    }

效果如下图所示:


添加指定布局到root上

效果确实如上面分析。也就是当attachToRoot为true,会自动把第一个参数resource的布局文件解析成View后添加到root所指定的View中。
所以不用调用addView方法刻意添加。如果加多addView:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ConstraintLayout cl_main = findViewById(R.id.cl_main);
        LayoutInflater inflater = getLayoutInflater();
        View view = inflater.inflate(R.layout.adapter_row_item,cl_main,true);
        cl_main.addView(view);
    }

会抛出下面异常:

     Caused by: java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.

意思是不能重复添加。

2.root不为null,attachToRoot为false

表示不将第一个参数指定的View添加到root,这种方式和直接将root设为null是有区别的。如果root为null,那么第一个参数所指定的View的根节点layout_width和layout_height就会失效,因为这个View不在任何容器上,如果现在想让这个View的根节点有效,又不想让它在任何一个容器中,那就可以设置root不为null,attachToRoot为false。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ConstraintLayout cl_main = findViewById(R.id.cl_main);
        LayoutInflater inflater = getLayoutInflater();
        inflater.inflate(R.layout.adapter_row_item,cl_main,false);

    }

实际效果adapter_row_item.xml解析出来的View没有添加到MainActivity布局上,得要加

        View view = inflater.inflate(R.layout.adapter_row_item,cl_main,false);
        cl_main.addView(view);

实际和上面效果一致。

3.root为null

当root为null的时候,无论attachToRoot的值为false还是true,效果都是一样的,因为不将第一个参数指定的View添加到任何容器中,也没用任何容器约束第一个参数所指定的View来生成布局参数:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ConstraintLayout cl_main = findViewById(R.id.cl_main);
        LayoutInflater inflater = getLayoutInflater();
        View view = inflater.inflate(R.layout.adapter_row_item,null,false);
        cl_main.addView(view);
    }

实际效果如下:


root为null

无论我设置FrameLayout的根节点宽高什么都是没有效果的,如果我设置TextView的属性就会有变化。

View inflate(int resource, ViewGroup root)

1.root为null

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ConstraintLayout cl_main = findViewById(R.id.cl_main);
        LayoutInflater inflater = getLayoutInflater();
        View view = inflater.inflate(R.layout.adapter_row_item,null);
        cl_main.addView(view);
    }

实际效果和root为null的情况一样

2.root不为null

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ConstraintLayout cl_main = findViewById(R.id.cl_main);
        LayoutInflater inflater = getLayoutInflater();
        inflater.inflate(R.layout.adapter_row_item,cl_main);
  
    }

实际效果和root不为null,attachToRoot为true情况一致。因为始终还是调用了三个参数的方法,只不过当root为null,的时候attachToRoot的值就会设为false,如果root不为null,attachToRoot的值为true。

总结

通过初步源码的分析和简单例子实践,对LayoutInflater和inflate方法有一定了解,其实root就是为XML布局生成的View指定一个父类,让其添加在上面,root如果不为null,那么XML布局的根节点就会生成布局参数,由attachToRoot决定这个View想不想成为root的孩子。root如果为null,由于XML布局所指定的View没有处于任何一个容器中,所以根节点的宽高属性失效,但是可以通过addView加载到root上。所以现在知道文章刚开始RecycleView在onCreateViewHolder方法里通常是加载Item的布局文件方法为啥要将attachToRoot设置为false,因为我们不负责将layout文件的View添加进ViewGroup,ListView和Recycleview负责决定什么时候展示它的子View,这个我们决定不了。
注意:

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

推荐阅读更多精彩内容