LayoutInflater中inflate源码分析

对项目中的代码进行优化,删除Layout中过度绘制的代码,无意中发现了一个问题,ListView中的item的高度设置后没有效果,listview算是Android开发中最常用的控件之一,一直都在用,但是却没有遇到过item中设置无效,找到解决办法后,意识到自己对平时调用的方法参数不够理解。

问题重现

原来的的xml布局

1
2
3
4
5
6
7
8
9
10
11
12
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="48dp">

<ImageView
android:id="@+id/iv_title"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerVertical="true"/>

</RelativeLayout>

不管怎样修改RelativeLayout中height中的值,运行后的item高度,一直保持不变,为32dp。

解决办法

  1. 通过内层控件去控制去控制整个item的高度,修改如下

    1
    2
    3
    4
    5
    6
    7
    <ImageView
    android:id="@+id/iv_title"
    android:layout_width="32dp"
    android:layout_height="32dp"
    android:layout_marginTop="8dp"
    android:layout_marginBottom="8dp"
    android:layout_centerVertical="true"/>
  2. 为RelativeLayout设置最小高度,添加属性

    1
    android:minHeight="48dp"
  3. 虽然可以解决该问题,但是总觉得指标不治本,如果遇到xml中嵌套多个控件,那么这种上面两种解法就难以实施了。再继续查找解决办法,原来Adapter中用的是

    1
    LayoutInflater.from(mAct).inflate(layoutId,null);

    改为如下则可以解决

    1
    LayoutInflater.from(mAct).inflate(layoutId,parent,false);

    为啥参数不同就导致结果不同呢?看下面的详细分析

LayoutInflater.inflate()的分析

简介

Instantiates a layout XML file into its corresponding {@link android.view.View}
* objects.

实例化一个XML布局文件到相应的View对象中

使用

1
LayoutInflater layoutInflater = LayoutInflater.from(mContext);

It is never used directly. Instead, use
* {@link android.app.Activity#getLayoutInflater()} or
* {@link Context#getSystemService} to retrieve a standard LayoutInflater instance

从注释可以看出,不直接创建对象,而是通过去取回一个实例,看from(mContext)的实现可以发现最终也是通过getSystemService获取

1
2
3
4
5
6
7
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}

inflate()源码分析

inflate()有四个重载的方法,第一二个方法把resource解析为XmlResourceParser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}

final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}

最后都调用了第四个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

final Context inflaterContext = mContext;
//把pareser转化为attributeSet
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
//把父布局赋予result
View result = root;

try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}

if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}

//获取根节点的名称
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
// 获取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)
// 设置xml的根布局属性
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) {
//把xml的根布局附上到root中
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;
}
}

}
···

return result;
}
}

从上面的代码可以看出,先判断是否为merge标签,如是的话,root不能为空或者attachToRoot不能为false,否则会抛出异常。

不为merge标签,获取xml中的根布局,如果root不为空,则生成xml中的属性params,如果attachToRoot为false,则设置到xml的根布局中,此时属性值有效。

如果attachToRoot为true,则把xml中的根布局以及属性附到root中,此时属性值同样有效。

1
2
3
if (root == null || !attachToRoot) {
result = temp;
}

最后,返回view,只有root不为空且attachToRoot为true,才返回root,否则都返回xml的根布局。

可以知道,listView中的getView是返回子item,为了让属性值有效,应该改为

1
convertView = LayoutInflater.from(mContext).inflate(R.layout.item_listtest, parent,false);

如果最后的attachToRoot参数为true,那么会发生崩溃,日志如下,因为getView拿到item后,还再执行addView。

1
Caused by: java.lang.UnsupportedOperationException: addView(View, LayoutParams) is not supported in AdapterView

如果想在一个界面中,想让rootView中添加一个View,让其属性有效,那么则改为

1
convertView = LayoutInflater.from(mContext).inflate(R.layout.item_listtest, parent, true);

总结

返回布局

  • 如果root不为null,attachToRoot为true,xml被addView到root中,返回root布局
  • 如果root为null,或者attachToRoot为false,返回xml布局

属性值

  • 如果root为null,属性值无效,因为属性值需要root布局去root.generateLayoutParams(),其他情况,属性值有效

公众号:亦袁非猿

欢迎关注,交流学习