Android LayoutInflater 深层解析

一般学过一段时间的 Android 的开发者都知道 LayoutInflater 可以用来动态把 layout 布局文件生成 View。下面我结合实例与源码一步一步分析 LayoutInflater 的使用方法和内部原理,首先从如何获得 LayoutInflater 的实例开始。

获得 LayoutInflater 实例

我们没法通过构造函数实例化 LayoutInflater 对象,因为构造函数是 protected 的,一般通过下面两种方法得到一个标准的 LayoutInflater 实例。

第一种方式

1
2
//android.app.Activity#getLayoutInflater()
LayoutInflater layoutInflater = mActivity.getLayoutInflater();

第二种方式

1
2
LayoutInflater inflater = (LayoutInflater)context.getSystemService
(Context.LAYOUT_INFLATER_SERVICE);

LayoutInflater 有个静态方法对第二种方式做了一下封装,也是我经常使用的方式,源码如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* Obtains the LayoutInflater from the given context.
*/
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 加载布局文件

得到 LayoutInflater 实例后就可以调用 inflate 方法加载布局文件了,一般使用其提供的下面两个方法:

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
/**
* Inflate a new view hierarchy from the specified xml resource. Throws
* {@link InflateException} if there is an error.
*
* @param resource ID for an XML layout resource to load (e.g.,
* <code>R.layout.main_page</code>)
* @param root Optional view to be the parent of the generated hierarchy.
* @return The root View of the inflated hierarchy. If root was supplied,
* this is the root View; otherwise it is the root of the inflated
* XML file.
*/
public View inflate(int resource, ViewGroup root) {
return inflate(resource, root, root != null);
}
/**
* Inflate a new view hierarchy from the specified xml resource. Throws
* {@link InflateException} if there is an error.
*
* @param resource ID for an XML layout resource to load (e.g.,
* <code>R.layout.main_page</code>)
* @param root Optional view to be the parent of the generated hierarchy (if
* <em>attachToRoot</em> is true), or else simply an object that
* provides a set of LayoutParams values for root of the returned
* hierarchy (if <em>attachToRoot</em> is false.)
* @param attachToRoot Whether the inflated hierarchy should be attached to
* the root parameter? If false, root is only used to create the
* correct subclass of LayoutParams for the root view in the XML.
* @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(int resource, ViewGroup root, boolean attachToRoot) {
if (DEBUG) System.out.println("INFLATING from resource: " + resource);
XmlResourceParser parser = getContext().getResources().getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}

可以看出其实第一个方法只是第二个方法的简化版,其实是一样的,我写了一个 demo 测试一下:

text.xml :

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="600px"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:padding="5dp"
android:textSize="16sp"
android:background="@drawable/rectangle_stroke_bankground">
</TextView>

代码 :

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
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FrameLayout frameLayout1 = (FrameLayout) findViewById(R.id.container1);
FrameLayout frameLayout2 = (FrameLayout) findViewById(R.id.container2);
FrameLayout frameLayout3 = (FrameLayout) findViewById(R.id.container3);
LayoutInflater layoutInflater = LayoutInflater.from(this);
layoutInflater.inflate(R.layout.text, frameLayout1, true);// here return frameLayout1, not the root of text layout.
TextView textView1 = (TextView) frameLayout1.findViewById(android.R.id.text1);
textView1.setText("The layout width of textView1 is " + getViewLayoutWidth(textView1));
TextView textView2 = (TextView) layoutInflater.inflate(R.layout.text, frameLayout2, false);
frameLayout2.addView(textView2);
textView2.setText("The layout width of textView2 is " + getViewLayoutWidth(textView2));
TextView textView3 = (TextView) layoutInflater.inflate(R.layout.text, null);
frameLayout3.addView(textView3);
textView3.setText("The layout width of textView3 is " + getViewLayoutWidth(textView3));
}
private String getViewLayoutWidth(View view) {
if (null == view || null == view.getLayoutParams()) {
return "";
}
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT) {
return "MATCH_PARENT";
} else if (layoutParams.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
return "WRAP_CONTENT";
} else {
return String.valueOf(layoutParams.width);
}
}

运行结果如下:

根据方法的说明和测试结果可以知道:

(1) 在指定了不为 null 的 root 参数时,若 attachToRoot 参数为 true,root 会作为已加载布局的 parent,而且方法返回的也是 root 而不是已加载布局的根视图; 若 attachToRoot 参数为 false,root 参数只是用来给已加载布局的根视图创建相应的 LayoutParams,方法返回的还是已加载布局的根视图。

(2) 在 root 参数为 null 时,已加载布局的根视图的 layout 属性会失效,如上面的 demo 中 textView3 的宽度不是 600px 而是 match_parent。相信有些开发也会遇到同样的问题,怎么根视图的 layout 属性无效了呢,而其他的非 layout 属性(如 padding)还是生效,看起来有点莫名其秒,而且非根视图的 layout 属性还是有效的。

现在带着疑问看看 LayoutInflater 在 inflate 方法中的return inflate(parser, root, attachToRoot)中具体是怎么加载布局文件的。

inflate 方法源码解析

因为 inflate(parser, root, attachToRoot) 的过程比较复杂,所以我直接在代码中加入了解析说明,在一行的开头出现中文注释的就是我加的说明,源码如下:

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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// LayoutInflater 使用了 Android 官方推荐的 Pull 解析方式来解析布局文件
// 整个解析过程是:
// 先解析根视图,然后调用 rInflate 递归地查找这个视图下的所有子试图,每次递归完成把这个视图添加到父视图中
//
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context)mConstructorArgs[0];
mConstructorArgs[0] = mContext;
// 这里把返回的结果 result 指向 root
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();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
// 从下面这个 if 语句可以知道加载 merge 为根试图时,必须指定不为 null 的 root,而且 attachToRoot 为 true
// 使用 merge 标签时,是直接使用 root 作为 merge 下面视图的 parent view 的。
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, attrs, false);
} else {
// Temp is the root view that was found in the xml
View temp;
// TAG_1995 为 "blink",这是 Android 中一个彩蛋,我稍后会说明
if (TAG_1995.equals(name)) {
temp = new BlinkLayout(mContext, attrs);
} else {
// 根据节点的名字与属性创建 View,createViewFromTag 里会调用 createView 方法,然后利用反射实例化 view 对象
temp = createViewFromTag(root, name, attrs);
}
ViewGroup.LayoutParams params = null;
// 下面这个 if 语句就是处理根视图的 layout 属性的
// 1. root != null 时,才会调用 root.generateLayoutParams(attrs) 把布局文件中layout属性读取出来
// 如果 attachToRoot 为 false,会通过 setLayoutParams 设置根视图的 layout 属性
// 如果 attachToRoot 为true,会在后面的代码中调用 root.addView(temp, params),其实 addView 里面就有通过 setLayoutParams 设置根视图的 layout 属性
// 2. root == null 时,不会去读取布局文件中的 layout 属性,所以 layout 属性无效了,而其他属性还是生效的
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.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// 递归查找这个视图下所有子视图
// Inflate all children under temp
rInflate(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);
}
// 注意一开始 result 的值是 root,result 为 inflate 方法最终返回的值
// 如果 root 为 null,或者 attachToRoot 为 false,result 的值改为根视图
// 如果 root 不为null,且 attachToRoot 为 true,root 在上面的语句中已经添加根视图作为 child了,此时 result 不变还是 root
// 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) {
InflateException ex = new InflateException(e.getMessage());
ex.initCause(e);
throw ex;
} catch (IOException e) {
InflateException ex = new InflateException(
parser.getPositionDescription()
+ ": " + e.getMessage());
ex.initCause(e);
throw ex;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return result;
}
}
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
/**
* Recursive method used to descend down the xml hierarchy and instantiate
* views, instantiate their children, and then call onFinishInflate().
*/
void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
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_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else if (TAG_1995.equals(name)) {
// XML 中 blink 标签生成 BlinkLayout
final View view = new BlinkLayout(mContext, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs, true);
viewGroup.addView(view, params);
} else {
// 根据标签生成 View,并读取 layout 属性,然后添加到父视图中
final View view = createViewFromTag(parent, name, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (finishInflate) parent.onFinishInflate();
}

经过上面的源码解析后,大家已经对 LayoutInflater 加载布局文件的过程已经大致了解了,但是可能还是不清楚为什么 root 参数为 null 时,就不去读取布局文件中的 layout 属性呢。下面我讲述一下个人的看法,大家有什么看法欢迎在文章下面评论:

首先大家要清楚 LayoutParams 的定义,LayoutParams 是视图用来告诉它们的父视图它们想在父视图中如何被布局的,注意其实 LayoutParams 真正是给父视图如何排列布局它的子视图的,也就是每个视图的 LayoutParams 其实是跟父视图有很大关系的。LayoutParams 在 Android的 容器视图中都有不同的子类,LinearLayout 下有 LinearLayout.LayoutParams,继承自 ViewGroup.MarginLayoutParams,还新增了 layout_weight 和 layout_gravity 两个 layout 属性。RelativeLayout.Params 在 ViewGroup.MarginLayoutParams 基础上加了 layout_toLeftOf、layout_toRightOf、layout_alignParentTop 等属性,FrameLayout.LayoutParams 在 ViewGroup.MarginLayoutParams 基础上加了 layout_gravity 属性。A 容器视图的 layout 属性无法被 B 容器视图解析,layout 属性只有在确定其容器视图的情况下才是有意义的,所以在 inflate 方法中,root 为 null 时,不知道根视图将来会添加到什么容器,所以也不知道生成什么 LayoutParams,可能有人会说 layout_width 和 layout_height 两个 layout 属性是所有 LayoutParams 共有的,为什么这两个属性也不读取呢。因为 Android 中的容器视图在 addview 的时候会调用 checkLayoutParams 检查 LayoutParams 与自己的 LayoutParams 是否相符,如果不符会调用 generateLayoutParams 生成默认的布局属性。例如根视图设置 FrameLayout.LayoutParams,但是之后被添加到 LinearLayout 中,checkLayoutParams 为 false,然后还是用的 LinearLayout 默认的布局属性。

至于如何解决布局文件中的属性失效的问题,可以调用 inflate(resourceId, parent, false) 方法,相信有人在使用 ListView 的 Adapter 过程中,发现 getView 方法中加载的布局文件的 margin 属性失效了,这时就可以用上面的方法解决了。如果没法确定 parent 视图,那就只能在外面再包一层 FrameLayout 了,当然个人不推荐这么做。

在上面解析源码的过程发现有个 blink 标签,大家可能基本没听过,平常也没有用过。其实这是 Android 的一个很有意思的东西,可以让它的 child view 闪烁。下面我们研究下源码,我分别截取关键部分:

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
private static final String TAG_1995 = "blink";
if (TAG_1995.equals(name)) {
temp = new BlinkLayout(mContext, attrs);
}
// BlinkLayout 是 LayoutInflater 的静态内部类
private static class BlinkLayout extends FrameLayout {
private static final int MESSAGE_BLINK = 0x42;
private static final int BLINK_DELAY = 500;
private boolean mBlink;
private boolean mBlinkState;
private final Handler mHandler;
public BlinkLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == MESSAGE_BLINK) {
if (mBlink) {
// 收到 MESSAGE_BLINK 消息后,修改 mBlinkState,并且刷新一下
mBlinkState = !mBlinkState;
makeBlink();
}
invalidate();
return true;
}
return false;
}
});
}
// 隔 500ms 发送 MESSAGE_BLINK 消息
private void makeBlink() {
Message message = mHandler.obtainMessage(MESSAGE_BLINK);
mHandler.sendMessageDelayed(message, BLINK_DELAY);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mBlink = true;
mBlinkState = true;
makeBlink();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mBlink = false;
mBlinkState = true;
mHandler.removeMessages(MESSAGE_BLINK);
}
@Override
protected void dispatchDraw(Canvas canvas) {
// mBlinkState 为 true 时,正常地 draw the child views.
// mBlinkState 为 false 时,不做任何事情,也就是 not draw the child views.
if (mBlinkState) {
super.dispatchDraw(canvas);
}
}
}

根据上面源码可以知道我们可以在 xml 中添加 作为一个视图,还可以在里面添加子视图,在 blink 标签内的视图,每隔 500ms 就会不显示,所以在界面上看就是一直闪烁了。这个东西个人感觉是 Android 中的一个彩蛋,还是挺有意思的,大家有空可以尝试一下。

关于 LayoutInflater 的原理我就讲到这里,大家有什么意见或问题欢迎在文章下面评论^_^

END
Johnny Shieh wechat
我的公众号,不只有技术,还有咖啡和彩蛋!