# 1、ViewRoot 和 DecorView

  1. ViewRoot 对应 ViewRootImpl 类,是连接 WindowManager 和 DecorView 的纽带。View 的三大流程是通过 ViewRoot 完成的。 在 ActivityThread 中,当 Activity 对象被创建完毕时,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl,且 ViewRootImpl 和 DecorView 会建立关联。如下代码,WindowManagerGlobal 的 addView () 方法:
1
2
3
4
5
6
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
...
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
...
}
  1. View 绘制流程从 performTraversals 开始,经过 Measure、layout、draw。流程图如下

在这里插入图片描述

\3. DecorView 是顶级 View,是一个 FrameLayout,上面是标题栏、下面是内容栏。内容栏就是 setContengView 的内容 view,id 是 content。事件 经过 DecorView 然后传给我们自己的 View。

# 2、 MeasureSpec

MeasureSpec 封装了从父级传递到子级的布局要求。系统把 view 的 LayoutParams 根据 父容器施加的规则(父容器的 SpecMode) 转换成 view 的 MeasureSpec,然后使用这个 MeasureSpec 确定 view 的测量宽高(不一定是最终宽高)。

# 2.1MeasureSpec

1.MeasureSpec—view 的测量规格:高 2 位的 SpecMode,低 30 位的 SpecSize。 2.SpecMode 的分类: UNPECIFIED,父容器对 view 不限制,要多大给多大,一般系统内部使用。 EXACTLY,父容器检测出 view 所需大小,view 最终大小就是 SpecSize 的值。对应 LayoutParams 中的 matchParent、具体数值 两种模式。 AT_MOST,父容器制定了可用大小即 SpecSize,view 的大小不能大于这个值,具体要看 view 的具体实现。对应 LayoutParams 中的 wrap_content。

# 2.2MeasureSpec 和 LayoutParams 的对应关系

前面说了 View 的 MeasureSpec 是由 LayoutParams 和父容器的 MeasureSpec 共同决定。顶级 view,即 DecorView,是由窗口尺寸和自身 LayoutParams 决定

1、DecorView,ViewRootImpl 中 measureHierarchy () 方法(performTraversals 中执行),代码如下,desiredWindowWidth、desiredWindowHeight 是屏幕的尺寸。

1
2
3
4
5
6
7
8
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
...
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}

performMeasure () 内部是调用 mView.measure (childWidthMeasureSpec, childHeightMeasureSpec),mView 就是 DecorVIew。继续看 getRootMeasureSpec()方法如下:

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
/**
* Figures out the measure spec for the root view in a window based on it's
* layout params.
*
* @param windowSize
* The available width or height of the window
*
* @param rootDimension
* The layout params for one dimension (width or height) of the
* window.
*
* @return The measure spec to use to measure the root view.
*/
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {

case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}

DecorView 的 MeasureSpec 就明确了,根据其 LayoutParams:

  • MATCH_PARENT:精确模式,就是窗口大小;
  • WRAP_CONTENT:最大值模式,最大值不能超过窗口大小;
  • 固定值(如 100dp):精确模式,就是 LayoutParams 的指定值。

2、普通 View,测量过程从 ViewGroup 传递下来,看 ViewGroup 的 measureChildWithMargins () 方法:

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
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

即先获取 child 的 MeasureSpec,再调 child.measure ()。可以看到,child 的 MeasureSpec 是由父容器的MeasureSpec、父容器的 padding、child 的 LayoutParams、child 的 marging 共同决定。继续看 getChildMeasureSpec () 方法:

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
/**
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
*
* The goal is to combine information from our MeasureSpec with the
* LayoutParams of the child to get the best possible results. For example,
* if the this view knows its size (because its MeasureSpec has a mode of
* EXACTLY), and the child has indicated in its LayoutParams that it wants
* to be the same size as the parent, the parent should ask the child to
* layout given an exact size.
*
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* @param childDimension How big the child wants to be in the current
* dimension
* @return a MeasureSpec integer for the child
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

//padding,就是已被占用的空间,就是 父容器的padding+child的marging
//size,是ViewGroup本身size减去已使用的空间,是ViewGroup能提供给child的最大值。
int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

可见,view 的 MeasureSpec 由 viewParent 的 MeasureSpec 和自身 layoutParams 确定。另外,child 的可利用的尺寸是 parent 尺寸减去 padding,上面代码已有注释,这很好理解。 梳理如下:

parentSpecMode /childLayoutParams EXACTLY AT_MOST UNSPECIFIED
dp/px EXACTLY childSize EXACTLY childsize EXACTLY childsize
match_parent EXACTLY parentSize AT_MOST parentSize UNSPECIFIED 0
wrap_content AT_MOST parentSize AT_MOST parentSize UNSPECIFIED 0

注意,parentSize 是父容器可使用的大小。

更新,看到鸿洋公众号的文章关于 UNSPECIFIED 说明:

MeasureSpec.UNSPECIFIED 是不是真的不常见?

在日常定制 View 时,确实很少会专门针对这个模式去做特殊处理,大多数情况下,都会把它当成 MeasureSpec.AT_MOST 一样看待,就比如最最常用的 TextView,它在测量时也是不会区分 UNSPECIFIED 和 AT_MOST 的。

不过,虽说这个模式比较少直接接触到,但很多场景下,我们已经在不知不觉中用上了,比如 RecyclerView 的 Item,如果 Item 的宽 / 高是 wrap_content 且列表可滚动的话,那么 Item 的宽 / 高的测量模式就会是 UNSPECIFIED。 还有就是 NestedScrollView ScrollView,因为它们都是扩展自 FrameLayout,所以它们的子 View 会测量两次,第一次测量时,子 View 的 heightMeasureSpec 的模式是写死为 UNSPECIFIED 的。 我们在自定义 ViewGroup 过程中,如果允许子 View 的尺寸比 ViewGroup 大的话,在测量子 View 时就可以把 Mode 指定为 UNSPECIFIED。

看到 ScrollView 重写了 measureChild 方法,指定高度的 mode 是 UNSPECIFIED

![ScrollView 重写了 measureChild 方法,指定高度的 mode 是 UNSPECIFIED](data:image/svg+xml;utf8,)

# 3、View 的工作流程

View 的三大流程,measure、layout、draw。measure 确定 view 的测量宽高,layout 确定 view 的最终宽高和四个顶点位置,draw 绘制到屏幕。

# 3.1 Measure 过程

view 的测量过程,由 measure () 方法完成。viewGroup 测量自身后,还需调用 child.measure () 遍历测量子 view。

# 3.1.1 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
/**
* <p>
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
* </p>
*
* <p>
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
* </p>
*
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the
* parent
* @param heightMeasureSpec Vertical space requirements as imposed by the
* parent
*
* @see #onMeasure(int, int)
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}

可见 view 的 measure () 方法是 final,不可被子类重写。里面调用 onMeasure (),实际真正的测量过程在 onMeasure () 中。所以只有 onMeasure () 可以且必须被子类重写。另外,参数 widthMeasureSpec、heightMeasureSpec 就是上一节最后的表格中的值。继续看 onMeasure ():

1
2
3
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

从名字就可以看出,setMeasuredDimension () 就是设置测量的尺寸,且在 onMeasure () 中必须被调用,否则在测量时会发送异常。getDefaultSize () 获取默认的宽 / 高。所以 View 类中的 onMeasure () 是设置默认的宽高。 继续看 getDefaultSize () 具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

UNSPECIFIED,一般是系统使用,不需要关心。这里 view 大小直接取 size,就是 getSuggestedMinimumWidth ()/getSuggestedMinimumHeight (),意思是 建议的 最小宽高。看下实现:

1
2
3
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

没有背景,就取 mMinWidth,就是 xml 中设置的 minWidth 属性值;有背景,取 mMinWidth 、背景的 MinimumWidth 的较大值。drawable 的 getMinimumWidth () 如下,有固有宽度就取固有宽度(如 BitmapDrawable),没有就是 0(如 ShadeDrawable)。

1
2
3
4
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

AT_MOST、EXACTLY,直接取 specSize,就是上一节最后的表格中的值,作为测量宽高。那这样取 specSize 是否合适呢? 再来看一遍 specSize 的来源。

parentSpecMode /childLayoutParams EXACTLY AT_MOST UNSPECIFIED
dp/px 1EXACTLY childSize 2EXACTLY childsize EXACTLY childsize
match_parent 3EXACTLY parentSize 4AT_MOST parentSize UNSPECIFIED 0
wrap_content 5AT_MOST parentSize 6AT_MOST parentSize UNSPECIFIED 0

1、2 的情况,具体 dp 值,取 SpecSize 没问题,因为是 EXACTLY,就是给定的的尺寸。 3 的情况,match_parent,取 SpecSize,即 parentSize,也没问题,因为是 EXACTLY,也是确定的尺寸。 4 的情况,match_parent,但父容器又是 wrap_content,系统就给了 AT_MOST+parentSize,限制最大尺寸为 parentSize。而这里直接取 specSize 即 parentSize,似乎也没问题。这个看一个例子一,如下,view 是 match_parent,可见 view 取得确实是 parentSize。

5、6 的情况,wrapContent 即 AT_MOST+parentSize,取 specSize 也就是 parentSize,所以和 3、4 一样都是 parentSize,即 View 类 中 默认 wrapContent 等同于 match_parent。

再看一个情况例子二,如下,View 换成 TextView(继承 View),尺寸就不是 parentSize 了,而是内容尺寸,说明 TextView 在 onMeasure 中做了处理。

继续看,例子三如下,同时有 TextView、View,此时 textView 又是取 parentSize(可用空间):

在这里插入图片描述

所以得出结论: 通常直接继承 View 的自定义 View,在 onMeasure () 需要处理 : a、wrap_content 的情况,否则 wrap_content 就等同于 match_parent; b、match_parent + 父容器 wrap_content 的情况,否则就像例子一,父容器 wrap_content 是无效的,处理方式就是例子二中的 textView。 总结就是,直接继承 View 的自定义 View,需要处理 AT_MOST 时的宽高

处理方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);

if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, mHeight);
}
}

实际就是在 AT_MOST 时 设置一个指定的尺寸 mWidth、mHeight,其他情况沿用系统。至于 mWidth、mHeight 是多少,则要具体看你的 view 的逻辑了。例如 TextView,可以参考其源码的实现。

# 3.1.2 ViewGroup 的测量过程

ViewGroup 需要完成自身的测量,还要遍历子 view 调用 measure () 方法进行测量。

ViewGroup 是抽象类,没有重写 onMeasure,因为无法做到统一,是让具体继承 ViewGroup 的子类重写自己的逻辑。但是提供一些方便的方法给子类调用。如 measureChildren ()、measureChild ()、measureChildWithMargins (),上面第二节分析过 measureChildWithMargins (),这里我们看下 measureChildren ():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Ask all of the children of this view to measure themselves, taking into
* account both the MeasureSpec requirements for this view and its padding.
* We skip children that are in the GONE state The heavy lifting is done in
* getChildMeasureSpec.
*
* @param widthMeasureSpec The width requirements for this view
* @param heightMeasureSpec The height requirements for this view
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

就是遍历子 view,调用 measureChild (),继续看:

1
2
3
4
5
6
7
8
9
10
11
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

通过 getChildMeasureSpec () 获取 child 的 MeasureSpec,然后调用 child.measure (),测量就传到 child 内部了,很好理解。measureChild () 相比 measureChildWithMargins () 没有考虑 child 的 margin 值。

上面说了,ViewGroup 没有重写 onMeasure,因为无法做到统一,让具体继承 ViewGroup 的子类重写自己的逻辑。具体看下 LinearLayout 的测量过程

1
2
3
4
5
6
7
8
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}

继续看 measureVertical ():

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
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
...
//下面这句官方注释:看每个人多高,也记住最大宽度。想想这不就是计算竖向LinearLayout宽高的思路嘛!
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
...
final View child = getVirtualChildAt(i);
...
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
...
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
//这里测量child(里面就是measureChildWithMargins())
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);

final int childHeight = child.getMeasuredHeight();
...
final int totalLength = mTotalLength;
//这里mTotalLength加上child的高度、margin,就是child高度累积。
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
...
//这里记录最大宽度(包含margin)
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
...
}
//遍历完了:高度加上自身的上下padding
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

//这里很重要:调用resolveSizeAndState--决定 计算的高度(高度累加)和 LinearLayout的父容器约束的高度,取哪一个。
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
...
//最大宽度加上左右margin
maxWidth += mPaddingLeft + mPaddingRight;

// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

//设置最终的测量尺寸(宽也也同样调用resolveSizeAndState决定取哪个)
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
}

所以,简单概括就是: 1. 先测量所有 child; 2. 根据 child 的情况获取自身宽高(累加高度、最大宽度)。

那么,是否就取 累加高度、最大宽度?再看下 resolveSizeAndState ():

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
/**
* Utility to reconcile a desired size and state, with constraints imposed
* by a MeasureSpec. Will take the desired size, unless a different size
* is imposed by the constraints. The returned value is a compound integer,
* with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
* optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
* resulting size is smaller than the size the view wants to be.
*
* @param size How big the view wants to be. --想要的尺寸
* @param measureSpec Constraints imposed by the parent. --父布局给的measureSpec
* @param childMeasuredState Size information bit mask for the view's
* children.
* @return Size information bit mask as defined by
* {@link #MEASURED_SIZE_MASK} and
* {@link #MEASURED_STATE_TOO_SMALL}.
*/
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
//AT_MOST时,想要的尺寸大于约束的尺寸,就只能取 约束的尺寸。
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
//dp值、match_parent且父EXACTLY,就是SpecSize
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}

这个过程就是 限制 AT_MOST 时,即 wrap_content(或 match_parent 且父 wrap_content)时高度不能大于 parent 的剩余空间。

# 3.1.3 获取 View 宽高的时机

Measure 过程完成,就可通过 getMeasuredWidth ()、getMeasuredHeight () 获取测量宽高。但某些极端情况 需要多次 Measure 才能确定最终宽高。所以在 onLayout 方法中获取测量宽高是真正 ok 的。 我们知道,activity 的 onCreate 中无法获取到 view 的宽高。实际 onCreate、onStart、onResume 都不能保证 view 已完成测量,所以可能获取的都是 0。因为 view 的 measure 和 activity 生命周期不是同步的。

以下是保证可以获取 view 测量宽高的方法

# 1、Activity/View # onWindowFocusChanged

onWindowFocusChanged:View 已初始化完毕,宽高已准备 ok。 但会多次调用,获取焦点、失去焦点都回调用。(这个回调是 ViewRootIml 中分发到 DecorView,接着到 Activity、到各级 View。)

1
2
3
4
5
6
7
8
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int measuredWidth = scoreView.getMeasuredWidth();
int measuredHeight = scoreView.getMeasuredHeight();
}
}

# 2、view.post(runnable)

view.post 可以把 runnable 放入消息队列,等待 looper 到此 runnable 是 view 已经初始化完成。v 详细原理参考【Android 源码解析】View.post () 到底干了啥

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onStart() {
super.onStart();
scoreView.post(new Runnable() {
@Override
public void run() {
int measuredWidth = scoreView.getMeasuredWidth();
int measuredHeight = scoreView.getMeasuredHeight();
}
});
}

# 3、ViewTreeObserver

ViewTreeObserver 有很多回调,其中有个 OnGlobalLayoutListener,当 View 树的状态发生改变或者 View 树内部 view 的可见性发生改变时 方法 onGlobalLayout () 都会被调用。所以是会回调多次。 此时也可以获取 view 的宽高:

1
2
3
4
5
6
7
8
9
10
11
12
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mDefaultControlLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
if (mIsGroupListAnimating) {
mIsGroupListAnimationPending = true;
} else {
updateLayoutHeightInternal(animate);
}
}
});

# 3.2Layout 过程

layout () 的作用是 View 用来确定 view 本身位置,内部调用 onLayout () 来确定子 view 的位置。 layout 过程比 measure 过程简单很多。看 View 的 layout 方法:

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
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//使用setFrame方法设置4个顶点,就确定位置了~
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//这里调用onLayout,是个空实现。ViewGroup中重写了,还是空实现,但加了abstract,即ViewGroup的子类必须重写onLayout确定子View的位置。
onLayout(changed, l, t, r, b);

if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}

mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}

...
}

先是用 setFrame 方法设置 4 个顶点,就确定位置了,即 mLeft、mTop、mBottom、mRight 确定了。 然后调用 onLayout,是个空实现。ViewGroup 中重写了 onLayout,还是空实现,但加了 abstract,即 ViewGroup 的子类必须重写 onLayout 确定子 View 的位置。 那就看看 LinearLayout 的 onLayout

1
2
3
4
5
6
7
8
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}

继续看 layoutVertical ():

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
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;

int childTop;
int childLeft;

// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;

// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;

final int count = getVirtualChildCount();

final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;

// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;

case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
//遍历子view
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
//获取child的测量宽高
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();

final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();

int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;

case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;

case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}

if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}

childTop += lp.topMargin;
//以上就是获取子view的左、上的位置,即宽高,然后调用setChildFrame
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
//top位置加上高度和margin,就是下一个view的top
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

i += getChildrenSkipCount(child, i);
}
}
}

就是遍历子 view,确认 childLeft、childTop,调用 setChildFrame 确认子 view 的位置:

1
2
3
4
private void setChildFrame(View child, int left, int top, int width, int height) {
//这里width、height就是 上面获取的 测量宽高
child.layout(left, top, left + width, top + height);
}

也就是调用 child 的 layout 方法,这样就走 child 的 layout 过程了。

一个问题:getMeasuredWidth () 与 getWidth () 有何区别? 答曰:一般情况,getMeasuredWidth () 与 getWidth () 两者无区别。 先看,getWidth ():

1
2
3
public final int getWidth() {
return mRight - mLeft;
}

在上面分析 LinearLayout 时,child.layout 的参数中 mRight 就是 mLeft + measuredWidth,所以 getWidth () 就是 measuredWidth。只不过是 measuredWidth 在测量过程产生,getWidth () 在 layout 过程产生。 只要不重写 view 的 layout () 方法(也不需要重写)改变顶点位置就不会出现不同的情况,例如下面这个最终宽高比测量宽高大 100。

1
2
3
public void layout(int l, int t, int r, int b) {
super.layout(l,t,r+100,b+100);
}

# 3.3Draw 过程

draw 过程: 1、画背景 2、画自己 -- onDraw,自己实现 3、画子 view-- dispatchDraw 4、画装饰

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
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/

// Step 1, draw the background, if needed
int saveCount;

if (!dirtyOpaque) {
drawBackground(canvas);
}

// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);

// Step 4, draw the children
dispatchDraw(canvas);

drawAutofilledHighlight(canvas);

// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}

// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);

// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);

if (debugDraw()) {
debugDrawFocus(canvas);
}

// we're done...
return;
}

ViewGroup 一般不用 onDraw 画自己,只需要画子 View 就可以了。但明确需要画自己的话,需要调用 setViewNotDraw (false);

以上 View 的三大流程就分析完了。

# 4、自定义 View

自定义 view 涉及 view 层次结构、事件分发、工作原理,有一定复杂度,但也是有章可循的。

# 4.1 自定义 view 的分类

  1. 继承 View:重写 onDraw,要处理 wrap_content、padding。
  2. 继承 ViewGroup:重写 onMeasure 测量自己、子 View,重写 onLayout 布局子 View。
  3. 继承特定 View(如 TextView):扩展自己的功能。
  4. 继承特定 ViewGroup(如 LinearLayout):扩展自己的功能。

# 4.2 自定义 view 注意点

  1. 支持 wrap_content:直接继承 View 或 ViewGroup 的,要在 onMeasure 中处理 wrap_content 的情况。
  2. 支持 padding:直接继承 View 在 onDraw 中处理;直接继承 ViewGroup,在 onMeasure、onLayout 中处理 padding 和子 view 的 margin。
  3. 不要在 View 中使用 handler,因为本身提供了 post 方法。
  4. 在 View#onDetachedFromWindow 中停止动画或线程。
  5. 处理好嵌套滑动。

# 4.3 例子

自定义 ViewGroup 实例:横向滑动 HorizontalView

# 4.4 自定义 view 的思想

先掌握基本功,弹性滑动、滑动冲突、绘制原理等,然后选择自定义的类别,按照注意事项多做就可以了。

Edited on Views times