ViewGroup是如何加载布局中的view?

本文从分析问题、查找资料到解决问题的过程,探讨了在Android中,ViewGroup如何加载布局文件中的子View。问题起源于 CoordinatorLayout 中 LayoutParams 的设置,通过研究源码,发现关键在于 `setContentView` 方法,它触发了 LayoutInflater 加载布局。LayoutInflater 不仅创建根视图(rootView),还递归地创建子视图并添加到 ViewGroup 中,同时生成相应的 LayoutParams。重写 ViewGroup 时,需要关注 generateLayoutParams 方法以及相关方法的复写。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LayoutInflater如何加载布局?

上面链接中的内容是此篇文章的基础,如果对LayoutInflater原理不熟悉可以去参考一下。

1.问题由来

本来在看CoordinatorLayout的源码,然后发现它内部定义了一个LayoutParams。那么问题就来了,在布局文件中的childView是没有置顶LayoutParams的,那么只有在childView创建的时候,由CoordinatorLayout来指定了。那CoordinatorLayout是在什么时候调用的childView的构造函数的?
在查看ViewGroup源码时发现了一个mChildren属性,类型是View[],显然就是用来保存子View的。
那么问题就出来了:ViewGroup中的mChildren是啥时候初始化的?

2.分析问题

a. 一开始分析的时候真是不知如何下手,首先从ViewGroup源码中查找相关的方法,倒是发现了一个方法addViewInner()在该方法内部,对mChildren进行了赋值和扩容等操作,但是往上跟就发现最后跟到了addView()方法,这是个公共方法,显然是给外部调用的。于是,线索就此中断!
b. 接着我就去网上查stactOverFlow,google一顿查询,无果。
c. 我想着老罗(罗升阳)不是分析Android分析得很深入嘛,去他blog找,找到一篇搭上点边的Android应用程序窗口和View的创建过程 。但人家重点是将View创建的整个流程,并没有讲child是如何添加进来的。

就在我黔驴技穷之际,在老罗的那篇blog中看到了在View创建流程中有一步是setContentView,于是就想在代码中找这个方法的具体代码,没想到这条路还真是走对了,在此谢谢老罗!!

3.解决问题

既然是从setContentView开始的,那就先看它的源码吧。这个方法的真正实现在PhoneWindow.java文件中。

@Override
    public void setContentView(int layoutResID) {
        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 {
            //加载布局到ContentView中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

其实就是LayoutInflater如何加载布局文章中讲的内容,只不过这里不是把layout中的控件添加到ViewGroup中,而是将整个layout文件的内容添加到contentView中。
不明白ContentView是什么的可以参考下图
这里写图片描述
(图片来源于网上)
只要看最左边的框图就行了,我们定义的layout文件就是加载在图中的ContentViews中。

上面讲到将layout文件加载到ContentView中,那么会不会将内部的子View也一起添加进来呢?
这个就是问题的关键了,马上去看了一下LayoutInflater如何加载布局?终于明白为什么明明分析过LayoutInflater源码却还是不知道childView是如何添加的了,因为我直接把加载childView的部分给略过了。
上文只分析到把rootView加载进来,就戛然而止了。
大致过程就是,先使用XmlParser从layout文件中将rootView读取出来(所谓rootView就是我们在布局文件中写在最前面的RelativeLayout或LinearLayout),然后根据rootView的名称获取到这个控件的包名,再通过反射方式来创建此控件。
创建完rootView之后,会继续调用rInflateChildren方法来创建children对象,这就是childView初始化的地方了!
我们来看源码,rInflateChildren方法内部直接调用rInflate方法

    void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;
        //遍历layout文件中的所有节点
        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)) {
                parseRequestFocus(parser, parent);
            } 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 {
                //关键代码在这里!!!
                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);
                viewGroup.addView(view, params);
            }
        }

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

这个方法就是遍历所有子节点,创建对应的View对象,并调用ViewGroup的generateLayoutParams方法生成对应的LayoutParams,然后将新对象通过ViewGroup.addView方法添加到ViewGroup中。因为可能有多层嵌套,所以会继续调用rInflateChildren方法。
到这里,问题的答案就出来了,在setContentView的时候就将childView添加到ViewGroup中了,并且会生成对应的LayoutParams,所以如果你重写了ViewGroup并且自己定义了LayoutParams,就至少需要重写ViewGroup的generateLayoutParams方法才行。
为什么是至少呢?
因为类似的方法有四个,其实是都需要复写的。

    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs)

    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) 

    protected ViewGroup.LayoutParams generateDefaultLayoutParams() 

    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值