android View 详解


打算,好好总结下View相关的一些知识,包括机制和源码解析。至于View的一些属性解析,可以参考我的另一篇笔记Alt text

从LayoutInflater说起

LayoutInflater就是用于加载布局,首先我们从它的用法说起。

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

当然还有一种简写发法(Android 对上面的方法进行了封装)

1
LayoutInflater inflater = LayoutInflater.from(context);

进行封装的源码如下:

1
2
3
4
5
6
7
8
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;
}

得到layoutInflater实例后,我们就可以调用它的inflate()方法来加载布局。

1
inflater.inflate(resourceId, root);

LayoutInflater加载布局示例

这个呢,典型的,大家可以看一下经常写的BaseAdapter的getView方法里面LayoutInflater的使用。这里也是以此来进行解析。
首先,我们可以简单的加载一个Button

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<Button android:id="@+id/button"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="121dp"
android:layout_height="1201dp"
android:text="Button">


</Button>

Java代码:

1
2
3
4
5
6
7
8
9
/*
LayoutInflater加载一个Button
通过LayoutParams来设置button大小
*/

mLayout = (RelativeLayout) findViewById(R.id.mLayout);
LayoutInflater inflater = LayoutInflater.from(this);
Button button = (Button) inflater.inflate(R.layout.item,null);
button.setLayoutParams(new ViewGroup.LayoutParams(360, ViewGroup.LayoutParams.MATCH_PARENT));
mLayout.addView(button);

很明显,最后我们要通过拿到View的parent(ViewGroup),然后通过addView来添加Button到它的父布局。
接下来,我们分析一下inflate()方法的参数列表

首先,我们用以下三种方式调用inflate()方法(以BaseAdapter的getView()为例)。

1
2
3
4
convertView = inflater.inflate(R.layout.item, null);
convertView = inflater.inflate(R.layout.item, parent, false);
//该句会报错,应为最后返回的是item的父布局(ViewGroup)
convertView = inflater.inflate(R.layout.item, parent, false);

对于,以上三种使用方式:

第一种情况下,对于item的宽高设置不会生效;而第二种情况下,item中Button的宽高设置生效了。第三种方式则报错:
Alt text
具体原因在源码分析中解释。

LayoutInflater源码解析

我们可以查看LayoutInflater.java的源码,然后后发现LayoutInflater中重载了两个inflate方法分别是

1
2
3
public View inflate(int resource, ViewGroup root) {
return inflate(resource, root, root != null);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public View inflate(int resource, 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();
}
}

这两个inflate方法呢最终都会调用到下面的XML文件解析方法

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
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
 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
}
//type为XmlPullParser.START_TAG 或者XmlPullParser.END_DOCUMENT
//如果为XmlPullParser.END_DOCUMENT,则XML文件异常,throw
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
//此处type肯定是XmlPullParser.START_TAG,即拿到XML文件的root node
final String name = parser.getName();

if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}

//处理merge标签(XML性能优化)
if (TAG_MERGE.equals(name)) {
//root为null或attachRoot为false则抛出异常
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//递归inflate()方法调用
rInflate(parser, root, attrs, false, false);
} else {
// Temp is the root view that was found in the xml
//temp是root view,根据tag节点创建View对象
final View temp = createViewFromTag(root, name, attrs, false);

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
//根据root生成合适的LayoutParams实例
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
//如果attachToRoot=false就调用view的setLayoutParams方法
temp.setLayoutParams(params);
}
}

if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp
//递归inflate剩下的children
rInflate(parser, temp, attrs, true, 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非空且attachToRoot=true则将xml文件的root view加到形参提供的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) {
//返回xml里解析的root view
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);
//返回参数root或xml文件里的root view
return result;
}
}

很明显,这里是用来pull方式来解析XML文件。

这里,总结下inflate(0方法参数的含义)

  • inflate(xmlId, null); 只创建temp的View,然后直接返回temp。

  • inflate(xmlId, parent); 创建temp的View,然后执行root.addView(temp, params);最后返回root。

  • inflate(xmlId, parent, false); 创建temp的View,然后执行temp.setLayoutParams(params);然后再返回temp。

  • inflate(xmlId, parent, true); 创建temp的View,然后执行root.addView(temp, params);最后返回root。

  • inflate(xmlId, null, false); 只创建temp的View,然后直接返回temp。

  • inflate(xmlId, null, true); 只创建temp的View,然后直接返回temp。

到此其实已经可以说明我们上面示例部分执行效果差异的原因了(在此先强调一个Android的概念,下一篇文章
我们会对这段话作一解释:我们经常使用View的layout_width和layout_height来设置View的大小,而且
一般都可以正常工作,所以有人时常认为这两个属性就是设置View的真实大小一样;然而实际上这些属性是用
于设置View在ViewGroup布局中的大小的;这就是为什么Google的工程师在变量命名上将这种属性叫
作layout_width和layout_height,而不是width和height的原因了。),如下:

  • nmInflater.inflate(R.layout.textview_layout, null)不能正确处理我们设置的宽和高是因
    为layout_width,layout_height是相对了父级设置的,而此temp的getLayoutParams为null。

  • mInflater.inflate(R.layout.textview_layout, parent)能正确显示我们设置的宽高是因为我们
    的View在设置setLayoutParams时params = root.generateLayoutParams(attrs)不为空。

    Inflate(resId , parent,false ) 可以正确处理,因为temp.setLayoutParams(params);这
    个params正是root.generateLayoutParams(attrs);得到的。

  • mInflater.inflate(R.layout.textview_layout, null, true)与mInflater.inflate(R.layout.textview_layout, null, false)不能正确处理我们设置的宽和高是
    因为layout_width,layout_height是相对了父级设置的,而此temp的getLayoutParams为null。

  • textview_layout_parent.xml作为item可以正确显示的原因是因为TextView具备上级ViewGroup,上
    级ViewGroup的layout_width,layout_height会失效,当前的TextView会有效而已。

上面例子中说放开那些注释运行会报错java.lang.UnsupportedOperationException:

addView(View, LayoutParams) is not supported是因为AdapterView源码中调用
了root.addView(temp, params);而此时的root是我们的ListView,ListView为AdapterView的子类
,所以我们看下AdapterView抽象类中addView源码即可明白为啥了,如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void addView(View child) {
throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
}

接下来看一下inflate方法中被调运的rInflate方法

rInflate方法解析

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
  void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
boolean finishInflate, boolean inheritContext) throws XmlPullParserException,
IOException {

final int depth = parser.getDepth();
int type;
//XmlPullParser解析器的标准解析模式
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

//找到START_TAG节点程序才继续执行这个判断语句之后的逻辑
if (type != XmlPullParser.START_TAG) {
continue;
}

//获取Name标记
final String name = parser.getName();
//处理REQUEST_FOCUS的标记
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
//处理tag标记
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
//处理include标记
//include节点如果是根节点就抛异常
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, parent, attrs, inheritContext);
} else if (TAG_MERGE.equals(name)) {
/merge节点必须是xml文件里的根节点(这里不该再出现merge节点)
throw new InflateException("<merge /> must be the root element");
} else {
//其他自定义节点
final View view = createViewFromTag(parent, name, attrs, inheritContext);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs, true, true);
//ViewGroup添加View
viewGroup.addView(view, params);
}
}
//parent的所有子节点都inflate完毕的时候回onFinishInflate方法
if (finishInflate) parent.onFinishInflate();
}

至此,LayoutInflater的源码解析结束!

从ViewGroup和View的角度解析

ViewGroup的onMeasure方法所做的是:

为childView设置测量模式和测量出来的值。

如何设置呢?就是根据LayoutParams

  • 如果childView的宽为:LayoutParams. MATCH_PARENT,则设置模式为MeasureSpec.EXACTLY,且为childView计算宽度。

  • 如果childView的宽为:固定值(即大于0),则设置模式为MeasureSpec.EXACTLY,且将lp.width直接作为childView的宽度。

  • 如果childView的宽为:LayoutParams. WRAP_CONTENT,则设置模式为:MeasureSpec.AT_MOST
    高度与宽度类似。

View的onMeasure方法

主要做的就是根据ViewGroup传入的测量模式和测量值,计算自己应该的宽和高:

一般是这样的流程:

  • 如果宽的模式是AT_MOST:则自己计算宽的值。
  • 如果宽的模式是EXACTLY:则直接使用MeasureSpec.getSize(widthMeasureSpec);

接下来,才进入真正的主角,View的绘制过程。

View绘制过程

因为内容过多,所以写在下一篇博客。