1.为什么有事件分发机制?
Android上面的View是树形结构,View可能会重叠在一起,当我们点击的地方有多个View都可以响应的时候,这个点击事件应该给谁呢?为了解决这个问题,就有了事件分发机制。
2.事件分发相关知识
2.1 事件分发究竟是什么?
2.1.1 MotionEvent
当用户点击屏幕里View或者ViewGroup的时候,将会产生一个事件对象,这个事件对象就是MotionEvent对象,这个对象记录了事件的类型,触摸的位置,以及触摸的时间等。MotionEvent里面定义了事件的类型,其实很容易理解,因为用户可以在屏幕触摸,滑动,离开屏幕动作,分别对应MotionEvent.ACTION_DOWN,MotionEvent.ACTION_MOVE,MotionEvent.ACTION_UP;
- MotionEvent.ACTION_DOWN:用户触摸View&ViewGroup。
- MotionEvent.ACTION_MOVE:用户手指移动View&ViewGroup。
- MotionEvent.ACTION_UP:用户手指离开屏幕。
- MotionEvent.ACTION_CANCEL:事件退出了,不是用户导致的。
因此用户在触摸屏幕到离开屏幕会产生一系列事件,ACTION _ DOWN->ACTION _ MOVE(0个或者多个)->ACTION _ UP,那么ACTION _ CANCEL事件是怎么回事呢?请看下面的图你就懂的更彻底了:
对于Down,Move,Up产生的情景我们都知道,都理解,但是Cancel呢?如果你能完美的解释Calcel产生的情景,那么这可能会是一个亮点。经过笔者查阅资料,这么描述Calcel是最合理的:
当控件收到前驱事件(什么叫前驱事件?一个从DOWN一直到UP的所有事件组合称为完整的手势,中间的任意一次事件对于下一个事件而言就是它的前驱事件)之后,后面的事件如果被父控件拦截,那么当前控件就会收到一个CANCEL事件,并且把这个事件会传递给它的子事件。(注意:这里如果在控件的onInterceptTouchEvent中拦截掉CANCEL事件是无效的,它仍然会把这个事件传给它的子控件)之后这个手势所有的事件将全部拦截,也就是说这个事件对于当前控件和它的子控件而言已经结束了。
简单的理解产生Cancel事件的条件就是:
-
父View收到ACTION_DOWN,如果没有拦截事件,则ACTION_DOWN前驱事件被子视图接收,父视图后续事件会发送到子View。
-
此时如果在父View中拦截ACTION_UP或ACTION_MOVE,在第一次父视图拦截消息的瞬间,父视图指定子视图不接受后续消息了,同时子视图会收到ACTION_CANCEL事件。
来个例子,我们知道ViewPager如何用户在A页滑动到B页,滑动到不及一半的位置,那么ViewPager就会给用户回退到A页,这是ViewPager的Cancel事件处理的。
ViewPager的onTouchEvent对ACTION_CANCEL的处理:
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged) {
scrollToItem(mCurItem, true, 0, false);
mActivePointerId = INVALID_POINTER;
endDrag();
needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
}
break;
拿ViewPager来说,在ScrollView包含ViewPager的情况下,对ViewPager做左右滑动,滑到一页的一半时往上下滑,ViewPager收到MotionEvent.ACTION_CANCEL后就能够回到先前那一页,而不是停在中间。
关于MotionEvent其它的认识,请看以下博客:
https://blog.csdn.net/vansbelove/article/details/78416791
2.1.2 事件分发的本质(定义)
其实事件分发的本质将点击屏幕产生的MotionEvent对象传递到某个具体的View然后处理消耗这个事件的整个过程。
2.1.3 事件怎么产生&事件分发产生的事件在哪些对象之间传递
事件是怎么产生的?当用户触摸,滑动,离开屏幕时,这个时候就产生了事件,Android系统将事件封装为MotionEvent对象,这个对象里包含事件的类型,事件触发的时间,以及触摸在屏幕的哪个位置等。那么这个事件MotionEvent对象在哪些对象之间传递呢?答案就是Activity&ViewGroup&View。
2.1.4 三个重要的有关事件分发的方法
a.dispatchTouchEvent
用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级的dispatchTouchEvent方法影响,表示是否消耗此事件。
b.onInterceptTouchEvent
在上述方法dispatchTouchEvent内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
c.onTouchEvent
同样也会在dispatchTouchEvent内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
接下来,我们来看看究竟这三个方法有着怎样的关系呢?请看下面的伪代码:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;//记录返回值
if(onInterceptTouchEvent(ev)){//判断是否拦截此事件
consume = onTouchEvent(ev);//如果当前确认拦截此事件,那么就处理这个事件
}else{
consume = child.dispatchToucnEvent(ev);//如果当前确认不拦截此事件,那么就将事件分发给下一级
}
return consume;
}
通过上述伪代码,我们可以得知点击事件的传递规则:对于一个根ViewGroup而言,点击事件产生后,首先会传递给它,这时它的dispatchTouch就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前的事件,接着事件就会交给这个ViewGroup处理,即它的onTouch方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此直到事件被最终处理。
当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调。这时事件处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么当前View的onTouchEvent方法不会被调用。由此可见,给View设置的onTouchListener的优先级比onTouchEvent要高。在onTouchEvent方法中,如果当前设置的有onClickListener,那么它的onClick方法会被调用。可以看出,平时我们常用的OnClickListener,其优先级最低,即处于事件传递的尾端。
当一个点击事件产生后,它的传递过程遵循如下顺序:Activity–>Window–>View,即事件总数先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View,顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依次类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理, 即Activity的onTouchEvent方法会被调用。这个过程其实很好理解,我们可以换一种思路,假设点击事件是一个难题,这个难题最终被上级领导分给了一个程序员去处理(这是事件分发过程),结果这个程序员搞不定(onTouchEvent返回了false),现在该怎么办呢?难题必须要解决,那就只能交给水平更高的上级解决(上级的onTouchEvent被调用),如果上级再搞不定,那就只能交给上级的上级去解决,就这样难题一层层地向上抛,这是公司内部一种常见的处理问题的过程。
2.1.5 事件分发的顺序
关于事件传递机制需要注意以下:
(1)同一见事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件的序列以down开始,中间含有数量不定的move事件,最终以up事件结束。
(2)正常情况下,一个事件序列只能被一个View拦截且消耗。这一条的原因可以参考(3),因为一旦一个元素拦截了某个事件,那么同一个事件序列的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如
一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
(3)某个View一旦决定拦截,那么这个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否拦截了。
(4)某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一件序列中的其他事件都不会再交给它处理,并且事件 将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短时间内上级就不敢再把事件交给这个程序员做了,二者是类似的道理。
(5)如果View不消耗ACTION_DOWN以外的事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
(6)ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。
(7)View没有onInterceptTouchEvent方法,一旦点击事件传递给它,那么它的onTouchEvent方法就会被调用。
(8)View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。
(9)View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
(10)onClick会发生的前提是当前View是可点击的,并且它接收到了down和up事件。
(11)事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
2.2 监听事件与MotionEvent事件关系
https://www.cnblogs.com/Claire6649/p/5947139.html
https://blog.csdn.net/mydreamongo/article/details/30465613
3 事件分发运用在哪里?
- 自定义View,这点不用笔者多说吧!自定义View里需要具备很多知识,像View绘制机制,Android坐标体系等,Android动画机制,内存管理等等,当然还包括事件分发啦。
- 解决项目中各种事件冲突问题,我相信你在平时的项目中没少处理事件冲突问题,在解决这些问题时,我们需要查阅资料,博客等。 https://www.jianshu.com/p/8bc0765dffc9
最后读者的推荐的大神好博文:
- https://www.jianshu.com/p/e99b5e8bd67b