假设我们的 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;
		}
}
  1. 从这段代码可以看出来,ViewGroup 会判断是否需要拦截当前事件,这里说的当前事件一般是指的某一个事件,最先开始的也就是 down 事件,一般一组事件,叫一组事件序列,也就是按下,移动,抬起有两个条件,一个是当前的事件类型为 ACTION_DOWN,然后就是 mFirstTouchTarget != null ,这个 mFirstTouchTarget 是什么意思呢,从下面后续的源码中可以看出来,只要是当事件经过了 ViewGroup 的子元素时候,那么 mFirstTouchTarget 会被赋值,所以,如果再后后续事件 ACTION_MOVE 和 ACTION_UP 事件,那么这个判断条件也会成立。同理,如果当前事件未传递到子 View 上,那么 mFirstTouchTarget 就会为 null,那么后续的 move 和 up 事件都不会再经过 onInterceptTouchEvent 方法。其实这也就间接的说明了,onInterceptTouchEvent 不是每次都会被调用的,所以在处理事件冲突的时候,要注意这个情况,可以将自己的处理事件冲突的方法放在 dispatchTouchEvent 方法中

  2. 不过这里还有一个参数,那就是 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 处理,点击事件的分发机制源码就已经分析完了。

掌握了事件分发是如何进行的,那么就可以很容易的解决事件冲突了,其实针对于事件冲突,也是有套路轨迹可循的,下一节,将分析事件冲突如何解决。