Android触摸事件分发机制

为什么要进行事件分发

android中View是树形结构的,view可能会重叠,当我们点击某个区域时,如下图中的View,由于Activity、ViewGroupA、ViewGroupB和View都能对触摸事件进行响应,系统无法确定该事件交由谁处理,这就需要事件分发机制来帮忙。

什么是触摸事件

官方文档中的描述:

Motion events describe movements in terms of an action code and a set of axis values. The action code specifies the state change that occurred such as a pointer going down or up. The axis values describe the position and other movement properties.

动作事件根据操作代码和一套坐标轴值来描述动作/运动。操作代码指定发生的状态更改,例如指针向下或向上。坐标轴值描述位置和其他动作属性。

事件通常被封装成MotionEvent对象。

常用事件

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

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

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

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

事件分发、拦截与消费

主要涉及三个方法:

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

Activity和View中都是没有事件拦截,这是因为:

Activity 作为原始的事件分发者,如果 Activity 拦截了事件会导致整个屏幕都无法响应事件,这肯定不是我们想要的效果。
View最为事件传递的最末端,要么消费掉事件,要么不处理进行回传,根本没必要进行事件拦截。

public boolean dispatchTouchEvent(MotionEvent ev)

用来进行事件的分发。如果事件能够传递给当前View,那么首先就会调用此方法,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消费该事件。

public boolean onInterceptTouchEvent(MotionEvent ev)

dispatchTouchEvent方法中调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么该事件不会再继续传递,此方法不会被再次调用,返回结果表示是否拦截某事件。

public boolean onTouchEvent(MotionEvent event)

dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

上述三个方法有何区别?又有何联系?可以用如下伪代码表示:

1
2
3
4
5
6
7
8
9
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}

稍微解释一下上述伪代码,对于ViewGroupA来说,点击事件产生后,首先会传递给它(Activity -> (PhoneWindow -> DecorView) -> ViewGroupA),这时它的dispatchTouchEvent()方法就会被调用。若ViewGroupA的onInterceptTouchEvent()返回true就表示它要拦截当前事件,接着事件就会被传递到ViewGroupA的onTouchEvent()方法中;若ViewGroupA的onInterceptTouchEvent()返回false则表示不拦截当前事件,这个事件就会传递给子视图ViewGroupB,接着ViewGroupB的dispatchTouchEvent()方法就会被调用。


下面我们简单地用几种特殊情况来理解事件分发。

点击View区域但没有任何View消费事件

无任何事件消费(简化)

注意:上图中onInterceptTouchEvent方法返回false后直接调用了子View的dispatchTouchEvent,实际上是ViewGroup的dispatchTouchEvent方法根据onInterceptTouchEvent方法的返回值调用的子View的dispatchTouchEvent方法。后面的图中也是一样的。

点击View区域且事件被View消费

View消费了事件(简化)

点击View区域但事件被ViewGroupB拦截

ViewGroupB拦截了事件

注意:上图中ViewGroupB的onInterceptTouchEvent方法返回true后,应该是由dispatchTouchEvent来调用onTouchEvent方法。这里分两种情况:如果拦截的事件是初始事件,也就是ACTION_DOWN事件的话,那么ViewGroupB就会调用自己的onTouchEvent方法自己来处理事件;如果拦截的事件不是初始事件的话,它就会把事件交还给Activity来处理,如果Activity.onTouchEvent也不处理的话就抛弃。

事件分发机制设计到到情形非常多,这里就不一一列举了,记住以下几条原则就行了。

  1. 如果事件被消费,就意味着事件信息传递终止。
  2. 如果事件一直没有被消费,最后会传给Activity,如果Activity也不需要就被抛弃。
  3. 判断事件是否被消费是根据返回值,而不是根据你是否使用了事件。

事件相关方法调用顺序

我们知道View可以注册很多监听器,例如单击事件onClick、长按事件onLongClick、触摸事件onTouch,并且View自身也有onTouchEvent()方法,那么这么多监听器到底哪个先执行呢?

如果我们认真思考一下的话,不需要看源码也能猜出来。

单击事件onClickListener需要两个事件ACTION_DOWN和ACTION_UP才能触发,如果先分配给它判断会导致其他事件阻塞,显然是不合理的,应该放到最后。

长按事件onLongClickListener只需要一个事件ACTION_DOWN就能触发,它应该比单击事件更早处理,但是长按也需要长时间等待(相对来说)才能触发,所以应该靠后。

触摸事件onTouchListener与onTouchEvent方法的区别是触摸事件是交由用户自己处理的,所以应该在最前面,同时会覆盖掉onTouchEvent。

View自身处理onTouchEvent是默认的一种处理方式,如果用户决定自己处理,也就不需要View自身来处理了,所以顺序应该在触摸事件后面。

这样的话我们得出了事件方法调用顺序:onTouchListener > onTouchEvent(可能不执行) > onLongClickListener > onClickListener

查看View中的dispatchTouchEvent()方法源码也能找到结果:

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
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;
}

onClick和onLongClick在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
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(); // 移除长按
...
performClickInternal(); // 检查单击,返回performClick()方法的执行结果
...
break;
case MotionEvent.ACTION_DOWN:
...
checkForLongClick(0,x,y); // 检测长按
...
break;
...
}
return true; // 表示事件被消费
}
return false;
}

结论与分析

(1)同一个事件序列(gesture)是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。

(2)正常情况下,一个事件序列只能被一个View拦截且消耗。

(3)某个View一旦决定拦截,那么这个事件序列都只能由它来处理(如果事件序列能传递给它的话),并且它的onInterceptTouchEvent不会再被调用。

ViewGroup中dispatchTouchEvent()方法源码中有一段描述的是否进行拦截,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Check for interception.
final boolean intercepted;
// 若当前事件为ACTION_DOWN或者子元素已经处理了事件,判断是否拦截
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,并且没有目标来处理该事件(子元素没有处理初始事件),直接拦截
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}

从上述源码可以看出,ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget不为null。前者我们知道,那mFirstTarget是什么呢?当事件由ViewGroup的子元素成功处理时,mFirstTarget会被指向子元素,也就是说当ViewGroup不拦截事件并且子元素成功处理时,mFirstTouchTarget != null成立。

现在我们模拟一种情况,当手指点击在ViewGroup上时,产生一个ACTION_DOWN事件,而当前ViewGroup是允许拦截的(FLAG_DISALLOW_INTERCEPT标志位为false),那么就会调用onInterceptTouchEvent方法判断是否拦截,若返回false,则ViewGroup就不会拦截当前事件,并交给子View进行处理。子View处理完毕后,mFirstTouchTarget会被赋值指向子View。接着手指移动一段距离并松开,产生ACTION_MOVE和ACTION_UP事件,由于此时mFirstTouchEvent != null成立,所以继续判断是否拦截,若ViewGroup决定拦截ACTION_MOVE,那么mFirstTouchEvent会被重置为null,当ACTION_UP被分发的时候,两个条件都不满足则直接进入循环体的else代码语句中,不再调用onInterceptTouchEvent()方法,直接将拦截标志设为true。

现在用代码来验证一下,在View中消费ACTION_DOWN事件,然后父ViewGroupB拦截ACTION_MOVE,打印日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
D/Activity: dispatchTouchEvent    ACTION_DOWN
D/ViewGroupA: dispatchTouchEvent ACTION_DOWN
D/ViewGroupA: onInterceptTouchEvent ACTION_DOWN
D/ViewGroupB: dispatchTouchEvent ACTION_DOWN
D/View: dispatchTouchEvent ACTION_DOWN
D/View: onTouchEvent 消费事件(return true
D/Activity: dispatchTouchEvent ACTION_MOVE
D/ViewGroupA: dispatchTouchEvent ACTION_MOVE
D/ViewGroupA: onInterceptTouchEvent ACTION_MOVE
D/ViewGroupB: dispatchTouchEvent ACTION_MOVE
D/ViewGroupB: onInterceptTouchEvent 拦截ACTION_MOVE
D/View: dispatchTouchEvent ACTION_CANCEL
D/View: onTouchEvent CANCEL
D/Activity: onTouchEvent ACTION_MOVE
D/Activity: dispatchTouchEvent ACTION_UP
D/ViewGroupA: dispatchTouchEvent ACTION_UP
D/ViewGroupA: onInterceptTouchEvent ACTION_UP
D/ViewGroupB: dispatchTouchEvent ACTION_UP
D/Activity: onTouchEvent ACTION_UP

可以看到当ACTION_DOWN被View消费之后就不会继续传递了,这时的mFirstTouchTarget指向View,而后续事件ACTION_MOVE被ViewGroupB拦截后,View会收到一个ACTION_CANCEL事件,表示这个事件序列的后续处理被取消。ACTION_MOVE事件被拦截后由于没有被消费,就不会再继续传递了,而是直接返回给Activity来处理,后续的ACTION_UP事件也会被ViewGroupB拦截,但这时onInterceptTouchEvent方法并没有被再次调用。

(4)某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它处理,并且事件将重新交给它的父元素去处理,即父元素的onTouchEvent会被调用。

(5)如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续事件,最终这些消失的点击事件会传递给Activity处理。

(6)ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent默认返回false。

(7)如果ViewGroup拦截了初始事件(ACTION_DOWN),那么它就会调用自己onTouchEvent方法来处理事件,如果拦截的不是初始事件,那么它不会自己处理,而是将事件返回给Activity。

(8)如果View当前处理的事件被上层ViewGroup拦截,View会收到一个ACTION_CANCEL事件,后续事件不会再传递过来。

(9)View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。

(10)View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable属性同时为false)。View的longClickable属性默认都为false,而clickable属性要看子类具体实现。

(11)View的enable属性不影响onTouchEvent的默认返回值。

(12)onClick会发生的前提是View是可点击的(clickable),并且它收到了down和up事件。

(13)事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。

参考:

MotionEvent

《Android开发艺术探索》

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

Android事件传递机制分析

Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

Android事件分发机制完全解析,带你从源码的角度彻底理解(下)


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!