Android----View事件分发和绘制机制

一.View 的事件分发

(一).概念

1.事件分发

View 的事件分发其实就是点击事件( MotionEvent )从产生后系统开始分发,到传递给一个具体的 View ( 或者Activity )的过程,View (或 Activity )会选择是否对事件进行消耗。

2.事件的类型和事件序列

(1)事件类型
  • MotionEvent.ACTION_DOWN 按下时产生的事件
  • MotionEvent.ACTION_MOVE 滑动时产生的事件
  • MotionEvent.ACTION_UP 抬起时产生的事件
  • MotionEvent.ACTION_CANCEL 发生异常时产生的事件
(2)事件序列

同一个事件序列指的是从按下时候到抬起时产生的一系列事件,通常以 DOWN 开始,中间有不定数个 MOVE ,最后以 UP 或者 CANCLE 结束。

3.分发对象和对应的方法

对事件的分发主要涉及三个对象,Activity , ViewGroup ,具体的 View,这三个对象按分发的层次依次是Activity -> ViewGroup -> 具体的 View 。而涉及分发的方法同样主要有三个:

  • dispatchTouchEvent 对一个事件进行分发,可能是分发给下一层处理,或者分发给自己。
  • onInterceptTouchEvent 这个方法只有 ViewGroup 有,用来判断对事件是否进行拦截,如果拦截就不会分发给下一层.
  • onTouchEvent 对事件进行处理,消耗或者不消耗,不消耗就会返回给上层。对于 ViewGroup 和 View 这个方法还受到 OnTouchListener 和 enable 属性 的影响,具体的后面会阐述。

(二).事件分发

1.Activity 对事件的分发

1
2
3
4
5
6
7
8
public boolean dispatchTouchEvent(MotionEvent ev) {
...

if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}

Activity 的事件的处理其实并不负责,即如果下层(不管是 ViewGroup 还是 View )消耗了这个事件,那么 if 语句就为 true , 则 dispatchTouchEvent 就返回 true 。如果没有消耗就自己对事件进行处理,即调用 onTouchEvent 方法。

1
2
3
4
5
6
7
8
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}

return false;
}

1
2
3
4
5
6
7
8
9
10
/** @hide */
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}

Activity 的 onTouchEvent 会对这个事件进行判断,如果事件在窗口边界外就返回 true,dispatchTouchEvent 就返回 true ;如果在边界内就 返回 false ,最后 dispatchTouchEvent 也会返回 false 。这部分流程如图 (这是截自整体流程图的一部分)
View 事件分发1.png

2.View 对事件的分发

这里先说 View 对事件的分发是因为 ViewGroup 继承自 View ,ViewGroup 对事件的分发会调用到父类(也就是View )的方法,因此先理清 View 的分发有助于理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean dispatchTouchEvent(MotionEvent event) {
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

if (!result && onTouchEvent(event)) {
result = true;
}
...
return result;
}

可以看到 View 的事件的处理是先判断 mOnTouchListener !=null 和 View 设置 ENABLED 这两个条件成不成立,不过成立则 调用 onTouch 方法,且如果 onTouch 返回了 true ,那个事件就被消耗 ,View 的 dispatchTouchEvent 就返回 true ; 相反,如果条件不成立或者 onTouch 返回 false ,那么就会执行 View 的 onTouchEvent 方法。

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
public boolean onTouchEvent(MotionEvent event) {  
...
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;


// 若可点击,包括LONG_CLICKABLE 或者 CLICKABLE
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {

switch (event.getAction()) {


case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;

...

// 执行performClick()
performClick();
break;


case MotionEvent.ACTION_DOWN:
...
break;


case MotionEvent.ACTION_CANCEL:
...
break;


case MotionEvent.ACTION_MOVE:
...
break;
} //>> 若可点击,就返回true
return true;
} //>> 若不可点击,就返回false
return false;
}
1
2
3
4
5
6
7
8
9
public boolean performClick() {  

if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}

在 onTouchEvent 方法中,如果 View 是可点击的,比如设置了 onClick 或者 onLongClick ,就会执行 onClick 方法,并且 onTouchEvent 返回 true ,如果是不可点击的就返回 false 。需要注意的是这里的 onTouchEvent 是可以被重写的。如果 onTouchEvent 返回 true 那么 View 的 dispatchTouchEvent 就返回 true ,事件就被消耗,如果 onTouchEvent 返回 false , 那么 dispatchTouchEvent 也返回 false ,这时 事件就交由上层处理,也就是 ViewGroup 。这部分流程如图

View 事件分发2.png

3.ViewGroup 对事件的分发

ViewGroup 对事件的分发也是从 dispatchTouchEvent 方法开始的,不同的是 ViewGroup 对了一个对事件进行拦截的方方法 onInterceptTouchEvent 。
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
public boolean dispatchTouchEvent(MotionEvent ev) {

final boolean intercepted;
...
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
...
//不拦截,分发给下一层
if (!canceled && !intercepted) {

...
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
...
}


...
// 子View 不处理,分发给自己
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) { //没有 子View 就自己处理
handled = super.dispatchTouchEvent(event);
} else { //有就分发给下一层
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}

ViewGroup 首先会判断 onInterceptTouchEvent 表示要不要拦截,这个方法也可以重写设置是否拦截,如果返回 false 就表示不拦截,这个事件就会分发给下一层,如果拦截就会分发给自己,当然如果子 View 不处理这个事件,还是会传到 ViewGroup ,ViewGroup 会调用父类也就是 View 的方法,后面的过程就和 View 对事件的处理是一样的 onTouch ,onTouchEvent , onClock …
这部分的流程如图:

View 事件分发 3.png

ViewGroup 对事件的拦截 onInterceptTouchEvent 并不是每一次都会调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (actionMasked == MotionEvent.ACTION_DOWN) {

cancelAndClearTouchTargets(ev);
resetTouchState();//重置标志
}
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
}

可以看到这里有三个条件:ACTION_DOWN 事件,mFirstTouchTarget 变量,FLAG_DISALLOW_INTERCEPT 这个标志。

1.对于 ACTION_DOWN 事件

在判断之前都会调用 resetTouchState 这个方法重新给 FLAG_DISALLOW_INTERCEPT 置位,因此只要是 DOWN 事件,onInterceptTouchEvent 就会调用。

2.对于 mFirstTouchTarget 这个变量

只要有 View 对事件进行处理,那么这个事件的后续事件就会直接交给这个 View ,mFirstTouchTarget 就直接指向 View , 正常情况下不会再询问 ViewGroup 是否拦截。而特殊情况就是下面这个。

3.对于 FLAG_DISALLOW_INTERCEPT

子 View 可以重置这个标志,使得 disallowIntercept 的值改变从而可能会重新 onInterceptTouchEvent 对事件进行拦截。

View 事件分发 4.png

到这里View 的事件分发的各个流程就已经讲完,最后是一个整体的流程:

View 事件分发机制.png

二.View 的绘制

(一).概念

1.绘制机制

View 的绘制机制实际上指的是 View 的三大流程

  • 测量流程,测量 View 的大小,对应 measure 方法。
  • 布局流程,有了 View 的大小后确定 View 的位置,对应 layout 方法。
  • 绘制流程,对 View 的颜色,内容等进行绘制,对应 draw 方法。

View 的绘制从 ViewRootImpl 的 performTraversals 开始,首先会调用 performMeasure 方法,在这个方法中会一个 View 会调用 measure 去测量自己的大小,在此之前会调用 onMeasure 去测量子 View 的大小,这样层层调用,最后就会完成整个测量过程,后面的 layout 和 draw 的过程也是大致如此。流程如图
View 绘制1.png

2.MeasureSpec

MeasureSpec 是一个测量规格,在测量一个 View 的时候 从父类计算出来的 MeasureSpec 会传给这个 View ,同时会根据 View 自身的 LayoutParams 属性,也就是指定的一些 MATCH_PARENT, WRAP_CONTENT,xxdp 等属性最终一起决定 View 的大小。

MeasureSpec 是一个 int 值,有32位,高2位代表 Mode ,后30 位Size .之所以将两个值包装在在一个 int 是因为这样可以减少减少对象的分配。而 Mode 表示测量模式,有三种测量模式分为别

  • UPSPECIFIED ,未指定,父 View 对子 View 不做任何限制
  • EXACTLY,精确,父 View 给子 View 的大小是一个确定的值,为 Size
  • AT_MOST,最大,父 View 给子 View 的大小 是一个不确定的值,最大为 Size

具体的计算就在下面的方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {

final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//这里的第一个参数就是 父View 的 MeasureSpec
//第二个参数就是父 View 对子View 的位置限制 padding
//和 子View 对自己的位置限制 margin 和 已使用的宽度 widthUsed
//第三参数就是 xml 指定的子 View 宽度
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);
}

在父 View 的 MeasureSpec 确定后,会传递给子 View ,子View 就会根据这个 MeasureSpec 和自己的 LayoutParams 属性,计算出自己的 MeasureSpec 和 大小(即 Size),然后就会将这个 MeasureSpec 传递给子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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

//父 View 减去一个 宽度/高度位置限制 就是 父View 给 子View 的 大小
int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// Parent has imposed an exact size on us
//父 View 是 EXACTLY
case MeasureSpec.EXACTLY:
// 子View 是 xxdp
if (childDimension >= 0) {
resultSize = childDimension; //使用 子View 指定的
resultMode = MeasureSpec.EXACTLY;

// 子 View 是 MATCH_PARENT
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size; //使用父 View 给的
resultMode = MeasureSpec.EXACTLY;

//子 View 是 WRAP_CONTENT
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; //使用父 View 给的
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent has imposed a maximum size on us
//父 View 是 AT_MOST
case MeasureSpec.AT_MOST:
// 子View 是 xxdp
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension; //使用 子View 指定的
resultMode = MeasureSpec.EXACTLY;

// 子 View 是 MATCH_PARENT
} 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; //使用父 View 给的
resultMode = MeasureSpec.AT_MOST;


//子 View 是 WRAP_CONTENT
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; //使用父 View 给的
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent asked to see how big we want to be
//父 View 是 UNSPECIFIED
case MeasureSpec.UNSPECIFIED:
// 子View 是 xxdp
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension; //使用 子View 指定的
resultMode = MeasureSpec.EXACTLY;

// 子 View 是 MATCH_PARENT
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; 默认为 0
resultMode = MeasureSpec.UNSPECIFIED;

//子 View 是 WRAP_CONTENT
} 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; 默认为 0
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
//将 Mode 和 Size 包装成 MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

子View 的 MeasureSpec 确定就是上述过程,这个过程可用下图表示
View 绘制2.png
理解这个图只需要注意下面几点

  • 只要子 View 是 xxdp 的,不管父View 是什么模式,子 View 都是使用自己的指定的大小 childDimension.
  • 如果父 View 是 确定的 (EXACTLY),那么 子View 是 MATCH_PARENT 也就是充满父 View 的话,子View 也是确定的 EXACTLY ;如果子 View 是 WRAP_CONTENT,那么子 View 就是不确定的,但是不能超过父 View 的大小,因此子View 就是 AT_MOST 。
  • 如果父 View 是不确定的 (AT_MOST ),那么不管子View 是 MATCH_PARENT 还是 WRAP_CONTENT ,子 View 都是不确定的。
  • 如果父 View 是未指定的 (UNSPECIFIED),那么子 View 也是未指定的,size 也就没意义,即为 0 。

(二).三个流程

View 的三个流程都都是从 ViewRootImpl 的 performTraversals 开始的,而且都是从 DecorView 开始的,这里就不对具体的情况进行梳理,而是从宏观的角度却分析,三个流程是如果在 ViewGroup 到其中的子View 中进行工作的。

1.measure

1
2
3
4
5
6
7
8
9
private void performTraversals() {
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}

在上述过程中,因为第一个 measureSpec 的产生总是布满全屏的, 即 measureSpec 是确定的 EXACTLY, size 就为屏幕大小.在 performMeasure 就会开始对 DecorView (也就是一个 ViewGroup ) 进行测量.

1
2
3
4
5
6
7
8
9
10
11
12
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
//对 DecorView 开始测量
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

因为 ViewGroup 继承自 View ,首先看 View 的 measure

1
2
3
4
5
6
7
8
9
10
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
...

}

1
2
3
4
5
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//测量自己的大小,一旦完成,View 的测量也就结束,这是 View 默认的方法,具体不同的 View 会有不同的测量方式.
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

在 View 中 measure 是一个 final 方法,因此不能被重写,在里面会调用 onMeasure 方法,这个方法就可以被重写,接下看 具体的某一个的 ViewGroup ( FrameLayout )中的这个方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
...

for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
...
}
}
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
...
}

在 FrameLayout 中的 onMeasure 首先会对去调用 measureChildWithMargins 去计算自己的 MeasureSpec 然后就去测量子 View 的大小,等所有的子 View 测量好了,就会测量自己的的大小.而子 View 会重复这个两个方法,最后完成所有 View 的 测量.这个流程如图所示:

View 绘制3.png

2.layout

1
2
3
4
5
6
7
8
9
10
11
12
13
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;

final View host = mView;
...

host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

...
}

在 performLayout 会直接 调用 host 的 layout ,这个 host 实际上就是 DecorView ,DecorView 是一个 ViewGroup ,首先看 ViewGroup 的 layout 方法.

1
2
3
4
5
6
@Override
public final void layout(int l, int t, int r, int b) {
...
super.layout(l, t, r, b);
...
}

1
2
3
4

@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);

在 ViewGroup 中 layout 是一个 final 的方法,在里面会调用父类的layout 方法,也就是 View 的 layout 方法.这里先说明 onLayout 方法,在这里是一个抽象方法,因为不同的 ViewGroup 对子 View 的位置安排是不一样的,因此具体的 onLayout 需要具体的继承类去实现.先看 View 中的 layout 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//setFrame 确定自己的位置
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);

...
}

首先在 layout 方法中会确定自己的位置,即 left,top,bottom,right 这个四个属性,接着就会调用 onLayout ,如果这是一个 View,那么 onLayout 就是一个空方法,如果这是一个 ViewGroup ,那么在这方法内就会去确定 子View 的位置.比如 FrameLayout 中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
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) {

for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
...
...
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}

这部分的流程如图:

View 绘制4.png

3.draw

draw 的绘制基本就包含五个步骤:

  • 绘制背景
  • 绘制自己的内容
  • 绘制子 View
  • 绘制foreground,比如滚动条
  • 绘制一些高光

这个过程对于 View 和 ViewGroup 是一样的,只不过 View 中 不会绘制自己的子 View ,因此是个 dispatchDraw(canvas) 在这里是个空方法.具体的流程如图:

View 绘制5.png

0%