问题
我们在代码中向一个 ViewGroup 动态添加一个 layout 时,经常会写如下代码:
View view = LayoutInflater.from(this).inflate(R.layout.test_layout, null);
ViewGroup rootView = findViewById(R.id.root_view);
rootView.addView(view);
注意第一句的 inflate 方法实际上有 2 个重载方法:
View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
当我们将第二个参数传 null
时,Android Studio 会提示我们:
意思就是当我们传
null
时,所有的 layout_ 参数都会被忽略。这个背后的意思是什么呢?哪些会被忽略呢?
分析
Inflate
跟踪源码最后我们到 LayoutInflater
的 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
方法中
final AttributeSet attrs = Xml.asAttributeSet(parser);
...
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
...
// 当 xml 的根标签是 时
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// rInflate 应该是 recurseInflate 的意思,表示这是一个递归的过程
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// 当 xml 的根标签不是 时,把根 view 实例化出来,解析了 xml 属性中非 layout_ 前缀的属性
// Temp is the root view that was found in the
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) { // 如果我们传了 root 进来
// 这里会解析 layout_ 为前缀的属性,并生成 LayoutParams
// 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);
}
...
}
// 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;
}
}
1. 解析根标签
通过 parser.next()
取出根标签,并且将 parser 指向二级标签。
2. 当根标签是
时
此时 inflate()
调用一定要有 root
。接着就调用 rInflate(parser, root, inflaterContext, attrs, false);
。根据 1 中分析,此时 parser
已经指向二级标签,因此 rInflate()
解析的是二级标签。因此代码逻辑直接跳过了根标签
中的所有属性(layout_
,view
自身属性等)。
根据上面的源码我们可以看到,AttributeSet
里保存了这个 xml 中的所有属性,包括根布局 tag 中的 layout 属性(layout_width
, layout_margin
等)和根 view 自身的属性(LinearLayout
就是 orientation
等)。
3. 当根标签是具体某个 view
时
代码的逻辑主要通过调用 createViewFromTag
来生成 xml 的根 view 的实例。生成根 view 实例只是通过反射调用了 view 的构造方法,并没有设置 view 的 LayoutParameters
。
View 的构造方法中,通过解析 AttributeSet
设置了自己的属性。比如 LinearLayout
的构造方法就从 AttributeSet
中解析了 orientation, gravity, weightSum, showDividers, dividerPadding 等属性。但是并没有带 layout_ 前缀的 xml 属性。
接下来,如果函数传进了 root,则再调用 generateLayoutParams
从 AttributeSet
中生成 LayoutParams
。如果 attachToRoot == true
,则还会调用 addView
方法将自己加进 ViewTree
中。
如果在
inflate
方法中不传rootView
,则不会解析根节点的带 layout_ 前缀的属性。如果设置了,则会解析带 layout_ 前缀的属性,并将结果赋值给根节点 view 的LayoutParams
。
addView
那么在上一节中 inflate
时没有传 root
进去,那么生成的 view 就没有 LayoutParams
。那么在 inflate
完之后,开发者调用 addView
加入到父 view 时是怎样的 layoutParams 呢?
跟进 addView
源码,
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
如果 params
为空,则会调用该 ViewGroup 的 generateDefaultLayoutParams()
生成一个。不同的 ViewGroup 实现了自己的逻辑。比如 FrameLayout
就是 match_parent
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
LinearLayout
则是看 orientation 分两种情况
@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;
}
所以如果通过调用 View inflate(@LayoutRes int resource, @Nullable ViewGroup root = null, boolean attachToRoot = false)
生成的 view 是没有 layoutParams 的,开发者再通过调用 addView
来加入父布局时,view 的 layoutParams 是通过父布局的 generateDefaultLayoutParams()
来生成的,不同的父布局会生成不同的 layoutParams。这就是为什么有时有 wrap_content 有时是 match_parent。