View测量流程介绍

This class represents the basic building block for user interface components. A Viewoccupies a rectangular area on the screen and is responsible for drawing and event handling.
View是屏幕上的一块矩形区域,负责界面的绘制与触摸事件的处理,它是一种界面层控件的抽象,所有的控件都继承自View。

View从ViewRoot.performTraversals()开始 经历measure、layout、draw三个流程最终显示在用户面前,用户在点击屏幕时,点击事件随着Activity传入Window,最终由ViewGroup/View进行分发处理。
本篇文章主要简单介绍了解View的测量流程,基于Android7.0系统源码分析。


View 方法了解

上一篇文章已经给出了View跟随Activity的生命周期流程图,这里就不再介绍。

以下是自定义View,源码中已经说明了各个方法调用的时机,这些方法什么用呢?

  • 场景1 :在Activity生命周期发生变化时,View也要做响应的处理,典型的有VideoView保存进度和恢复进度。(例如:onVisibilityChanged方法)
  • 场景2 :释放线程、资源(例如:onDetachedFromWindow方法)
  • 还有很多场景,针对自定义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
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
public class CustomView extends View {

private static final String TAG = "View";

public CustomView(Context context) {
super(context);
Log.d(TAG, "CustomView()");
}

public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
Log.d(TAG, "CustomView()");
}

public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Log.d(TAG, "CustomView()");
}

/**
* View在xml文件里加载完成时调用
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
Log.d(TAG, "View onFinishInflate()");
}

/**
* 测量View及其子View大小时调用
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "View onMeasure()");
}

/**
* 布局View及其子View大小时调用
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.d(TAG, "View onLayout() left = " + left + " top = " + top + " right = " + right + " bottom = " + bottom);
}

/**
* View大小发生改变时调用
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.d(TAG, "View onSizeChanged() w = " + w + " h = " + h + " oldw = " + oldw + " oldh = " + oldh);
}

/**
* 绘制View及其子View大小时调用
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d(TAG, "View onDraw()");
}

/**
* 物理按键事件发生时调用
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
Log.d(TAG, "View onKeyDown() event = " + event.getAction());
return super.onKeyDown(keyCode, event);
}

/**
* 物理按键事件发生时调用
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
Log.d(TAG, "View onKeyUp() event = " + event.getAction());
return super.onKeyUp(keyCode, event);
}

/**
* 触摸事件发生时调用
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "View onTouchEvent() event = " + event.getAction());
return super.onTouchEvent(event);
}

/**
* View获取焦点或者失去焦点时调用
*/
@Override
protected void onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
Log.d(TAG, "View onFocusChanged() gainFocus = " + gainFocus);
}

/**
* View所在窗口获取焦点或者失去焦点时调用
*/
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
Log.d(TAG, "View onWindowFocusChanged() hasWindowFocus = " + hasWindowFocus);
}

/**
* View被关联到窗口时调用
*/
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Log.d(TAG, "View onAttachedToWindow()");
}

/**
* View从窗口分离时调用
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
Log.d(TAG, "View onDetachedFromWindow()");
}

/**
* View的可见性发生变化时调用
*/
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
Log.d(TAG, "View onVisibilityChanged() visibility = " + visibility);
}

/**
* View所在窗口的可见性发生变化时调用
*/
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
Log.d(TAG, "View onWindowVisibilityChanged() visibility = " + visibility);
}
}

View的测量流程

View所占的区域是一个矩形

  • 位置:由左上角坐标(getLeft(), getTop())决定,它是以它父View的左上角为坐标原点计算出来的,单位是pixels
  • 大小:View的大小由两对值来表示。getMeasuredWidth()/getMeasuredHeight()这组值表示该View在它父View里期望的大小值,在measure()方法完成后可获得。 getWidth()/getHeight()这组值表示了该View在屏幕上的实际大小,在draw()方法完成后可获得。
  • 内边距:View的内边距用padding来表示,它表示View的内容距离View边缘的距离。通过getPaddingXXX()方法获取。需要注意的是我们在自定义View的时候需要单独处理 padding,否则它不会生效。
  • 外边距:View的内边距用margin来表示,它表示View的边缘离它相邻的View的距离。

对于onMeasure()方法

  • View:View 在 onMeasure() 中会计算出自己的尺寸然后保存;
  • ViewGroup:ViewGroup在onMeasure()中会调用所有子View的measure()让它们进行自我测量,并根据子View计算出的期望尺寸来计算出它们的实际尺寸和位置然后保存。同时,它也会 根据子View的尺寸和位置来计算出自己的尺寸然后保存

在做测量的时候,measure()方法被父View调用,在measure()中做一些准备和优化工作后,调用onMeasure()来进行实际的自我测量。

  • MeasureSpec介绍(它用来把测量要求从父View传递给子View)
    • 高2位:SpecMode,测量模式
    • 低30位:SpecSize,在特定测量模式下的大小

测量模式有三种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static class MeasureSpec {

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;

//父View不对子View做任何限制,需要多大给多大,这种情况一般用于系统内部,表示一种测量的状态
public static final int UNSPECIFIED = 0 << MODE_SHIFT;

//父View已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值,它对应LayoutParams中的match_parent和具体数值这两种模式
public static final int EXACTLY = 1 << MODE_SHIFT;

//父View给子VIew提供一个最大可用的大小,子View去自适应这个大小。
public static final int AT_MOST = 2 << MODE_SHIFT;
}

在View测量的时候,LayoutParams会和父View的MeasureSpec相结合被换算成View的MeasureSpec,进而决定View的大小。

View的MeasureSpec计算源码如下所示:

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
public abstract class ViewGroup extends View implements ViewParent, ViewManager {

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

}

该方法用来获取子View的MeasureSpec,由参数我们就可以知道子View的MeasureSpec由父容器的spec,父容器中已占用的空间大小 padding,以及子View自身大小childDimension共同来决定的。

普通View的MeasureSpec的创建规则:

  • 当View采用固定宽高的时候,不管父容器的MeasureSpec是什么,resultSize都是指定的宽高,resultMode都是MeasureSpec.EXACTLY。
  • 当View的宽高是match_parent,当父容器是MeasureSpec.EXACTLY,则View也是MeasureSpec.EXACTLY,并且其大小就是父容器的剩余空间。当父容器是MeasureSpec.AT_MOST 则View也是MeasureSpec.AT_MOST,并且大小不会超过父容器的剩余空间。
  • 当View的宽高是wrap_content时,不管父容器的模式是MeasureSpec.EXACTLY还是MeasureSpec.AT_MOST,View的模式总是MeasureSpec.AT_MOST,并且大小都不会超过芙蓉的剩余空间。

对于顶级View(DecorView)其MeasureSpec由窗口的尺寸和自身的LayoutParams共同确定的。
对于普通View其MeasureSpec由父容器的Measure和自身的LayoutParams共同确定的。

View的绘制会先调用View的measure()方法,measure()方法用来测量View的大小,实际的测量工作是由子View的onMeasure()来完成的。

View.onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法源码

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
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {

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

//设置View宽高的测量值
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;

measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);

//measureSpec指的是View测量后的大小
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
//MeasureSpec.UNSPECIFIED一般用来系统的内部测量流程
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//我们主要关注着两种情况,它们返回的是View测量后的大小
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

//如果View没有设置背景,那么返回android:minWidth这个属性的值,这个值可以为0
//如果View设置了背景,那么返回android:minWidth和背景最小宽度两者中的最大值。
protected int getSuggestedMinimumHeight() {
int suggestedMinHeight = mMinHeight;

if (mBGDrawable != null) {
final int bgMinHeight = mBGDrawable.getMinimumHeight();
if (suggestedMinHeight < bgMinHeight) {
suggestedMinHeight = bgMinHeight;
}
}

return suggestedMinHeight;
}
}

View的onMeasure()方法实现比较简单,它调用setMeasuredDimension()方法来设置View的测量大小,测量的大小通过getDefaultSize()方法来获取。

如果我们直接继承View来自定义View时,需要重写onMeasure()方法,并且需要指定高度为wrap_content时的大小。
(这是因为通过上面的描述我们知道etDefaultSize()方法中AT_MOST与EXACTLY模式下,返回的 都是specSize,这个specSize是父View当前可以使用的大小,如果不处理,那wrap_content就相当于match_parent。)
处理代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "widthMeasureSpec = " + widthMeasureSpec + " heightMeasureSpec = " + heightMeasureSpec);

//指定一组默认宽高,至于具体的值是多少,这就要看你希望在wrap_cotent模式下
//控件的大小应该设置多大了
int mWidth = 200;
int mHeight = 200;

int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);

int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

if (widthSpecMode == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}

注:你可以自己尝试一下自定义一个View,然后不重写onMeasure()方法,你会发现只有设置match_parent和wrap_content效果是一样的,事实上TextView、ImageView 等系统组件都在wrap_content上有自己的处理,可以去瞧一瞧相关源码。

前面说到了View的onMeasure(),那我们看看ViewGroup的onMeasure()方法有何区别吧!由于ViewGroup是继承自View的一个抽象类,没有实现onMeasure(),所以我们只能找子类

View.onMeasure()方法的具体实现一般是由其子类来完成的,对于应用窗口的顶级视图DecorView来说,它继承于FrameLayout,那我们来看看FrameLayout.onMeasure() 方法的实现。

FrameLayout.onMeasure(int widthMeasureSpec, int heightMeasureSpec):

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 class FrameLayout extends ViewGroup {

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int count = getChildCount();

int maxHeight = 0;
int maxWidth = 0;

// Find rightmost and bottommost child
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
}
}

// Account for padding too
maxWidth += mPaddingLeft + mPaddingRight + mForegroundPaddingLeft + mForegroundPaddingRight;
maxHeight += mPaddingTop + mPaddingBottom + mForegroundPaddingTop + mForegroundPaddingBottom;

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

// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}

setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec),
resolveSize(maxHeight, heightMeasureSpec));
}

public static int resolveSize(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:
result = Math.min(size, specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
}

可以看到该方法主要做了以下事情:

  • 调用measureChildWithMargins()去测量每一个子View的大小,找到最大高度和宽度保存在maxWidth/maxHeigth中。

  • 将上一步计算的maxWidth/maxHeigth加上padding值,mPaddingLeft,mPaddingRight,mPaddingTop ,mPaddingBottom表示当前内容区域的左右上下四条边分别到当前视图的左右上下四条边的距离, mForegroundPaddingLeft ,mForegroundPaddingRight,mForegroundPaddingTop ,mForegroundPaddingBottom表示当前视图的各个子视图所围成的区域的左右上下四条边到当前视图前景区域的 左右上下四条边的距离,经过计算获得最终宽高。

  • 当前视图是否设置有最小宽度和高度。如果设置有的话,并且它们比前面计算得到的宽度maxWidth和高度maxHeight还要大,那么就将它们作为当前视图的宽度和高度值。

  • 当前视图是否设置有前景图。如果设置有的话,并且它们比前面计算得到的宽度maxWidth和高度maxHeight还要大,那么就将它们作为当前视图的宽度和高度值。

  • 经过以上的计算,就得到了正确的宽高,先调用resolveSize()方法,获取MeasureSpec,接着调用父类的setMeasuredDimension()方法将它们作为当前视图的大小。

  • resolveSize(int size, int measureSpec)方法是按照以下逻辑生成最后的大小。

    • MeasureSpec.UNSPECIFIED: 取size
    • MeasureSpec.AT_MOST: 取size, specSize的最小值
    • MeasureSpec.EXACTLY: 取specSize

注意事项

Measure的整个流程完成以后,我们可以通过getMeasuredWidth()与getMeasuredHeight()来获得View的宽高。但在某些情况下,系统需要经过多次Measure才能确定 最终的宽高,因此在onMeasure()方法中拿到的宽高很可能是不正确的,比较好的做法是在onLayout()方法中获取View的宽高。