事件分发
事件分发是Android View体系里非常重要的知识点,熟悉事件分发之后可以自定义出各种交互复杂的View,也可以解决开源库中的各种滑动冲突bug。下边开始具体的实验和分析。
创建了2个自定义的ViewGroup:GrandFatherLayout继承自LinearLayout,FatherLayout继承自RelativeLayout。创建了一个自定义View(MyButton)继承自Button。3个View中,只在dispatchTouchEvent
、onInterceptTouchEvent
、onTouchEvent
中增加了打印日志的代码,其他未做修改。将MyButton放在FatherLayout中,将FatherLayout放在GrandFatherLayout中,视图层次结构如下:
打印日志相关代码类似如下代码:
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.e("zx", "GrandFatherLayout-onTouchEvent-DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e("zx", "GrandFatherLayout-onTouchEvent-MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e("zx", "GrandFatherLayout-onTouchEvent-UP");
break;
default:
break;
}
return super.onTouchEvent(event);
}
未修改任何方法返回值时,点击按钮,打印日志如下:
2.jpg
从以上结果可以总结出:
事件是以ACTION_DOWN开始,中间有若干个ACTION_MOVE,最后以ACTION_UP结束
。事件是从视图树上由上往下传递,调用顺序为dispatchTouchEvent->onInterceptTouchEvent->onTouchEvent
。
onInterceptTouchEvent
ViewGroup
的特有方法,表示是否拦截事件,默认不拦截。
如果在FatherLayout的onInterceptTouchEvent()
中返回true,事件流如下所示:
3.jpg
如果在FatherLayout的onInterceptTouchEvent()
中返回true,同时onTouchEvent()
也返回true,事件流如下所示:
4.jpg
从结果中可以总结出:
-
onInterceptTouchEvent()返回true时,代表ViewGroup拦截此事件,此ViewGroup的onTouchEvent()会被调用,子View将无法收到事件流
。 -
onInterceptTouchEvent()
返回true时,后续事件都不会再调用它的onInterceptTouchEvent()
,而是直接调用它的onTouchEvent()
。通俗点讲就是:如果事件被当前ViewGroup拦截,后续的一系列事件,一直到ACTION_UP(本次事件流结束),都是由它来处理,不会再询问是否拦截
。
至于为什么onTouchEvent()
返回true时,事件流改变了,将会在onTouchEvent中解释。
增加条件,如果在FatherLayout的onInterceptTouchEvent()
的Down事件中返回false,Move事件中返回true,也就是不拦截按下事件,但是拦截移动事件。同时onTouchEvent()
也返回true,事件流如下所示:
6.jpg
注意绿色箭头处的日志,FatherLayout的子View收到了ACTION_CANCEL事件。总结出:父级View拦截事件流时,如果同一事件流中,拦截之前已经有事件到达了子View中,那么子View会收到ACTION_CANCEL事件,用于子View结束事件流(例如:按钮将按下的状态恢复)
。
onTouchEvent
实际的事件处理逻辑,返回值代表是否消耗当前事件。
上边onInterceptTouchEvent的测试中,onTouchEvent()
返回false,打印出如下结果:
4.jpg
onTouchEvent()
返回true,打印出如下结果:
4.jpg
从结果中可以总结出:
-
onTouchEvent()返回true时,事件会被消耗掉,不会向上交给父级的onTouchEvent()
。 -
onTouchEvent()返回false时,事件向上交给父级的onTouchEvent(),父级再交给父级的父级,层层向上,直到被消耗掉(也就是onTouchEvent()返回true)
。
修改一下,让onTouchEvent()
ACTION_DOWN事件时返回true,ACTION_MOVE和ACTION_UP事件返回false。打印结果如下:
7.jpg
结果跟onTouchEvent()
中全部返回true相比,区别只是绿色箭头指向的部分。事件没有向上交给父级,也就是说事件被消耗了。同时ACTION_MOVE和ACTION_UP事件没有被消耗时,事件不会交给父级而是直接抛给了Activity。总结:
-
只需要在onTouchEvent()的ACTION_DOWN事件中返回true即可消耗事件
。 -
View不消耗ACTION_MOVE和ACTION_UP事件时,事件将不经过父级,而是直接抛给Activity
。
如果反过来,让onTouchEvent()
ACTION_DOWN事件时返回false,ACTION_MOVE和ACTION_UP事件返回true。打印结果如下:
8.jpg
这个结果与前边onTouchEvent()
事件全部返回false的结果一模一样(onTouchEvent章节一开始的例子中没有打印Activity的日志,如果打印了,结果是一致的),都是将ACTION_DOWN事件逐级向上交给父级,如果向上传递的过程中没有被消耗,就会一直传递给Activity。并且,如果没有消耗ACTION_DOWN事件,那么设置的是否消耗后续事件也就不生效了,后续事件与它以及它的父级就无缘了,后续事件会被直接抛给Activity。
除了onTouchEvent()
之外,View还可以通过setOnTouchListener中的onTouch()
响应事件,二者的区别是:
-
onTouchEvent()
既可以是全局(整个Activity)的监听,又可以是单个View的监听,而setOnTouchListener的onTouch()
则是针对某个具体的View的监听,并且需要提前setOnTouchListener才能被回调。 -
onTouch()
的优先级比onTouchEvent()
高,当onTouch()
返回值是true(事件被消耗)时,onTouchEvent()
将不会被执行;当onTouch()
返回值是false(事件未消耗)时,onTouchEvent()
方法才会被执行。需要额外说明的是,如果
onTouchEvent()
方法不会被执行时,依赖于onTouchEvent()
的监听都不会被执行,这些监听有如下几种:OnClickListener的onClick()(在onTouchEvent的ACTION_UP事件中执行的)、OnLongClickListener的onLongClick(),GestureDetector.OnGestureListener的一系列事件、GestureDetector.OnDoubleTapListener的一系列事件。
dispatchTouchEvent
用来进行事件的分发。如果事件能够传递给当前view,那么此方法一定会被调用,返回结果表示是否消耗当前事件。
View的dispatchTouchEvent()
逻辑如下:
public boolean dispatchTouchEvent(MotionEvent event) {
//如果setOnTouchListener了,并且onTouch返回true,就返回true
if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {
return true;
}
// 如果没有setOnTouchListener了,或者onTouch返回false,就调用onTouchEvent,
// 并将onTouchEvent的返回值作为dispatchTouchEvent的返回值返回。
return onTouchEvent(event);
}
这是伪代码,源代码比这个多。这个伪代码中可以看出:如果View setOnTouchListener了,就先调用OnTouchListener的onTouch(),如果onTouch()返回true,dispatchTouchEvent就返回true,代表事件被消耗。如果onTouch()返回false,代表onTouch()没有消耗事件,就继续执行onTouchEvent(),并将onTouchEvent的返回值作为dispatchTouchEvent的返回值返回。
这段伪代码,也证明了之前onTouchEvent()与onTouch()的区别。
ViewGroup的dispatchTouchEvent()
伪代码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onInterceptTouchEvent(ev)) {
handled = onTouchEvent(ev);
} else {
handled = child.dispatchTouchEvent(ev);
}
return handled;
}
如果ViewGroup的onInterceptTouchEvent()方法返回true就表示它要拦截当前事件,接着事件就会交给这个 ViewGroup处理,即它的 onTouchEvent方法就会被调用(此时时间ViewGroup等同于View,如果有OnTouchListener也会优先被执行);如果这个ViewGroup的onInterceptTouchEvent()方法返回false,就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent()方法就会被调用,如此反复直到事件被最终消耗。
requestDisallowInterceptTouchEvent
这个方法一般在子View中调用,通过调用getParent().requestDisallowInterceptTouchEvent(true)
,阻止父级ViewGroup对其MOVE或者UP事件进行拦截。requestDisallowInterceptTouchEvent()
只对当前事件流有效,无法一直起作用,当下一个事件流的Down事件到来时,父级ViewGroup会将拦截状态FLAG_DISALLOW_INTERCEPT重置,所以requestDisallowInterceptTouchEvent()无法影响父级ViewGroup对Down事件的处理,由于拦截状态重置,上次时间流调用的requestDisallowInterceptTouchEvent()
就失效了,如果想再次请求父组件不拦截,需要再调用一次requestDisallowInterceptTouchEvent()
。requestDisallowInterceptTouchEvent()
通常用于解决滑动冲突。
滑动冲突
这一部分直接引用任玉刚大佬的《Android开发艺术探索》,过于经典,所以就原封不动借鉴(程序员的事情,怎么能叫抄呢)过来了。
外部拦截法
所谓外部拦截法是指点击事情都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,这种方法的伪代码如下所示。
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (父容器需要当前点击事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
上述代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不需做修改并且也不能修改。这里对上述代码再描述一下,在onInterceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递给子元素了;其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false;最后是ACTION_UP事件,这里必须要返回false,因为ACTION_UP事件本身没有太多意义。 考虑一种情况,假设事件交由子元素处理,如果父容器在ACTION_UP时返回了true,就会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的onClick事件就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而ACTION_UP作为最后一个事件也必定可以传递给父容器,即便父容器的onInterceptTouchEvent方法在ACTION_UP时返回了false。
内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法稍显复杂。它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此类点击事件)){
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
上述代码是内部拦截法的典型代码,当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动而且也不能有改动。除了子元素需要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用parent.requestDisal-lowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。 为什么父容器不能拦截ACTION_DOWN事件呢?那是因为ACTION_DOWN事件并不受FLAG_DISALLOW_INTERCEPT这个标记位的控制,所以一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中去,这样内部拦截就无法起作用了。父元素所做的修改如下所示。
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
自定义View时触摸事件处理
自定义View或者ViewGroup时,可以按照以下步骤处理触摸事件:
-
重写
onTouchEvent()
,在里面写上触摸反馈逻辑,比如响应拖动等等,并返回true
(关键是ACTION_DOWN
事件时返回true
)。 -
如果是会发生触摸冲突的
ViewGroup
,还需要重写onInterceptTouchEvent()
,在ACTION_DOWN时返回false
,并在确认接管事件流时返回一次true
,以实现对事件的拦截。 -
当子 View 临时需要阻止父 View 拦截事件流时,可以调用父 View 的
requestDisallowInterceptTouchEvent(true)
,通知父 View 在当前事件流中不再通过onInterceptTouchEvent()
来拦截。
手势事件监听
用于监听用户的单击、滑动、长按、双击等行为,主要用到系统提供的GestureDetector
和3种Listener。
使用步骤如下:
-
按照自己需求,实现3种Listener中的一种。
-
使用上一步实现的Listener创建
GestureDetector
。 -
接管目标View的
onTouchEvent()
或者OnTouchListener的onTouch()
方法,并将GestureDetector的onTouchEvent()
的返回值返回,如下所示://接管onTouchEvent @Override public boolean onTouchEvent(MotionEvent event) { return gestureDetector.onTouchEvent(event); } //接管OnTouchListener的onTouch view.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return gestureDetector.onTouchEvent(event); } });
下边是3种Listener的介绍。
监听器 | 功能 |
---|---|
OnGestureListener | 手势检测,有以下类型事件:按下(onDown)、 触摸反馈(onShowPress) 、单击抬起(onSingleTapUp)、滚动(onScroll)、长按(onLongPress)、快速滑动(onFling) |
OnDoubleTapListener | 检测双击,有三个类型事件:双击(onDoubleTap)、单击确认(onSingleTapConfirmed) 和 双击事件回调(onDoubleTapEvent) |
SimpleOnGestureListener | 上述接口的空实现,使用这个时,不必实现接口的所有方法,比较方便 |
需要特别说明的2点:
设置了手势监听后,OnClickListener的onClick()将不会被执行
。使用接管onTouchEvent这种方式时,由于onTouchEvent直接被接管了,View类中自带的onTouchEvent里的代码都不会被执行,所以onClick()无法被执行;使用接管OnTouchListener的onTouch这种方式时,由于onTouch的优先级比onTouchEvent高,所以onTouchEvent不会被执行,而onClick()依赖于onTouchEvent,所以也不会被执行。- 连续两次点击,DoubleTap和SingleTapConfirmed只有一个会被执行,也就是说,要么是单击,要么双击。原因是SingleTapConfirmed会在第一次点击后等待,等时间超过了双击事件最长间隔后才回调(如果没有第二次点击),此时就绝对不会是双击事件,这也是Confirmed的意思。而OnGestureListener的onSingleTapUp就不会严格区分,双击事件第一次抬起时,onSingleTapUp就会被执行。
简单理解
事件分发的整个流程,非常像公司里任务分配的流程。
- 任务总是由老板往下分派(Activity的dispatchTouchEvent),并且老板不会主动接某个具体的任务(Activity没有onInterceptTouchEvent)。
- 任务到达中层领导时,中层领导会进行分派(ViewGroup的dispatchTouchEvent),任务分派过程中会首先考虑自己是否要接这个任务(ViewGroup的onInterceptTouchEvent)。一般情况下,中层领导也不接这个任务(ViewGroup的onInterceptTouchEvent默认返回false),而是向下分派给下级(child.dispatchTouchEvent)。有时候,中层领导由于某些原因需要自己亲自接手这个任务(ViewGroup的onInterceptTouchEvent返回true),他就会亲自解决任务(执行onTouchEvent),此时任务就不会分配给他的下级(子View无法收到事件流)。如果任务解决了(onTouchEvent返回true),任务就不会再交给他的领导(父级onTouchEvent不会被执行),如果任务没解决(onTouchEvent返回false),任务就会交给他的领导,由他的领导继续处理(父级onTouchEvent会被执行)。如果这个中层领导是个光杆司令(没有子View的ViewGroup),那么任务就得由他自己来处理(执行onTouchEvent)。如果任务经过层层的领导,这些领导都没有接手(ViewGroup都没有拦截),最终任务会会被分配给底层的程序员(View)。
- 程序员如果解决了这个任务(View的onTouchEvent返回true),那么任务就终结了(事件流结束)。如果程序员没能解决这个问题,那么就只能把任务继续向上交给上级领导(父级ViewGroup的onTouchEvent会被处理),领导如果解决了,任务结束,领导如果没解决,就会继续向上交给领导的领导。如果任务一级一级交上去,始终没有被解决,那么任务最终会被丢给老板(Activity执行onTouchEvent)。
- 有时候程序员会主动告诉领导某某类型的任务你就交给我吧(parent.requestDisallowInterceptTouchEvent(true)),此时领导就会直接将任务交给程序员(View的onTouchEvent被执行),当程序员处理了一部分后觉得剩余的任务由领导处理更合适时(达到父容器的拦截条件),就会告诉父级接下来的任务由你来继续吧(parent.requestDisallowInterceptTouchEvent(false)),然后父级接手,继续处理任务,直到任务被解决。
这个例子可以方便理解事件分发流程,但是这个例子会丢失一些细节,所以这个例子只适合初步理解,初步理解之后还是应该看源码和写验证代码来学习其中的细节。