上面链接中的内容是此篇文章的基础,如果对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)