Android事件分发机制——两分钟看懂
事件分发机制相关的3个方法
- dispatchTouchEvent() 分发
- onInterceptTouchEvent() 拦截
- onTouchEvent() 消耗
ViewGroup有1.2.3
View只有1.3
ViewGroup里面的dispatchTouchEvent方法十分的复杂,下面是它的简化版源码:
public boolean dispatchTouchEvent(Motion e){
boolean result = false;
if(onInterceptTouchEvent(e)){
//如果当前View截获事件,那么事件就会由当前View处理,即调用onTouchEvent()
result = onTouchEvent(e);
}else{
//如果不截获那么交给其子View来分发
result = child.dispatchTouchEvent(e);
//如果子View不消耗,调用自己的onTouchEvent()
if(result == false){
result = onTouchEvent(e);
}
}
return result;
}
具体的用户操作示意图
图片右上方为布局示例
当用户点击途中View视图的时候,就会进行下面的流程
注:
本流程是在用户不重写dispatchTouchEvent方法的基础上的流程(一般也不需要修改)
并且省略了Activity层(Activity层在这里我们可以把它看成一个ViewGroup)
面试的时候我们只要讲出这个流程就够了
在用户点击View(这个View指代具体的Button,TextView等等)之后
- 首先会传递给ViewGroup1(可能是LinearLayout等各种布局或者容器),ViewGroup1的dispatchTouchEvent方法调用onInterceptTouchEvent,在没有重写或者返回false的情况下会调用ViewGroup2的dispatchTouchEvent方法,如果返回值是true,表示我(ViewGroup1)需要拦截这个点击事件,接下来直接调用他自己的onTouchEvent方法,不会再向下传递。
- ViewGroup2和ViewGroup1一样。
- 如果前两层都没有拦截事件,那么就会走到View层的dispatchTouchEvent,接着调用onTouchEvent。
onTouchEvent在没有重写或者返回false的情况下表示没有消耗这个点击事件会继续向上层传递给ViewGroup2,如果返回true,表明view层消耗了这个请求,直接结束整个流程。 - ViewGroup2的onTouchEvent也是和View层的一样,通过返回值判断是否消耗事件。
- ViewGroup1和ViewGroup2一样。
总结:
整个事件分发过程就像是由dispatchTouchEvent控制的递归,流程下行方向需要考虑onInterceptTouchEvent方法是否拦截请求,上行方向需要考虑onTouchEvent是否消耗事件(当然重写dispatchTouchEvent,直接返回true也可以消耗事件,这里考虑的话容易混乱)。
下面为拓展内容,有时间再继续看
上面我们从总体上理解了事件分发机制,但是实际开发当中只有在自定义View中我们会重写OnTouchEvent方法更多的我们是通过setOnClickListener里面的OnClick或者setOnTouchListener里面的onTouch的方式来处理点击事件的,那么OnTouch OnTouchEvent OnClick之间是什么关系呢?他们是怎样分工怎样相互协作的呢?
为了直观的解释问题,我简单的写了一个小的Demo,结合起来一起看方便理解。
Demo地址:https://github.com/LinLeyang/EventDispatch
首先界面上还是和上面一样的嵌套关系,如图所示
viewtu
其中的View都是自定义的控件,我们重写对应方法进行检验
首先第一轮什么都不做处理走正常的流程
第一轮 | OnTouch | onInterceptTouchEvent | OnTouchEvent | OnClick |
---|---|---|---|---|
ViewGroup1 | FALSE | FALSE | SUPER | NO |
ViewGroup2 | FALSE | FALSE | SUPER | NO |
View | FALSE | NONE | SUPER | NO |
FALSE TRUE 是返回值
SUPER是返回父类方法
NONE是没有这个方法
NO是没有设置监听
正常流程调用,但是发现非常智能的一点就是发现没人消耗的Down事件,直接结束了。MOVE和UP都不处理了
第二轮 | OnTouch | onInterceptTouchEvent | OnTouchEvent | OnClick |
---|---|---|---|---|
ViewGroup1 | FALSE | FALSE | SUPER | HAVE |
ViewGroup2 | FALSE | FALSE | SUPER | HAVE |
View | FALSE | NONE | SUPER | HAVE |
点击ViewGroup2
点击的最底层消耗事件。
第三轮 | OnTouch | onInterceptTouchEvent | OnTouchEvent | OnClick |
---|---|---|---|---|
ViewGroup1 | FALSE | FALSE | SUPER | HAVE |
ViewGroup2 | FALSE | FALSE | FALSE | HAVE |
View | FALSE | NONE | SUPER | HAVE |
发现一个问题就是一个View的OnTouchEvent可能开始能调用到,但是后面的一系列事件可能与他无缘。
第四轮 | OnTouch | onInterceptTouchEvent | OnTouchEvent | OnClick |
---|---|---|---|---|
ViewGroup1 | FALSE | FALSE | SUPER | NO |
ViewGroup2 | FALSE | FALSE | SUPER | NO |
View | FALSE | NONE | DOWN_TRUE | NO |
TextView只消耗DOWN事件,发现后面的事件也默然被他消耗了,这里体现了事件处理问题的惯性。
第五轮 | OnTouch | onInterceptTouchEvent | OnTouchEvent | OnClick |
---|---|---|---|---|
ViewGroup1 | FALSE | DOWN_FALSE | SUPER | HAVE |
ViewGroup2 | FALSE | FALSE | SUPER | HAVE |
View | FALSE | NONE | SUPER | HAVE |
DOWN_FALSE的意思是默认TRUE只有对DOWN事件展示FALSE
点击TextView
我们第一次看到了3这个action状态,就是说CANCLE状态,虽然ViewGroup1拦截了事件,但是因为之前TextView消耗了DOWN事件,所以ViewGroup1并没有拦截到第二个事件而是向TextView发送了一个CANCLE事件(意思是告诉它由于一些原因后面的事件不用你监听了),VIewGroup1从第三个MOVE事件开始拦截的。并且可以发现最终也没有调用OnClick事件,因为没有DOWN,算不上点击。
总结:
-
事件会通过dispatchTouchEvent由Activity->ViewGroup->View
在没有ViewGroup拦截onInterceptTouchEvent的情况下,逐层分发事件
直到View层,然后反过来寻找消耗者,消耗者可以通过OnTouch > OnTouchEvent OnClick消耗事件,事件一旦被消耗就返回true结束这个事件传递。
Tips:OnTouchEvent必须要调用super方法才能调用到OnClick -
针对Down事件,Down事件相当于事件的起点,如果一个view不消耗Down事件,那么其他事件他也无法消耗,反之当一个View消耗了DOWN事件的时候,后续的一系列事件默认都会由它消耗,除非上层的ViewGroup拦截,拦截后会发出CANCLE事件发给消耗DOWN事件的VIew。