Android触摸事件的响应机制
BY 志为
昨天项目组同学在做一个需求时遇到了一些触摸事件处理方面的问题,然后大家发现原来之前对Android触摸事件的响应机制的理解比较片面,没有彻底搞清楚它,于是做了一些研究和实验,搞清楚了大概是怎么回事,在这里总结一下做一个分享,如果有什么遗漏或者错误之处,还请大神们指正!
触摸事件类型
用户触摸屏幕所产生的Touch Event在Android里是用一个MotionEvent对象来传递和处理的,我们最关注的是MotionEvent里的action,可以看到有 ACTION_DOWN, ACTION_UP,ACTION_MOVE,ACTION_CANCEL,ACTION_POINTER_DOWN,ACTION_POINTER_UP 等等很多种,在这里面最需要关注的是ACTION_DOWN和ACTION_UP,它们一个代表了用户按压动作的开始,一个代表了用户按压动作的结束,其他的一些ACTION基本都是发生在这2个ACTION之间的(ACTION_CANCEL等特殊的暂不讨论)。
在用户的一次单手指触摸屏幕过程中,简单的讲,会按顺序产生一个ACTION_DOWN,若干个ACTION_MOVE,和一个ACTION_UP,我们下面的讨论也会基于这个简单case。
触摸事件的传递处理顺序
一个触摸事件首先是在硬件层面触发,然后逐层传递到软件直至我们的app,前面的细节一般来说不用了解,我们讨论的事件入口从Activity开始。
笼统的说(实际上细节有所不同,下面会提到)
触摸事件的传递顺序是:
Activity -> 当前活动窗口(PhoneWindow) -> 窗口的Top-Level View(DecorView) -> 各级ViewGroup (如各种Layout) -> ... -> 各级ViewGroup -> 叶子节点 View
而触摸事件的处理顺序则刚好相反:
叶子节点 View -> 各级ViewGroup -> ... -> 各级ViewGroup -> (Window和DecorView只有分发没有处理) -> Activity
当叶子节点View接受到事件之后,会试图做出处理,如果它处理了,则上面各层不再处理,如果它没有处理则往上由它的父ViewGroup处理,这样逐层向上按顺序试图处理,直到Activity。
分发和处理的关键函数
从上面笼统提到的事件分发处理顺序可以看到,关键的分发处理集中在Activity,ViewGroup和View中,那么对于它们来说,有如下几个分发和处理的关键函数,这里先做一个简单介绍,后续再做详细说明。
1,boolean dispatchTouchEvent(MotionEvent event)
- 这个函数是最关键的分发处理函数,里面既包含了分发的逻辑,又包含了对处理的逻辑调用
- 分发逻辑:这个函数会先调用子view的dispatchTouchEvent进行分发
- 处理逻辑:如果子view没有对事件进行消化处理的话,这个函数就会调用本UI组件的处理函数如onTouchEvent
- 函数返回值表示这个触摸事件是否被当前这个UI组件(Activity/ViewGroup/View)消化处理了
2,boolean onTouchEvent(MotionEvent event)
- 这个函数是UI组件自己实现用来响应处理触摸事件的
- 函数返回值表示这个触摸事件是否被当前这个UI组件(Activity/ViewGroup/View)消化处理了
3,View.OnTouchListener: boolean onTouch(View v, MotionEvent event)
- 这个函数不是Activity/ViewGroup/View本身的响应处理函数,而是一个Listener的响应处理函数
- 需要给View通过setOnTouchListener来设置Listener以使得这个onTouch函数能够起作用
- Activity没有setOnTouchListener
- 函数返回值表示这个触摸事件是否被当前这个UI组件(Activity/ViewGroup/View)消化处理了
4,ViewGroup: boolean onInterceptTouchEvent(MotionEvent event)
- 这个函数表明是否要拦截这个事件,前面提到过事件的分发顺序是在View tress里从根到叶子逐层分发,处理则是反向的从叶子到根逐层处理,onInterceptTouchEvent如果返回true,则表示我这一层要拦截这个事件自己进行处理了,不要把它分发到子View里
- 这个函数只有ViewGroup含有,View没有,因为View已经是叶子节点了,没有子View了
- 这个函数默认是返回false的,一般不要轻易override它,因为常规来讲是应该由子View先响应处理的
分发和处理的细节流程
ACTION_DOWN的处理
前面说过ACTION_DOWN是一个触摸动作的起始,所以对ACTION_DOWN的处理和对其他事件的处理在细节上是有不同的,各个UI组件对于ACTION_DOWN事件的处理流程可以看到如下:
(注意这里 view 表示一个视图组件,它可以是一个 View 也可以是一个 ViewGroup)
Activity -> dispatchTouchEvent:
- 通过getWindow().superDispatchTouchEvent(event)把事件分发到当前活动窗口 (PhoneWindow),之后是 窗口的Top-Level View(DecorView),调用了DecorView的dispatchTouchEvent,DecorView继承自ViewGroup,所以这里实际上就进入了ViewGroup层面的dispatchTouchEvent
- 如果superDispatchTouchEvent最终返回true(即下层的某个ViewGroup或者View消化处理了该函数,dispatchTouchEvent返回true),则直接返回
- 如果返回值为false,则调用Activity的onTouchEvent对事件进行处理
ViewGroup -> dispatchTouchEvent:
- 首先是检查本view里是否保存了一个motion target(步骤4提到了怎么设置motion target),如果有则清除它
- 然后是调用onInterceptTouchEvent看这个事件是否需要被自己拦截,如果返回true,则直接进入步骤7开始自己处理事件的流程
- 如果返回false,则需要遍历所有的子view,遍历的顺序是:
- 首先按照Z-order
- 在同一Z值下如果可以的话按照子view的drawing order,这里的drawing order需要ViewGroup的子类override了getChildDrawingOrder才会实际生效
- 遍历子view的时候,如果这个触摸事件发生的位置在这个view的视觉范围以内,那么就调用 child.dispatchTouchEvent将事件分发给这个子view,如果这个子view消化处理了这个事件(即 dispatchTouchEvent返回true),本view会将该子view赋值给一个表明motion target的变量,且此时跳出循环
- 循环遍历结束后,如果有子view处理了该事件(即motion target不为空),则返回true表明此事件已经被处理
- 如果没有任何一个子view处理了该事件(即motion target不为空),则本view需要进行处理,进入步骤6
- 查看当前view是否注册了OnTouchListener,如果有,则调用该listener的onTouch函数来处理事件,如果onTouch返回true表示消化处理了该事件,则直接返回true
- 如果onTouch返回false表示没有处理,则继续调用本view的onTouchEvent函数来处理事件,这里会返回onTouchEvent的返回值
View -> dispatchTouchEvent:
- 查看当前view是否注册了OnTouchListener,如果有,则调用该listener的onTouch函数来处理事件,如果onTouch返回true表示消化处理了该事件,则直接返回true
- 如果onTouch返回false表示没有处理,则继续调用本view的onTouchEvent函数来处理事件,这里会返回onTouchEvent的返回值
其他ACTION的处理
上面说了ACTION_DOWN的处理,那ACTION_DOWN后续的ACTION_MOVE,ACTION_UP之类的事件又是怎么处理的呢?它们的处理方式略有不同:
在Activity层面来看,它们的处理和ACTION_DOWN没有区别。
在ViewGroup层面来看,处理开始有了差异:
ViewGroup -> dispatchTouchEvent:
- 检查当前view在之前处理ACTION_DOWN(ACTION_MOVE,ACTION_UP之类的事件一定是有一个配对的ACTION_DOWN事件在前面发生)的时候是否已经设置了一个motion target
- 如果有这个target,那么表明之前的ACTION_DOWN事件就是由该子view处理的,此时直接调用motion_target.dispatchTouchEvent
- 如果没有这个target,那么表明之前的ACTION_DOWN没有任何一个子view处理,那么后续这些事件也不要发给子view了,直接自己处理,进入步骤4
- 查看当前view是否注册了OnTouchListener,如果有,则调用该listener的onTouch函数来处理事件,如果onTouch返回true表示消化处理了该事件,则直接返回true
- 如果onTouch返回false表示没有处理,则继续调用本view的onTouchEvent函数来处理事件,这里会返回onTouchEvent的返回值
- 注意整个流程中都跳过了onInterceptTouchEvent的拦截
在View层面来看,处理方式也是一样的。
所以这里可以看到的现象就是:ACTION_DOWN被谁处理了,后续的ACTION_MOVE,ACTION_UP等事件最终也会交由谁处理。
这里画了一个简单的漫画,希望能够帮助更容易的理解:
onTouchEvent() or OnTouchListener.onTouch()?
从上面的内容可以看出来,我们要想对一个触摸事件进行响应,可以在view的onTouchEvent()函数里处理,也可以给view设置一个OnTouchListener然后在listener的onTouch()函数里处理,那么它们有些什么区别?我们应该怎么选择呢?
首先看区别:
- onTouchEvent()是View自己的函数,对于我们来说无法override 各种View或者ViewGroup的onTouchEvent()函数,只能是在自己自定义的view里面实现override;而 OnTouchListener的onTouch()是可以对任何View/ViewGroup起作用的,我们只需要在代码里为该View /ViewGroup加一个listener就行
- OnTouchListener的onTouch()的执行顺序在view的onTouchEvent()之前,如果在onTouch()函数里面响应完了触摸事件并返回true之后,onTouchEvent()是不会被调用的
知道了区别,那么我们就可以轻易的做出选择了(只是一家之言,欢迎各种不同意见):
- OnTouchListener的onTouch()基本上是万能的,任何时候都可以用它,所以大部分时候直接用它就行了
- 在自定义view里面,如果出于代码结构和功能清晰的目的,可以使用onTouchEvent()
其他
这个总结其实把整个响应机制给简化了,实际的处理过程要比这个麻烦很多,比如还涉及到onClick的产生及响应,以及其他一些复杂情况的处理,所以如果想要彻底搞清楚每一点,最好的办法还是去看Android源码!
另外,自己实际的写一个本地debug的app,override各种View/ViewGroup并打印log,可以帮助你更简单的梳理清楚整个流程。