LayoutInflater#inflate()方法详解

1.方法原型及参数的意义

public View inflate(@LayoutRes int resource, 
                        @Nullable ViewGroup root, 
                            boolean attachToRoot)  

初次接触这个方法还是在1年多以前使用RecycleView的Adapter的时候,当时怎么也理解不了这是在干嘛,后来勉强知道这个方法主要是用于把编写好的XML转化为能在屏幕上显示的View,再后来知道了各个参数的意义,但是没深究过原因。恰逢最近在看源码,就结合方法的源码来分析一下各个参数的不同取值的效果。
三个参数中的第一个很好理解,第二个和第三个如果就初次接触的话还是会有点懵逼的。

resource:需要转换成View的布局文件。
root: 可选的参数,表示为resource提供一个根布局,使得他的宽高属性生效。
attachToRoot:是否将resource的View加入到root中,然后返回。

2.举例探究参数不同取值的情形

首先,创建一个Activity和他对应的布局文件,并且新建一个待加载的布局文件layout_to_be_added.xml。

TestActivity.java

public class TestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

    }
}

activity_test.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:id="@+id/ll_activity_test"
    android:background="#8af47a"
    >

</LinearLayout>

layout_to_be_added.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/layout_to_be_add"
    android:background="#dc4444"
    >

    <Button
        android:id="@+id/btn_test"
        android:text="一个Button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />
</RelativeLayout>

此时的界面如下图:
初始的时候

2.1 root != null, attachToRoot == true

在这种情况下,如果想把layout_to_be_added.xml加载到activity_test.xml中,按照常规的思路,可能会写出这样的代码。

public class TestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        LinearLayout activityLayout = (LinearLayout) findViewById(R.id.ll_activity_test);//获取到activity的ViewGroup

        View layoutToBeAdded =
                LayoutInflater.from(this).inflate(R.layout.layout_to_be_added,activityLayout,true);

        activityLayout.addView(layoutToBeAdded);
    }
}

看似没有一点毛病,但是run一下竟然报错了:

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

报错的主要原因是子布局早已有了父布局,无法添加。

其实这个错误发生的原因很好理解,由于root不为空(指定成了Activity的布局),而attachToRoot又为true,这会使得layout_to_be_added.xml对应的View会在inflate方法内部被加载到activityLayout之中,然后返回。既然已经加载进去了,自然不需要再次单独调用addView方法。
既然如此,将activityLayout.addView(layoutToBeAdded);注释掉即可,注释掉后会显示如下界面:

正常显示

2.2 root != null, attachToRoot == false

那么,如果root设置为activityLayout,attachToRoot为false又会如何呢?显然,这时候返回的View就是layout_to_be_added.xml对应的View,最后需要手动调用addView方法添加到Activity中:

public class TestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        LinearLayout activityLayout = (LinearLayout) findViewById(R.id.ll_activity_test);//获取到activity的ViewGroup

        View layoutToBeAdded =
                LayoutInflater.from(this).inflate(R.layout.layout_to_be_added,activityLayout,false);

        activityLayout.addView(layoutToBeAdded);
    }
}

当然,此时显示肯定是一切正常:

正常显示

到了这里心中难免会有疑惑:明明只需要返回layout_to_be_added.xml对应的View,把root设置为activityLayout干啥?可不可以把它设置成其他的ViewGroup?答案是当然可以。
这里的root的作用仅仅是为了让layout_to_be_added.xml设置的布局参数(如layout_width,layout_height,layout_gravity等)生效,因为view的onMeasure过程是由父容器传递过来的,如果没有指定父容器,id为id="@+id/layout_to_be_add"的这个RelativeLayout的布局参数就会失效,所以必须得指定ViewGroup,当然这里可以指定任意的ViewGroup,因为它的作用仅仅是为了让View的布局参数有效,完全可以直接new一个Layout,比如activityLayout是一个LinearLayout,这里完全就可以这样写:

LinearLayout l = new LinearLayout(this);
View layoutToBeAdded =
        LayoutInflater.from(this).inflate(R.layout.layout_to_be_added,l,false);

2.3 root == null, attachToRoot == true /false

结合第二种情况的分析,如果不指定root,那么layout_to_be_added.xml对应的View的布局参数将失效,这时候无论attachToRoot设置为什么值都会出现如下的显示效果:
root设置为null的时候
此时由于Button是有父布局的,所以Button的布局参数还是有效的。

3.结合源码分析不同参数的意义

LayoutInflater.java

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
          //省略无关代码...
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);//又调用了另一个重载方法
        } finally {
            parser.close();
        }
    }

很明显,在try的语句块里调用了另一个重载方法:

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);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;

            //默认返回root
            View result = root;

            try {
                // Look for the root node.
                int type;
                    //...

                final String name = parser.getName();
                //...

                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
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                            //...
                        // Create layout params that match root, if supplied
                        //构建ViewGroup.LayoutParams,以便设置给temp
                        params = root.generateLayoutParams(attrs);
                        //attachToRoot为false,则直接将ViewGroup.LayoutParams设置给temp
                        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,且attachToRoot为true,则将View添加到root中去
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.

                    //如果没有设置root,或者attachToRoot为false,则直接将View赋值给result,最后返回,由于没有root,故temp的布局参数会失效(因为从上面的代码可以看出并没有给temp设置ViewGroup.LayoutParams)
                    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);
            }

            return result;
        }
    }

结合源码,我也在里面给出了关键步骤的注释,整个逻辑还是不难理解。
总之,如果没有设置root,则不会给temp设置正确LayoutParams,这样一来temp的布局参数就成了默认的了(相当于没有)。当然如果将attachToRoot设置为true,在设置了root的情况下,temp会被直接加入root,然后返回,否则,将会返回没有布局参数的temp(这里就是指layout_to_be_added.xml对应的View)。而如果attachToRoot设置为了flase,这不会将View加载到root中。

4.不同情形下参数该如何传值?

这里的传值主要以attachToRoot的值分情况讨论:

4.1 常见到的情形是attachToRoot==flase的情况:

如果希望单纯滴把一个layout转换成View,attachToRoot就应该为false,比如:
1.经典的RecyclerView的Adapter的场景:

     @Override
        public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
        {

            View view = LayoutInflater.from(
                    parent.getContext()).inflate(R.layout.item_home, parent,
                    false)
            MyViewHolder holder = new MyViewHolder(view);
            return holder;
        }

这里如果将attachToRoot设置成true是肯定会报错的,为啥?当然是因为RecyclerView的item的添加与删除是由Adapter全权负责的,并不需要我们手动操作,控制权不在我们这里,当然,这一点在其源码里也能够体现:

if (child.getParent() != null) {

            throw new IllegalStateException("The specified child already has a parent. " +

                    "You must call removeView() on the child's parent first.");

        }
}

2.给Fragment设置布局的时候

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_class_running_ranking, container, false);
    }

以上代码是ViewPager+Fragment+TabLayout的经典使用场景下的Fragment,这里Fragment何时添加到容器中,也全靠adapter来处理,不用我们手动操作。

3.在自定义View中用于将某个布局转化成View的时候。

4.2 再看看attachToRoot==true的情况:

这种情况下主要是会用到inflate的另一个重载方法:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
很显然这样用会直接将layout转换成View,然后加载到我们希望的ViewGoup中,多用于自定义ViewGroup中,比较简单。

最后,为了View的能像布局文件里描绘的那样显示,能传root的时候还是传吧~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值