视图的载体View

什么是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创建的时候。001

onPause

  当Activity退到后台的时候。002

onRestart

  当Activity从后台进入前台的时候。003

onDestroy

  当Activity销毁的时候。004

有什么作用呢

  那我们了解View的这些生命周期方法有什么作用呢?下面我就列举下我们经常遇见的问题。

在Activity中获取View的宽高

  你是否也曾经在Activity的onCreate,onResume等方法中获取过View的宽高,是否也同样得到了0的结果。从View的生命周期方法调用我们可以看出,在Activity的onResume方法调用的时候,View还没有完成测量,当然获取到的是0了。我们可以在Activity的onWindowFocusChanged()方法中获取View的宽高。

1
2
3
4
5
6
7
override 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
8
override fun onWindowVisibilityChanged(visibility: Int){
super.onWindowVisibilityChanged(visibility)
if (visibility == View.VISIBLE){
// Activity Resumed
} else {
//Activity Paused
}
}

释放资源

  有时候我们需要在View从Window上分离的时候释放一些占用内存的资源,比如Bitmap的回收,线程的释放等。

1
2
3
4
override 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
12
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];
//如果子控件的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
13
protected 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
57
public 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
7
public 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
4
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

  来看看getSuggestedMinimumWidth()和getSuggestedMinimumHeight()方法。

1
2
3
4
5
6
7
protected 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
18
public 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
2
protected 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
49
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;
}

//先保存之前的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
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
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
//View的矩阵是否发生了变化
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;

int drawn = mPrivateFlags & PFLAG_DRAWN;
//View之前的宽高
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
//父控件传入的宽高
int newWidth = right - left;
int newHeight = bottom - top;
//尺寸是否发生了变化
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

invalidate(sizeChanged);

//设置View的4个顶点
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

mPrivateFlags |= PFLAG_HAS_BOUNDS;
//如果View的尺寸发生了变化,则调用sizeChange()
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}

if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);
invalidateParentCaches();
}

mPrivateFlags |= drawn;

mBackgroundSizeChanged = true;
mDefaultFocusHighlightSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}

notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}

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
67
protected 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()方法。

本文标题:视图的载体View

文章作者:严方雄

发布时间:2018-04-10

最后更新:2018-09-13

原始链接:http://yanfangxiong.com/2018/04/10/视图的载体View/

0%