View 的事件分发机制
在介绍点击事件的传递规则之前,首先我们要明白这里分析的对象就是 MotionEvent ,即点击事件。所谓的点击事件的事件分发,其实就是对 MotionEvent 事件的分发过程,即当一个 MotionEvent 产生了以后,系统需要把这个事件传递给一个具体的 View ,而这个传递的过程就是分发过程
。点击事件的分发过程由三个很重要的方法以递归的方式共同完成:
- public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发。如果事件能够传递给当前 View ,那么此方法一定会被调用,返回结果受当前 View 的 onTouchEvent 和 下级 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。 - public boolean onInterceptTouchEvent(MotionEvent ev)
在上述方法内部调用,用来判断是否拦截某个事件,如果当前 ViewGroup(只存在 ViewGroup中) 拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
如果 onInterceptTouchEvent 方法对 down 事件返回 false ,后续的事件(多个move 和 up)依然会传递给它们的 onInterceptTouchEvent 方法,这一点和 onTouchEvent 的行为是不一样的。
如果onInterceptTouchEvent方法对 down 事件返回 true(这表示当前控件拦截此事件序列),后续的事件(多个move和up)则不会在执行 onInterceptTouchEvent 方法,然后会执行 onTouchEvent 方法。如果onTouchEvent 方法返回 true 表示消费此事件,返回 false 表示不消费此事件。 - public boolean onTouchEvent(MotionEvent ev)
在 dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。
假如 down 事件传给 view 的 onTouchEvent 方法时,它返回了 false ,down 事件会继续向上传递给父 View 的 onTouchEvent ,即使它们在 onInterceptTouchEvent 方法中说它们不想拦截这个 down 事件,但是么有办法,没有子 view 愿意处理该事件。
递归的含义:以 down 事件为例,down 事件的处理实际上经历了一下一上的两个过程,下是指:从最外层到最内层的 onInterceptTouchEvent ,上是指:从最内层到最外层的 onTouchEvent,当然,任何一步的事件传递的方法返回 true ,都能阻止它继续传播。
类型 | 相关方法 | Activity | ViewGroup | View |
---|---|---|---|---|
事件分发 | dispatchTouchEvent | √ | √ | √ |
事件拦截 | onInterceptTouchEvent | × | √ | × |
事件消费 | onTouchEvent | √ | √ | √ |
从上表可以看出 Activity 和 View 都是没有拦截事件(onInterceptTouchEvent)的,这是因为:
- Activity 作为原始的事件分发者,如果 Activity 拦截了事件会导致整个屏幕无法响应事件,这不是我们需要的结果
- View 作为事件传递的最末端,要么消费事件,要么不处理进行回传,根本没有必要进行事件拦截。
上述三个方法大概可以用以下伪代码表示:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
上述伪代码已将三者的关系表现的淋漓尽致。通过上面的伪代码,我们也可以大致了解点击事件的传递规则:对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这时它的 dispatchTouchEvent 就会被调用,如果这个 ViewGroup 的 onInterceptTouchEvent 返回 true 就表示它要拦截当前事件,接着事件就会交给这个 ViewGroup 处理,即它的 onTouchEvent 方法就会被调用;如果这个 ViewGroup 的 onInterceptTouchEvent 方法返回 false 就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的 dispatchTouchEvent 方法就会被调用,如此反复直到事件被最终处理。
当一个 View 需要处理事件时,如果它设置了 OnTouchListener ,那么 OnTouchListener 中的 onTouch 方法会被回调。这时事件如何处理还要看 onTouch 的返回值,如果返回 false ,则当前 View 的 onTouchEvent 方法会被调用;如果返回 true ,那么 onTouchEvent 方法将不会被调用。由此可见,给 View 设置的 OnTouchListener ,其优先级比 onTouchEvent 要高。在 onTouchEvent 方法中,如果当前设置的有 OnClickListener ,那么它的 onClick 方法会被调用。可以看出,平时我们常用的 OnClickListener ,其优先级最低,即处于事件传递的尾端。
总体来说,当一个点击事件产生后,它的传递过程遵循如下顺序: Activity -> Window -> View ,即事件总是先传递给 Activity ,Activity 再传递给 Window,最后 Window 在传递给顶级 View 。顶级 View 接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个 View 的 onTouchEvent 返回 false ,那么它的父容器的 onTouchEvent 将会被调用,以此类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给 Activity 处理,即 Activity 的 onTouchEvent 方法会被调用。
关于事件传递的机制,常用的结论:
- 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以 down 事件开始,中间含有数量不定的 move 事件,最终以 up 事件结束。
- 正常情况下,一个事件序列只能被一个 View 拦截且消耗。因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别有两个 View 同时处理,但是通过特殊手段可以做到,比如一个 View 将本该自己处理的事件通过 onTouchEvent 强行传递给其他 View 处理。
- 某个 View 一旦决定拦截,那么一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的 onInterceptTouchEvent 不会再被调用。
- 某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件(onTouchEvent 返回了false),那么同一事件序列中的其他事件不会再交给它来处理,并且事件将重新交给它的父元素去处理,即父元素的 onTouchEvent 会被调用。
- 如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,这时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。
- ViewGroup 默认不拦截任何事件。Android 源码中 ViewGroup 的 onInterceptTouchEvent 方法默认返回 false 。
- View (不含ViewGroup)没有 onInterceptTouchEvent 方法,一旦点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。
- View 的 onTouchEvent 默认都会消耗事件(返回 true),除非它是不可点击(clickable 和 longClickable 同时为 false)。View 的 longClickable 属性默认都是为 false ,clickable 属性要分情况,比如 Button 的 clickable 属性默认为 true ,而 TextView 的 clickable 默认为 false 。
- View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个 View 是 disable 状态的,只要它的 clickable 或者 longClickable 有一个为 true,那么它的 onTouchEvent 就返回 true。
- onClick 会发生的前提是当前 View 是可点击的,并且它收到了 down 和 up 的事件。
- 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再有父元素分发给子View ,通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。
结合以下文章效果更佳:
可能是讲解Android事件分发最好的文章
Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
Android事件分发机制 详解攻略,您值得拥有
下面来说一个比较重要的方法requestDisallowInterceptTouchEvent:这个方法可以设置 FLAG_DISALLOW_INTERCEPT标记位,FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了down以外的其他点击事件。
ViewGroup.中 dispatchTouchEvent 重点源码:
/**
* 源码分析:ViewGroup.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
// ..................省略
// ViewGroup会在down事件到来时做重置状态的操作,而在resetTouchState方法中会对FLAG_DISALLOW_INTERCEPT进行重置,
//因此子View调用requestDisallowInterceptTouchEvent方法并不能影响ViewGroup对down事件的处理
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// ViewGroup 分两种情况下会判断要拦截当前事件:1、事件类型为 down 2、和 mFirstTouchTarget != null
//1、只要是down事件就拦截
//2、mFirstTarget 的意思是:当事件由ViewGroup的子元素成功处理时,mFirstTarget 会被赋值并指向子元素。
//换句话说就是:ViewGroup不拦截事件并交由子元素处理时mFirstTouchTarget != null成立
//一旦事件由ViewGroup拦截时,mFirstTouchTarget != null就不成立,进一步来讲,当 move和up事件到来时,由于mFirstTouchTarget != null不成立,
//将导致ViewG的 onInterceptTouchEvent不会再被调用,并且同一序列的其他事件都会默认交给这个ViewGroup处理。
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
//遍历ViewGroup的所有子元素
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
//.....省略验证
//判断子元素是否能够接收到点击事件
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//判断点击事件是否坐落在子元素的区域内
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//实际上调用的就是子元素的dispatchTouchEvent方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
//.........省略
}
ViewGroup 中 onInterceptTouchEvent 的源码:
/**
* 分析:ViewGroup.onInterceptTouchEvent()
* 作用:是否拦截事件
* 说明:
* a. 返回true = 拦截,即事件停止往下传递(需手动设置,即复写onInterceptTouchEvent(),从而让其返回true)
* b. 返回false = 不拦截(默认)
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
View 中 dispatchTouchEvent 源码:
/**
* 源码分析:View.dispatchTouchEvent()
*/
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
// 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()
// 1. mOnTouchListener != null
// 2. (mViewFlags & ENABLED_MASK) == ENABLED
// 3. mOnTouchListener.onTouch(this, event)
// 下面对这3个条件逐个分析
/**
* 条件1:mOnTouchListener != null
* 说明:mOnTouchListener变量在View.setOnTouchListener()方法里赋值
*/
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
// 即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
}
/**
* 条件2:(mViewFlags & ENABLED_MASK) == ENABLED
* 说明:
* a. 该条件是判断当前点击的控件是否enable
* b. 由于很多View默认enable,故该条件恒定为true
*/
/**
* 条件3:mOnTouchListener.onTouch(this, event)
* 说明:即 回调控件注册Touch事件时的onTouch();需手动复写设置,具体如下(以按钮Button为例)
*/
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
// 若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束
// 若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event)
滑动冲突
外部拦截法
所谓外部拦截法时指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写 父容器的 onInterceptTouchEvent 方法,在内部做相应的拦截即可。
在 onInterceptTouchEvent 方法中:
down 事件必须返回 false;
move 事件根据需要来决定,如果父容器拦截返回就放回 true,否则返回 false;
up 事件 返回 false 。
伪代码:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (父容器需要拦截当前事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
return intercepted;
}
内部拦截法
内部拦截法是值父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理,这种方法和 Android 中的事件分发机制不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作,使用起来较外部拦截法稍显复杂,需要:
①重写子元素的 dispatchTouchEvent 方法:
down 事件的parent.requestDisallowInterceptTouchEvent(true);
move 事件根据需要返回parent.requestDisallowInterceptTouchEvent(false),这时父容器可以拦截事件,否则不拦截。
伪代码:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
Log.i("子元素","dispatchTouchEvent: "+ event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
Log.d(TAG, "dx:" + deltaX + " dy:" + deltaY);
if (父容器需要拦截当前事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
return super.dispatchTouchEvent(event);
}
②重写父元素也需要在 onInterceptTouchEvent 默认拦截除了 down 以外的其他事件,这样当子元素调用 parent.requestDisallowInterceptTouchEvent(false) 方法时,父元素才能继续拦截所需的事件。
伪代码:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
Log.i("父容器","onInterceptTouchEvent: "+ event);
if (action == MotionEvent.ACTION_DOWN) {
return false;//down事件传递到子元素,在子元素中开始 dispatchTouchEvent 的循环处理
} else {
return true;// move和up事件返回 true,这时本控件的onTouchEvent 会执行。
}
}
对于内部拦截法我有一个疑问:
- 父元素拦截除了 down 事件以外的其他事件,那么父元素在拦截 move 事件后,这个move 事件就让不能传递到子元素中,那么子元素的 move 事件就无法接收到被父元素拦截的 move 事件,那怎么在子元素的 move 事件里面执行 parent.requestDisallowInterceptTouchEvent(false) ?有老铁知道的留个言!
其实这个问题就出现在 “那么父元素在拦截 move 事件后” 这里了!这里的问题就已经假设了父元素已经拦截的 move 事件,其实不然!在父容器拦截之前,还有一个 dispatchTouchEvent 过程,这个父 dispatchTouchEvent 会调用 子dispatchTouchEvent ,就是在 子dispatchTouchEvent 中我们设置了 parent.requestDisallowInterceptTouchEvent(false) 和 父元素中我们已经拦截了 move 事件,这才导致父元素才能成功拦截 move 事件。
对于滑动冲突的内部拦截中,左右切换时,在父元素里处理序列事件的流程图(虚线框内表示不执行):
不知道你看到第 move 事件的执行流程会不会感觉奇怪呢?为什么第一个 move 事件不执行 onInterceptTouchEvent 方法?那是因为在 down 事件的 dispatchTouchEvent 中设置了 parent.requestDisallowInterceptTouchEvent(true) 父元素不拦截事件,就导致直接去执行子元素的dispatchTouchEvent 方法了。那为什么第二个 move 事件又拦截 move事件呢?因为在第一个 move 事件当 滑动距离水平大于竖直时,子元素 dispatchTouchEvent 设置了parent.requestDisallowInterceptTouchEvent(false) 父元素拦截事件,所以就会执行 onInterceptTouchEvent 方法。
测试源码在Github,滑动冲突外部和内部拦截
站在巨人的肩膀上:
Android 开发艺术探究 ——任玉刚