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 |
|
稍微解释一下上述伪代码,对于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区域但事件被ViewGroupB拦截
注意:上图中ViewGroupB的onInterceptTouchEvent方法返回true后,应该是由dispatchTouchEvent来调用onTouchEvent方法。这里分两种情况:如果拦截的事件是初始事件,也就是ACTION_DOWN事件的话,那么ViewGroupB就会调用自己的onTouchEvent方法自己来处理事件;如果拦截的事件不是初始事件的话,它就会把事件交还给Activity来处理,如果Activity.onTouchEvent也不处理的话就抛弃。
事件分发机制设计到到情形非常多,这里就不一一列举了,记住以下几条原则就行了。
- 如果事件被消费,就意味着事件信息传递终止。
- 如果事件一直没有被消费,最后会传给Activity,如果Activity也不需要就被抛弃。
- 判断事件是否被消费是根据返回值,而不是根据你是否使用了事件。
事件相关方法调用顺序
我们知道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 |
|
onClick和onLongClick在onTouchEvent()方法中执行:
1 |
|
结论与分析
(1)同一个事件序列(gesture)是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。
(2)正常情况下,一个事件序列只能被一个View拦截且消耗。
(3)某个View一旦决定拦截,那么这个事件序列都只能由它来处理(如果事件序列能传递给它的话),并且它的onInterceptTouchEvent不会再被调用。
ViewGroup中dispatchTouchEvent()方法源码中有一段描述的是否进行拦截,如下:
1 |
|
从上述源码可以看出,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 |
|
可以看到当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事件除外。
参考:
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!