Android开发之LayoutInflater及其源码分析

LayoutInflater的inflate方法是在Android开发中常用到的一个方法,不过它的参数却让很多初学者很头疼,不知道它们分别代表什么含义,本文结合了其他几篇博客做出了一个总结。inflater方法有以下4个重载的方法如下:

public View inflate(int resource, ViewGroup root)
public View inflate(int resource, ViewGroup root, boolean attachToRoot) 
public View inflate(XmlPullParser parser, ViewGroup root)
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)

在这4个重载方法中,最常用的是前两个(准确的说是第二个,反正第三第四个方法我是没用过),那么本文就详细的分析以下前两个方法的参数设置为不同的值会导致什么结果。第二个方法比第一个多了一个attachToRoot参数,其他都是一样的。先来看看第二个方法的三个参数:第一个参数resource是要加载的布局资源,第二个参数root是解析出来View的父View,第三个参数attachTooRoot表示是否将解析出来的View当作第二个参数root的子View。

先不说那些花里胡哨的,直接给出结论

  1. 如果root为null,attachToRoot将失去作用,设置任何值都没有意义。
  2. 如果root不为null,attachToRoot设为true,则会给加载的布局文件的指定一个父View (就是参数中的root)。
  3. 如果root不为null,attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效(这条结论比较难理解)。
  4. 在不设置attachToRoot参数的情况下(就是调用第一个方法),如果root不为null,attachToRoot参数默认为true。

第一条结论很好理解,attachToRoot参数就是针对第二个参数root而言的,如果root为空的话attachToRoot当然就没了意义。

针对第二条结论,这里举了一个郭神博客的例子:比如说当前有一个项目,其中MainActivity对应的布局文件叫做activity_main.xml,代码如下所示:

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

</LinearLayout>

只有一个空的LinearLayout,因此界面上应该不会显示任何东西。那么接下来我们再定义一个布局文件,给它取名为button_layout.xml,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="300dp"
    android:layout_height="180dp"
    android:text="Button" >
</Button>

这个布局文件也只有一个按钮而已。现在我们要通过LayoutInflater来将button_layout中的按钮添加到主布局文件的LinearLayout中。MainActivity中的代码如下所示:

public class MainActivity extends AppCompatActivity {

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

        LinearLayout mainLayout = findViewById(R.id.main_layout);
        LayoutInflater.from(this).inflate(R.layout.button_layout, mainLayout, true);
    }
}

运行出来结果如下:
在这里插入图片描述
可以看到,代码里面没调用mainLayout的addView方法,按钮也被放到了mainLayout中,这证实了第二条结论。

对于第三条结论,还是上面的例子只是将方法中的attachToRoot设为false,运行结果如下:

在这里插入图片描述
发现界面中什么都没有,这是因为根据第二条结论,如果设为false则inflate方法不会为这个按钮添加父布局,需要我们手动添加,代码如下:

public class MainActivity extends AppCompatActivity {

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

        LinearLayout mainLayout = findViewById(R.id.main_layout);
        View view = LayoutInflater.from(this).inflate(R.layout.button_layout, mainLayout, false);
        mainLayout.addView(view);
    }
}

再次运行,和第二条结论中效果一样:
在这里插入图片描述
这里便产生了一个疑问,第三条结论说的会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效代表什么意思呢?
我们修改以下代码,将第二个参数root改为null:

View view = LayoutInflater.from(this).inflate(R.layout.button_layout, null, false);

运行以下是这样的:
在这里插入图片描述
发现按钮形状变了,其宽高不再是我们在布局里面设置的300dp和180dp了,如果你不信邪的话可以随意设置宽高,发现不管怎么设置都是上面这个结果,为什么会这样呢?
这里直接复制以下郭神的解释(偷个懒):

其实这里不管你将Button的layout_width和layout_height的值修改成多少,都不会有任何效果的,因为这两个值现在已经完全失去了作用。平时我们经常使用layout_width和layout_height来设置View的大小,并且一直都能正常工作,就好像这两个属性确实是用于设置View的大小的。而实际上则不然,它们其实是用于设置View在布局中的大小的,也就是说,首先View必须存在于一个布局中,之后如果将layout_width设置成match_parent表示让View的宽度填充满布局,如果设置成wrap_content表示让View的宽度刚好可以包含其内容,如果设置成具体的数值则View的宽度会变成相应的数值。这也是为什么这两个属性叫作layout_width和layout_height,而不是width和height。再来看一下我们的button_layout.xml吧,很明显Button这个控件目前不存在于任何布局当中,所以layout_width和layout_height这两个属性理所当然没有任何作用。那么怎样修改才能让按钮的大小改变呢?可以在button的外部再设置一层布局:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
 
    <Button
        android:layout_width="300dp"
        android:layout_height="80dp"
        android:text="Button" >
    </Button>
 
</RelativeLayout>

可以看到,这里我们又加入了一个RelativeLayout,此时的Button存在与RelativeLayout之中,layout_width和layout_height属性也就有作用了。当然,处于最外层的RelativeLayout,它的layout_width和layout_height则会失去作用。这个方法虽然有效,但是平白无故的多嵌套了一个布局是没有什么好处的(因为系统就会多解析了一层布局),那么有什么更好的方法呢?说到这里就知道了第三个结论的意义所在了吧,虽然attachToRoot设为false但是如果root不设置为空,它会让这个布局的最外层所有layout属性进行设置,当该view被添加到root中时,这些layout属性会自动生效。

对于第四条结论,更改MainActivity代码:

public class MainActivity extends AppCompatActivity {

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

        LinearLayout mainLayout = findViewById(R.id.main_layout);
        LayoutInflater.from(this).inflate(R.layout.button_layout, mainLayout);
    }
}

运行发现结果之前的是一样的:
在这里插入图片描述
这是因为没有attachToRoot的话会默认为true,就相当于第二个重载方法里面将其设为true。

这样一来就通过一个例子来证明了上述四条结论的正确性,但是知其然也要知其所以然,接下来就从源码的角度上看一看为什么这四条结论是对的。
其实虽然inflate方法有4个重载,但是不管调用哪一个,最后都会调用到最后一个方法中。
点开LayoutInflater的第4个inflate方法源码:

/* @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.
 */
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            ....
            
            View result = root;

            try {
                advanceToRootNode(parser);
                final String name = parser.getName();                
                ...    
                //判断根节点是否为merge                            
                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) {
                        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设置布局参数
                            temp.setLayoutParams(params);
                        }
                    }

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

                    // Inflate all children under temp against its context.
                    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.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(
                        getParserStateDescription(inflaterContext, attrs)
                        + ": " + 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;
        }
    }

上述代码首先将生命了一个返回值result对象指向root,为什么要这么做呢?来看一下上面的注释:注释里面说如果root不为null且attachToRoot为true,则将root返回,否则就返回要解析xml文件的根节点,也就是说返回值只有这两中可能,具体返回什么要视参数情况而定。

接着往下看,先判断了xml的根节点是否为merge,根节点设为merge代表将你写的这个资源文件以系统的FrameLayout为父布局,在根节点是merge情况下,父root为null或者attachToRoot选择了flase,则抛出异常,因为merge需要融合,而我们父root又为null或者选择不附加到父root,这不符合逻辑,所以抛了个异常,反之则执行rinflate方法。

若xml的节点不是merge,即正常的布局,则调用了createViewFromTag方法创建了一个temp(也就是xml中的根View),现在思路就变的清晰了,要返回的两个选择现在都有了,要么是result(也就是root),要么是temp(布局的根view)。

接着往下看,如果root不为空的话,就创建一个布局参数LayoutParams,这时候如果attachToRoot为false的话,就通过temp.setLayoutParams(params)来将布局参数设置到temp上,这恰好证明了第三条结论的正确性。

接下来调用了rInflateChildren(parser, temp, attrs, true)来解析temp下的子节点,在这个方法中调用rInflate方法再解析子节点下面的子节点,这样一步一步递归,每次递归完成后则将这个View添加到父布局当中,这样就完成了整个xml文件的解析。
rInflateChildren方法如下:

void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    final int depth = parser.getDepth();
    int type;
    boolean pendingRequestFocus = false;
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
        if (type != XmlPullParser.START_TAG) {
            continue;
        }
        final String name = parser.getName();
        if (TAG_REQUEST_FOCUS.equals(name)) {
            pendingRequestFocus = true;
            consumeChildElements(parser);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else {
            // 生成View
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            // 递归解析子节点
            rInflateChildren(parser, view, attrs, true);
            // 添加到父view中
            viewGroup.addView(view, params);
        }
    }
    if (pendingRequestFocus) {
        parent.restoreDefaultFocus();
    }
    if (finishInflate) {
        parent.onFinishInflate();
    }
}

回到inflate方法中,接下来判断如果root不为null而且attachToRoot为true的话,通过root.addView(temp, params);方法将temp放到root中来,这验证了第二条结论。

再接着,若root为空或者attachToRoot为false的话,就将result指向temp,这种情况inflate方法返回值就是xml的根节点,否则返回root,这验证了最上面注释的内容。方法的最后,将result返回。

这样一来inflate方法就分析的差不多了。接下来再来一个小小的拓展,若将root设为null,我们手动为其添加父布局:

public class MainActivity extends AppCompatActivity {

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

        LinearLayout mainLayout = findViewById(R.id.main_layout);
        View v = LayoutInflater.from(this).inflate(R.layout.button_layout, null);
        mainLayout.addView(v);
    }
}

那么根据结论3,在button_layout定义的外层布局属性将不会生效,那么问题来了,这个按钮将如何显示呢?带着这个问题我们点开addView方法:

public void addView(View child) {
    addView(child, -1);
}

public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

这里先判断一下child.getLayoutParams是否为空(肯定是空的,因为在inflate方法中没给他创建布局参数),为空则调用generateDefaultLayoutParams()方法创建默认的布局参数:

protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

其宽高都是WRAP_CONTENT,但是实际上,ViewGroup有很多子类,不同的布局会有不同的设置方法,它们都重写了generateDefaultLayoutParams()这个方法,来看一看LinearLayout的generateDefaultLayoutParams方法:

@Override
    protected LayoutParams generateDefaultLayoutParams() {
        if (mOrientation == HORIZONTAL) {
            return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        } else if (mOrientation == VERTICAL) {
            return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        }
        return null;
    }

可以看到,同样是LinearLayout,设为水平和竖直创建的布局参数都有所不同,让我们验证一下:
这个是将LinearLayout设为水平:
在这里插入图片描述
设为竖直:
在这里插入图片描述

本文参考自:

  1. 获取View实例——LayoutInflater
  2. Android LayoutInflater原理分析,带你一步步深入了解View(一)

猜你喜欢

转载自blog.csdn.net/weixin_44965650/article/details/106769540