在介绍点击事件的传递规则之前,首先我们要明白这里分析的对象就是 MotionEvent ,即点击事件。所谓的点击事件的事件分发,其实就是对 MotionEvent 事件的分发过程,即当一个 MotionEvent 产生了以后,系统需要把这个事件传递给一个具体的 View ,而这个传递的过程就是分发过程
。点击事件的分发过程由三个很重要的方法以递归的方式共同完成:
递归的含义:以 down 事件为例,down 事件的处理实际上经历了一下一上的两个过程,下是指:从最外层到最内层的 onInterceptTouchEvent ,上是指:从最内层到最外层的 onTouchEvent,当然,任何一步的事件传递的方法返回 true ,都能阻止它继续传播。
类型 | 相关方法 | Activity | ViewGroup | View |
---|---|---|---|---|
事件分发 | dispatchTouchEvent | √ | √ | √ |
事件拦截 | onInterceptTouchEvent | × | √ | × |
事件消费 | onTouchEvent | √ | √ | √ |
从上表可以看出 Activity 和 View 都是没有拦截事件(onInterceptTouchEvent)的,这是因为:
上述三个方法大概可以用以下伪代码表示:
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 方法会被调用。
关于事件传递的机制,常用的结论:
结合以下文章效果更佳:
可能是讲解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 会执行。
}
}
对于内部拦截法我有一个疑问:
其实这个问题就出现在 “那么父元素在拦截 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 开发艺术探究 ——任玉刚