什么是View
View是屏幕上的一块矩形区域,负责绘制和触摸反馈。
View的生命周期
View中有很多回调方法,它们在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/**
* View在xml文件中加载完成的时候调用
*/
fun onFinishInflate()
/**
* View关联的Window可视性发生变化的时候调用
*/
fun onWindowVisibilityChanged(visibility: Int)
/**
* View的可视性发生变化的时候调用
*/
fun onVisibilityChanged(visibility: Int)
/**
* View关联的Window获取焦点或者失去焦点的时候调用
*/
fun onWindowFocusChanged(hasWindowFocus: Boolean)
/**
* View获取焦点或者失去焦点的时候调用
*/
fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?)
/**
* 测量View及子View的时候调用
*/
fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
/**
* 当View的大小发生变化的时候调用
*/
fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int)
/**
* 布局View及其子View的时候调用
*/
fun onLayout(changed: Boolean, w: Int, h: Int, oldw: Int, oldh: Int)
/**
* 绘制View及其子View的时候调用
*/
fun onDraw(canvas: Canvas)
/**
* View被关联到Window的时候调用
*/
fun onAttachedToWindow()
/**
* View从Window上分离的时候调用
*/
fun onDetachedFromWindow()
/**
* 触摸事件发生的时候调用
*/
fun onTouchEvent(event: MotionEvent?)
/**
* 物理按键事件发生的时候调用
*/
fun onKeyDown(keyCode: Int, event: KeyEvent)
/**
* 物理按键事件发生的时候调用
*/
fun onKeyUp(keyCode: Int, event: KeyEvent)
和Activity生命周期的关系
为了研究View生命周期和Activity生命周期之间的关系,我编写了一个CustomView类,下面我们就来看看究竟发生了什么有趣的事情。
onCreate
当Activity创建的时候。
onPause
当Activity退到后台的时候。
onRestart
当Activity从后台进入前台的时候。
onDestroy
当Activity销毁的时候。
有什么作用呢
那我们了解View的这些生命周期方法有什么作用呢?下面我就列举下我们经常遇见的问题。
在Activity中获取View的宽高
你是否也曾经在Activity的onCreate,onResume等方法中获取过View的宽高,是否也同样得到了0的结果。从View的生命周期方法调用我们可以看出,在Activity的onResume方法调用的时候,View还没有完成测量,当然获取到的是0了。我们可以在Activity的onWindowFocusChanged()方法中获取View的宽高。1
2
3
4
5
6
7override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
Log.d("Amoryan", "${customView.width}")
Log.d("Amoryan", "${customView.height}")
}
}
保存和恢复数据
在Activity的生命周期发生变化的时候,View有可能需要作出相应的相应,比如VideoView需要保存和回复当前进度。1
2
3
4
5
6
7
8override fun onWindowVisibilityChanged(visibility: Int){
super.onWindowVisibilityChanged(visibility)
if (visibility == View.VISIBLE){
// Activity Resumed
} else {
//Activity Paused
}
}
释放资源
有时候我们需要在View从Window上分离的时候释放一些占用内存的资源,比如Bitmap的回收,线程的释放等。1
2
3
4override fun onDetachedFromWindow(){
super.onDetachedFromWindow()
//释放资源
}
测量流程
View在做测量的时候,measure()方法会被父控件调用,在measure()方法中调用自身的onMeasure()方法进行实际的测量。
View和ViewGroup的测量是有区别的,View的测量会计算自身的尺寸;但是ViewGroup会先遍历子View的,调用子View的measure()方法,最后再计算自身的尺寸。
ViewGroup的测量
我们打开ViewGroup.java的源码文件,找到measureChildren()。1
2
3
4
5
6
7
8
9
10
11
12protected 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];
//如果子控件的Visibility属性不是View.GONE,则进行测量
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
这个方法做的事情很明确,会遍历子控件,如果子控件的visibility属性不是View.GONE,则调用measureChild()方法。下面我们再来看看measureChild()方法做了什么事情。1
2
3
4
5
6
7
8
9
10
11
12
13protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//获取子控件的LayoutParams
final LayoutParams lp = child.getLayoutParams();
//生成子控件width的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
//生成子控件height的MeasureSpec
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//调用子控件的measure方法进行测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
从源码可以看出,这个方法做了如下几件事情
1. 会先获取子控件的LayoutParams;
2. 然后根据自身的MeasureSpec,和子控件的LayoutParams计算子控件的MeasureSpec;
3. 最后再调用子控件的measure()方法进行子控件的测量流程。
那么子控件的MeasureSpec是如何生成的呢,下面我们就来看看getChildMeasaureSpec()方法是如何计算的。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
57public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获取ViewGroup的specMode和specSize
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//计算当前能够给予的最大值(父控件给予的值减去ViewGroup的内边距)
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
//根据ViewGroup的MeasureSpec和View的LayoutParams得到View的MeasureSpec
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//子控件的LayoutParams是具体值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子控件的LayoutParams是MATCH_PARENT
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子控件的LayoutParams是WRAP_CONTENT
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//调用MeasureSpec的makeMeasureSpec方法生成子控件的MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这个方法的逻辑也十分明了
1. 得到ViewGroup的specMode和specSize;
2. 获取ViewGroup能够给予子View的最大size;
3. 根据ViewGroup的specMode以及子View的LayoutParams得到子View的specMode和specSize;
4. 通过MeasureSpec的makeMeasureSpec()方法生成子View的MeasureSpec。
最后再来看看makeMeasureSpec()方法。1
2
3
4
5
6
7public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
虽然if-else分支计算的值是一样的,但是我还是好奇的看了看sUseBrokenMakeMeasureSpec这个成员变量。发现在View构造的时候会根据版本修改这个值。1
sUseBrokenMakeMeasureSpec = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1
只是在API17之前使用旧的MeasureSpec计算方式。
View的测量
看完ViewGroup的测量之后,我们再来看看View的onMeasure()方法。1
2
3
4protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
来看看getSuggestedMinimumWidth()和getSuggestedMinimumHeight()方法。1
2
3
4
5
6
7protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
这个方法只是获取最小的宽度和高度。然后我们来看看getDefaultSize()方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public static int getDefaultSize(int size, int measureSpec) {
//先赋值为最小值
int result = size;
//获取specMode和specSize
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//根据specMode得到最终size,如果MeasureSpec不是UNSPECIFIED,那么最终的size就是ViewGroup能给予的最大size
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
可以看到这个方法会根据specMode得到View最终的size,但但是,AT_MOST表示LayoutParams是WRAP_CONTENT,从源码可以看出,如果设置为WRAP_CONTENT,最终计算的值实际上并不是包裹内容的,而是父控件能够给予的最大值,所所以,这就说明了为什么我们在自定义View的时候需要重写onMeasure方法给出specMode是AT_MOST的时候的实际size的计算方式了。
布局流程
View的布局流程主要是layout()和onLayout()方法,从ViewRootImpl的performLayout()中会调用根View的layout()方法,然后再逐层的遍历,在layout()中传入View的left, top, right, bottom值,并且调用onLayout()进行实际的布局。对于View,因为没有子控件,所以onLayout()什么也不做。1
2protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
layout
ViewGroup在onLayout()方法中会调用子View的layout(),告诉子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
42
43
44
45
46
47
48
49public 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;
}
//先保存之前的left, top, right, bottom
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//setFrame()确定View的位置,changed表示View的矩阵是否发生了变化
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//调用onLayout()
onLayout(changed, l, t, r, b);
//是否需要绘制滚动条
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
//调用onLayoutChangeListener的方法
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);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
我们可以看到layout()方法主要做了两件事情
1. 调用setFrame()确定View的四个顶点的位置;
2. 对于ViewGroup,调用onLayout()确定子View的位置。
setFrame
1 | protected boolean setFrame(int left, int top, int right, int bottom) { |
FrameLayout的onLayout
ViewGroup的onLayout()是一个抽象方法,因为不同的ViewGroup有不同的逻辑,这里我们来看看FrameLayout的onLayout()。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
67protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
//获取内边距
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
//遍历子控件
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
//如果子View的visibility属性不是View.GONE
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
//没有设置gravity,则默认为Gravity.LEFT|Gravity.TOP
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
//根据水平Gravity确定子View的left位置
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL://如果是水平居中
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT://如果是靠右
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
//根据垂直Gravity确定View的top位置
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL://垂直居中
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
//调用子控件的layout()
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
这个方法实际上做的事情非常简单
1. 它将gravity分为了水平方向和垂直方向;
2. 通过水平方向的gravity计算出子View的left值;
3. 通过垂直方向的gravity计算出View的top值;
4. 最后再调用子View的layout()方法。