假设我们的 Activity 中的布局是这样的
ViewGroup 的事件处理过程
当在屏幕中手指按下,首先事件分发的流程最初都是从 Activity 的 dispatchTouchEvent 开始的,假设如上的界面中,我们点击了 Button 控件,看下此次流程是怎么样的,看下 Activity 中里面做了什么
1
2
3
4
5
6
7
8
9
10
|
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
// 如果改事件未处理,返回了 false ,则会回调 activity 自己的 onTouchEvent 方法中处理
return true;
}
return onTouchEvent(ev);
}
|
onUserInteraction 方法是一个空的实现,并没有做什么处理,紧接着是调用了 getWindow 中的 superDispatchTouchEvent 方法。进入到 Window 的抽象类中,可以发现官方的解释,Window 的唯一实现类就是 PhoneWindow, 所以这里调用的就是 PhoneWindow 中的方法了,看下这里做了什么处理
1
2
3
4
|
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
|
这里调用的 mDecor.superDispatchTouchEvent(event) 方法, 这里如果了解 Activity 的视图结构的话,其实就会知道 mDecor 就是 DecorView ,再看下这个类的具体实现是什么
1
2
3
|
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
|
由于 DecorView 继承自 FrameLayout ,而 FrameLayout 继承自 ViewGroup ,所以最终的调用也就是到了 ViewGroup 当中,其实可以发现,这里从 Activity 整个流程下来,并没有做什么逻辑上的判断处理,都只是交给另外一个类去处理。既然如此,就看下 ViewGroup 中的代码是如何实现的吧,不过 ViewGroup 中的代码比较复杂,我们先可以看下一段伪代码的实现,大致流程是这样的,代码如下:
1
2
3
4
5
6
7
8
9
10
11
|
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
|
这段代码其实就可以很直观的表现出 ViewGroup 的传递流程,基本上是算递归的层层调用,首先在当前的 ViewGroup 的话,会经过自己的 dispatchTouchEvent 方法,然后再询问经过 onInterceptTouchEvent 方法,确认事件是否需要拦截,如果返回 true 代表拦截当前事件,那么会经过自己的 onTouchEvent 方法。如果返回 false,不拦截,那么会传递到子 View 的 dispatchTouchEvent 方法中,如此反复,直到事件结束。
接着我们再看下 ViewGroup 中具体的实现,由于这个方法比较长,先看部分的实现,做分段说明,先看下面这一点,很显然,它描述的逻辑是是否拦截点击事件这个逻辑,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public boolean dispatchTouchEvent(MotionEvent ev) {
final boolean intercepted;
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;
}
} else {
intercepted = true;
}
}
|
从这段代码可以看出来,ViewGroup 会判断是否需要拦截当前事件,这里说的当前事件一般是指的某一个事件,最先开始的也就是 down 事件,一般一组事件,叫一组事件序列,也就是按下,移动,抬起
有两个条件,一个是当前的事件类型为 ACTION_DOWN,然后就是 mFirstTouchTarget != null ,这个 mFirstTouchTarget 是什么意思呢,从下面后续的源码中可以看出来,只要是当事件经过了
ViewGroup 的子元素时候,那么 mFirstTouchTarget 会被赋值,所以,如果再后后续事件 ACTION_MOVE 和 ACTION_UP 事件,那么这个判断条件也会成立。同理,如果当前事件未传递到子 View 上,那么 mFirstTouchTarget 就会为 null,那么后续的 move 和 up 事件都不会再经过 onInterceptTouchEvent 方法。其实这也就间接的说明了,onInterceptTouchEvent 不是每次都会被调用的,所以在处理事件冲突的时候,要注意这个情况,可以将自己的处理事件冲突的方法放在 dispatchTouchEvent 方法中
不过这里还有一个参数,那就是 FLAG_DISALLOW_INTERCEPT 标记位,一般情况下,如果不设置的话,disallowIntercept 一般为 false,一般修改这个值的话,是在子 View 中通过 requestDisallowInterceptTouchEvent 方法,来修改这个标记位的。这个标记位一旦被设置,也就是 disallowIntercept 某种条件下为 true 的时候,那么就会导致除了 ACTION_DOWN 的以外事件无法接受,为什么说除了 ACTION_DOWN 的以外事件呢 ?因为 ViewGroup 在事件分发时候,如果是 ACTION_DOWN 的时候,会重置这个状态,导致子view设置的这个标记位无效。因此,当面对 ACTION_DOWN 事件时,ViewGroup 总是会询问自己的 onInterceptTouchEvent 方法是否需要拦截事件。那么在 ACTION_DOWN 的时候,是怎么恢复的呢 ?
代码如下 :
1
2
3
4
|
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
|
如上所示,每次在 ACTION_DOWN 的时候,都会在调用 resetTouchState 方法,然后重置这个标记位。
接着再看看 ViewGroup 不拦截事件
的时候,事件会向下分发给它的子 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
|
if (!canceled && !intercepted) { // 事件没有被拦截
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
|
上述代码首先是遍历 ViewGroup 中的所有子 View,然后判断子 View 是否能够接受到事件
,是否能够接受到事件有两点可以来衡量,一个是子 View 是否正在播放动画和点击事件的坐标是否在子 View 的区域内。如果都不符合,continue 后继续询问下一个子元素。其中 dispatchTransformedTouchEvent 方法其实就是事件往子 View 进行分发的方法,在他的内部有如下一段代码:
1
2
3
4
5
|
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
|
其实就是根据 dispatchTransformedTouchEvent 方法传递的 child 来做的区分,如果 child 为null,那么将会调用自身 View 的 dispatchTouchEvent 方法,如果不为 null ,那么将会调用子 View 的 dispatchTouchEvent 方法,这样就算是完成了一轮的事件分发。
如果子 View 的 dispatchTouchEvent 返回 true,也就是当前事件被消费了,那么 dispatchTransformedTouchEvent 就会为 true,那么就会调用如下代码:
1
2
3
4
5
6
|
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 事件被消费,返回 true
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
|
其中,addTouchTarget 方法中,其实是给 mFirstTouchTarget 给赋值
了,这里的 mFirstTouchTarget 和 newTouchTarget 其实是相等的, mFirstTouchTarget 这个值也就是我们上面提到过的,在 ViewGroup 当前事件传递到了子 View 中,也就是调用 handled = child.dispatchTouchEvent(event) 返回了 true,那么 mFirstTouchTarget 就不会为 null
, 那么后续如果有 ACTION_MOVE 和 ACTION_UP 事件,那么也可以收到事件了。看看 addTouchTarget 内部是如何实现的:
1
2
3
4
5
6
|
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget; // 下一个子元素,进行赋值,是一个单链表结构
mFirstTouchTarget = target;
return target;
}
|
上述代码可以很明显的看到,mFirstTouchTarget 被赋值,mFirstTouchTarget 是否被赋值,将直接影响到 ViewGroup 对事件的拦截策略,如果mFirstTouchTarget 为 null ,那么 ViewGroup 默认拦截接下来同一事件序列中所有的点击事件,也就是后续 ACTION_MOVE 和 ACTION_UP 事件被拦截了
需要注意的是,如果上面代码中的 for 循环遍历所有的子元素如果都没有被处理的话,这包含了两种情况,第一种是 ViewGroup 没有子元素;第二种是子元素在 dispatchTouchEvent 中返回了 false,这一般是因为子元素中 onTouchEvent 中返回了 false(其实也就说明了这两种情况都是导致 mFirstTouchTarget 为 null 的情况发生的原因)也就是对应上面的源码部分 dispatchTransformedTouchEvent 返回了 false。这种情况下,ViewGroup 会处理自己的点击事件。然后会调用如下的代码:
1
2
3
4
5
|
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
|
到了这里 child 传递的 null 了,从上面的分析可以看出,那么调用的就是 super.dispatchTouchEvent(event) 的方法了,其实,也就是调动到了 View 的 super.dispatchTouchEvent 方法了,即点击事件开始交给 View 来处理了。这里也就可以说明一个情况了,只有 ViewGroup 会传递事件一级一级的往下分发,当这个 View 没有子元素
(View 没有子元素,ViewGroup 可以没有子元素),随即也就切回到 View 的事件处理过程了。
View 对点击事件处理过程
View 对点击事件的处理过程会简单一些,注意这里是 View, 而不是 ViewGroup,看下他的 dispatchTouchEvent 方法实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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;
}
}
|
这里的事件处理会比较简单,因为 View (不是 ViewGroup)是一个单独的元素
,它没有子元素,无法向下传递事件,所以它只能够自己处理,这里唯一要注意的就是几个优先级了,首先是会判断有没有设置 mOnTouchListener 事件,如果外部设置了,并且在 onTouch 方法中返回了 true,那么 onTouchEvent 方法将不会被调用,如果返回 false,则会调用 onTouchEvent 方法,所以说 onTouch 方法比 onTouchEvent 方法优先级高一些。再看看 View 中的 onTouchEvent 是如何实现的,这里我们也进行一点点的拆分,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
|
可以看到,View 如果是 DISABLED 不可用的状态的,仍然会根据 clickable (根据控件点击事件状态) 的结果值,去消费响应事件的。尽管这个控件是不可用的。
紧接着会调用如下代码:
1
2
3
4
5
|
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
|
如果 View 设置有代理,那么会执行 mTouchDelegate.onTouchEvent 方法
再看下 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
|
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
break;
case MotionEvent.ACTION_DOWN:
// ...
break;
case MotionEvent.ACTION_MOVE:
// ...
break;
}
return true; // 最终消费了事件
}
|
根据上面的 clickable 状态可以知道,只要 CLICKABLE 或者是 LONG_CLICKABLE 有一个状态为 true, 那么 onTouchEvent 就会返回为 true,那么代表它消费了事件,不管他是不是 DISABLE 的状态。其中这里当手指抬起的时候,那么就会进入 ACTION_UP 代码块中,然后调用 performClickInternal 方法,然后再调用 performClick 方法,如果 View 设置了 OnClickListener 的话,那么就会触发 onClick 方法执行。performClick 方法代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
// 最终调用 onClick 方法,将 View 对象传递出去
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
|
从 ViewGroup 的事件分发,到最后直接给到 View 处理,点击事件的分发机制源码就已经分析完了。
掌握了事件分发是如何进行的,那么就可以很容易的解决事件冲突了,其实针对于事件冲突,也是有套路轨迹可循的,下一节,将分析事件冲突如何解决。