安卓自定义 View 进阶:事件分发机制详解

在上一篇文章 事件分发机制原理 中简要分析了一下事件分发机制的原理,原理是十分简单的,一句话就能总结:责任链模式,事件层层传递,直到被消费。 虽然原理简单,但是随着 Android 不断的发展,实际运用场景也越来越复杂,所以想要彻底玩转事件分发机制还需要一定技巧,本篇事件分发机制详解将带大家了解 ...

你以为我接下来要讲源码?
我就不按套路,所有的源码都是为了适应具体的应用场景而写的,只要能够理解运用场景,理解源码也就十分简单了。所以本篇的核心问题是:正确理解在实际场景中事件分发机制的作用。 会涉及到源码,但不是主角。

注意:本文中所有源码分析部分均基于 API23(Android 6.0) 版本,由于安卓系统源码改变很多,可能与之前版本有所不同,但基本流程都是一致的。

常见事件

既然是事件分发,总要有事件才能分发吧,所以我们先了解一下常见的几种事件。

根据面向对象思想,事件被封装成 MotionEvent 对象,由于本篇重点不在于此,所以只会涉及到几个与手指触摸相关的常见事件:

事件 简介
ACTION_DOWN 手指 初次接触到屏幕 时触发。
ACTION_MOVE 手指 在屏幕上滑动 时触发,会会多次触发。
ACTION_UP 手指 离开屏幕 时触发。
ACTION_CANCEL 事件 被上层拦截 时触发。

对于单指触控来说,一次简单的交互流程是这样的:

手指落下(ACTION_DOWN) -> 移动(ACTION_MOVE) -> 离开(ACTION_UP)

  • 本次事例中 ACTION_MOVE 有多次触发。
  • 如果仅仅是单击(手指按下再抬起),不会触发 ACTION_MOVE。

事件分发、拦截与消费

关于这一部分内容,上一篇文章 [事件分发机制原理][dispatch-touchevent-theory] 已经将流程整理的比较清楚了,本文会深入细节来研究这些内容。之所以分开讲,是为了防止大家被细节所迷惑而忽略了整体逻辑。

表示有该方法。
X 表示没有该方法。

类型 相关方法 ViewGroup View
事件分发 dispatchTouchEvent
事件拦截 onInterceptTouchEvent X
事件消费 onTouchEvent

View 相关

dispatchTouchEvent 是事件分发机制中的核心,所有的事件调度都归它管。不过我细看表格, ViewGroup 有 dispatchTouchEvent 也就算了,毕竟人家有一堆 ChildView 需要管理,但为啥 View 也有?这就引出了我们的第一个疑问。

Q: 为什么 View 会有 dispatchTouchEvent ?

A: 我们知道 View 可以注册很多事件监听器,例如:单击事件(onClick)、长按事件(onLongClick)、触摸事件(onTouch),并且View自身也有 onTouchEvent 方法,那么问题来了,这么多与事件相关的方法应该由谁管理?毋庸置疑就是 dispatchTouchEvent,所以 View 也会有事件分发。

相信看到这里很多小伙伴会产生第二个疑问,View 有这么多事件监听器,到底哪个先执行?

Q: 与 View 事件相关的各个方法调用顺序是怎样的?

A: 如果不去看源码,想一下让自己设计会怎样?

  • 单击事件(onClickListener) 需要两个两个事件(ACTION_DOWN 和 ACTION_UP )才能触发,如果先分配给onClick判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用。(最后)
  • 长按事件(onLongClickListener) 同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP,应该排在 onClick 前面。(onLongClickListener > onClickListener)
  • 触摸事件(onTouchListener) 如果用户注册了触摸事件,说明用户要自己处理触摸事件了,这个应该排在最前面。(最前)
  • View自身处理(onTouchEvent) 提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onTouchListener 后面。(onTouchListener > onTouchEvent)

所以事件的调度顺序应该是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener

下面我们来看一下实际测试结果:

手指按下,不移动,稍等片刻再抬起。

[Listener ]: onTouchListener      ACTION_DOWN
[GcsView  ]: onTouchEvent         ACTION_DOWN
[Listener ]: onLongClickListener  
[Listener ]: onTouchListener      ACTION_UP
[GcsView  ]: onTouchEvent         ACTION_UP
[Listener ]: onClickListener      

可以看到,测试结果也支持我们猜测的结论,因为长按 onLongClickListener 不需要 ACTION_UP 所以会在 ACTION_DOWN 之后就触发。

接下来就看一下源码是怎么设计的(省略了大量无关代码):

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    boolean result = false;    // result 为返回值,主要作用是告诉调用者事件是否已经被消费。
    if (onFilterTouchEventForSecurity(event)) {
        ListenerInfo li = mListenerInfo;
        /** 
         * 如果设置了OnTouchListener,并且当前 View 可点击,就调用监听器的 onTouch 方法,
         * 如果 onTouch 方法返回值为 true,就设置 result 为 true。
         */
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        /** 
         * 如果 result 为 false,则调用自身的 onTouchEvent。
         * 如果 onTouchEvent 返回值为 true,则设置 result 为 true。
         */
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ...
    return result;
}

如果觉得源码还是太长,那么用伪代码实现应当是这样的(省略若干安全判断),简单粗暴:

```
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener.onTouch(this, event)) {
return true;
} else if (onTouchEvent(event)) {
return true;
}
return false;
}

正当你沉迷在源码的"精妙"逻辑的时候,你可能没发现有两个东西失踪了,等回过神来,定睛一看,哎呦妈呀,**OnClick 和 OnLongClick 去哪里了?**

不要担心,OnClick 和 OnLongClick 的具体调用位置在 **onTouchEvent** 中,看源码(同样省略大量无关代码):

java
public boolean onTouchEvent(MotionEvent event) {
...
final int action = event.getAction();
// 检查各种 clickable
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
...
removeLongPressCallback(); // 移除长按
...
performClick(); // 检查单击
...
break;
case MotionEvent.ACTION_DOWN:
...
checkForLongClick(0); // 检测长按
...
break;
...
}
return true; // ◀︎表示事件被消费
}
return false;
}

**注意了,第一个重点要出现了(敲黑板)!**

![](http://ww3.sinaimg.cn/large/005Xtdi2jw1f8p431ehi3j304w04wmx1.jpg)

 **注意上面代码中存在一个 `return true;` 并且是只要 View 可点击就返回 true,就表示事件被消费了。**

 举个栗子: I have a **RelativeLayout**,I have a **View**,Ugh,**RelativeLayout - View** 

xml

现在你有了一个 **RelativeLayout - View** 你开开心心的为 RelativeLayout 设置了一个点击事件`myClick`,然而你会发现不论怎么点都不会接收到信息,仔细一看,发现内部的 View 有一个属性 `android:clickable="true"` 正是这个看似不起眼的属性把事件给消费掉了,由此我们可以得出如下结论:  
**1. 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。**  
 **2. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。**

关于 View 的事件分发先说这么多,下面我们来看一下 ViewGroup 的事件分发。



### ViewGroup 相关

**ViewGroup(通常是各种Layout) 的事件分发相对来说就要麻烦一些,因为 ViewGroup 不仅要考虑自身,还要考虑各种 ChildView,一旦处理不好就容易引起各种事件冲突,正所谓养儿方知父母难啊。**

#### VIewGroup 的事件分发流程又是如何的呢?

 上一篇文章 [事件分发机制原理][dispatch-touchevent-theory] 中我们了解到事件是通过ViewGroup一层一层传递的,最终传递给 View,ViewGroup 要比它的 ChildView 先拿到事件,并且有权决定是否告诉要告诉 ChildView。在默认的情况下 ViewGroup 事件分发流程是这样的。

* 1.判断自身是否需要(询问 onInterceptTouchEvent 是否拦截),如果需要,调用自己的 onTouchEvent。
* 2.自身不需要或者不确定,则询问 ChildView ,一般来说是调用手指触摸位置的 ChildView。
* 3.如果子 ChildView 不需要则调用自身的 onTouchEvent。


用伪代码应该是这样的:

java
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false; // 默认状态为没有消费过

if (!onInterceptTouchEvent(ev)) {   // 如果没有拦截交给子View
    result = child.dispatchTouchEvent(ev);
}

if (!result) {                      // 如果事件没有被消费,询问自身onTouchEvent
    result = onTouchEvent(ev);
}

return result;

}

**有人看到这里可能会有疑问,我看过源码,ViewGroup 的 `dispatchTouchEvent` 可有二百多行呢,你弄这几行就想忽悠我,别以为我读书少。**

当然了,上述源码是不完善的,还有很多问题是没有解决的,例如:

##### 1. ViewGroup 中可能有多个 ChildView,如何判断应该分配给哪一个?

这个很容易,就是把所有的 ChildView 遍历一遍,如果手指触摸的点在 ChildView 区域内就分发给这个View。

##### 2. 当该点的 ChildView 有重叠时应该如何分配?

当 ChildView 重叠时,**一般会分配给显示在最上面的 ChildView**。  
如何判断哪个是显示在最上面的呢?后面加载的一般会覆盖掉之前的,所以**显示在最上面的是最后加载的**。

如下:

xml

```

当手指点击有重叠区域时,分如下几种情况:

top Created with Sketch.