技术背景
从 View 体系中认识 Touch 事件传递,暂时留一条线索:
" View 最原始的事件从哪里来? ”
从 WindowCallbacKWrapper开始的。
那么,我们开始吧!
tip:阅读源码前,建议读懂 Android View体系之基础常识及技巧。
千里之行,始于Activity
从 window
层开始下发事件后, Activity
开始处理事件,会调用 ViewGroup#dispatchTouchEvent
1 | Activity.java |
Activity#dispatchTouchEvent
方法最后会通过 DecorView
触发 ViewGroup#dispatchTouchEvent
开始分发事件。
总结:Activity
下发 Touch
事件到 DecorView
并由 DecorView
开始向下传递。
ViewGroup之核心分发
DecorView
调用 dispatchTouchEvent
分发 Touch
事件。代码很长,可是不难,逻辑比较清晰。
1 |
|
如果看完上述注释还有点蒙,一定要多撸几次源码。有几个点想说明一下,可能大家会好理解要一点。
TouchTarget mFirstTouchTarget
的作用mFirstTouchTarget
贯穿dispatchTouchEvent
流程,实际上它是一个链表,用于记录所有接受Touch
事件的 子view
。在日常开发中有没有遇到过这样一个逻辑“ 如果一个view
没有接收过ACTION_DOWN
的事件,那么后续ACTION_MOVE
和ACTION_UP
也一定不会分发到这个view
。 ”。这个逻辑是基于mFirstTouchTarget
记录的view
实现的。
总结: 根据上述注释逻辑链。
- 过滤’不合法’的
Touch
事件; - 如果是 MotionEvent.ACTION_DOWN ,则初始化一些状态;
- 判断事件是否需要拦截,是否需要取消;
- 如果不需要拦截&不是取消事件,则会向子
view
下发Touch
事件; - 如果没有任何子
view
消费事件,则会自己处理,如果已有子view
消费事件,判断当前新处理的target
对象是否是mFirstTouchTarget
链表最新一个,如果是则默认为当前传递已经传递事件,否则返回子view
递归结果。
上述有两个递归,在 注释10
和 注释14
,这里你可能会有疑惑,这两个的关系是什么。注释10
实际上是返回以 viewGroup 为根节点的 view 下是否有节点消费点击事件,如果有则记录下当前子 view。注释14
实际上是返回以 viewGroup 为根节点的 view 下是否有节点消费事件。
ViewGroup之递归入口
在上一章节,dispatchTouchEvent
多次调用 dispatchTransformedTouchEvent
,这里做下简单分析。
1 | private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, |
总结:dispatchTransformedTouchEvent
会对非 MotionEvent.ACTION_CANCEL
事件做转化并递归返回所有事件的下发结果。
View也可分发事件?
既然不是 view
,那么 dispatchTouchEvent
应该不是属于下发范畴的,那会是什么呢?
1 | public boolean dispatchTouchEvent(MotionEvent event) { |
上述逻辑表明有三种场景下会返回 true
结果。
两种场景为:第一种是拖拽场景,比如listview等控件存在这种逻辑;另一种是开发者设置了 OnTouchListener 对象并在 onTouch
函数中处理并返回 true
结果。
最后一种场景为普遍场景,及如果没有上述两种场景且是当前是最外层 view
时(事件已经无法再传递),则会调用自身的 onTouch
方法处理。
总结:view#dispatchTouchEvent
会在事件下发链末端调用,并把当前 view
的 onTouch
返回值作为 dispatchTouchEvent
返回值。
回看ViewGroup如何拦截
上述篇章的下发逻辑都需要判断 Touch 事件是否需要被拦截,先看看代码。
1 | public boolean onInterceptTouchEvent(MotionEvent ev) { |
上面的代码在绝大部分情况下都返回 false
。除非你鼠标事件且在上面滚动,这个场景很像你在滚动网页一样,那么当前的页面就会拦截滚动事件进行页面滚动。
值得注意的是:如果你在该方法返回 true
进行拦截,那么你会走下面的调用逻辑:
- viewGroup#dispatchTouchEvent
- viewGroup#dispatchTransformedTouchEvent
- view#dispatchTouchEvent
- view#onTouchEvent
总结:viewGroup#onInterceptTouchEvent
是 ViewGroup 特有的方法。默认情况下 ViewGroup 不会拦截 Touch 事件,如果拦截了 Touch 事件,则会交给 View#onTouch
进行处理。
事件的宿命onTouchEvent
这个方法是处理 Touch 事件,并返回结果给 dispatchTouchEvent
的。可以理解为:它决定了某个 view
是否真正消费 Touch 事件。直接看源码。
1 | public boolean onTouchEvent(MotionEvent event) { |
上述代码中,有两处 callback 的逻辑你可能还没有完全明白,一个是 TapCallback,一个是 LongPressCallback,分别对应 mPendingCheckForTap
和 mPendingCheckForLongPress
,看下完整的代码。
1 | // 代码段1,类 CheckForTap |
代码段1
会调用 代码段5
,实际上也是延迟发送 “处理长按行为”。和直接调用 代码5
不同,代码5
中延迟 500-delayOffset
ms 执行 “处理长按行为”,而源码的调用基本都是默认 delayOffset = 0
。代码1 则以 delayOffset = 100
先延迟 100 ms之后延迟 400 ms 发送处理长按行为,同样需要 500 ms 才会支持 “处理长按行为”。那到底为啥要这么做呢?
原因是当前处理的 view 位于可滑动的容器内需要延迟处理接收的按压事件。这样讲有点抽象,你可以这样理解,android 把 MotionEvent.ACTION_DOWN
场景区分为 滑动(scroll)
和 轻敲(tap)
,用延迟的时间来判断手势已经发生了位移。如果发生了位移,则还依然需要保持判断有效长按时间(500 ms)不变,所以会追加 400 ms延迟来 post 一个 “处理长按行为” 任务。
上述6个代码段用于加深理解 onTouchEvent
内事件的处理而已。从上上段代码上看,我们总结下整个流程:
- 如果
view
不可用则根据是否可点击来直接消费MotionEvent.ACTION_UP
- 如果
view
设置了mTouchDelegate
,则默认消费 Touch 事件 - 如果
view
可点击或者在 tooltip 显示状态下默认消费事件,否则返回 false 给dispatchTouchEvent
。MotionEvent.ACTION_UP
分支会设置按压状态,触发点击或长按事件,最后重置状态MotionEvent.ACTION_DOWN
分支延迟发送“处理长按行为”MotionEvent.ACTION_CANCEL
分支重置处理 Touch 过程中设置的状态MotionEvent.ACTION_MOVE
分支处理滑动 RippleDrawable 效果并在手势滑出 View 范围情况下重置状态
总结: onTouchEvent
是真正完成对 Touch 事件的处理,并把处理结果作为dispatchTouchEvent
的递归结果。
5个案例加强理解
GitHub链接上有本次 Touch传递测试代码
测试案例两个 viewGroup
和 一个 view
- 场景一:
View3#onTouchEvent
返回true
消费所有事件,上层不拦截。
场景一可知:View3
消费所有事件并返回 true
,对于上层下发的任何事件,dispatchTouchEvent
都返回 true
。
- 场景二:
View3#onTouchEvent
返回false
不消费 Touch 事件,上层不拦截,Linearlayout2
返回true
消费所有 Touch 事件。
场景二可知:如果末层不消费所有事件,则 ACTION_DOWN
会开始从末层向上传递。Linearlayout2
消费了ACTION_DOWN
之后,其及上层dispatchTouchEvent
都返回 true
。ACTION_DOWN
之后的事件序列(如ACTION_MOVE,ACTION_UP)都会往Linearlayout2
分发,其下层就再也收不到后续事件了。
- 场景三:
Linearlayout2#onInterceptTouchEvent
返回true
拦截 Touch 事件,但是Linearlayout2#onTouchEvent
返回false
不消费事件。
场景三可知:Linearlayout2
拦截了 ACTION_DOWN
之后,其子 View 再也收不到任何事件,其消费结果由 onTouchEvent
决定,如果不消费,则往上层传,直到找到某层消费事件。如果没有任何一层消费,则后续事件序列也不会下发了。
- 场景四:
Linearlayout2#onInterceptTouchEvent
返回true
拦截 Touch 事件,但是Linearlayout2#onTouchEvent
返回true
消费事件。
场景四可知:Linearlayout2
拦截了 ACTION_DOWN
之后,其子 View 再也收不到任何事件,如果消费 ACTION_DOWN
,则后续事件序列都往Linearlayout2
下发。
- 场景五:
View3#onTouchEvent
返回true
消费所有除ACTION_CANCEL
事件,但是Linearlayout2#onInterceptTouchEvent
拦截了ACTION_MOVE
事件且不消费任何事件。
场景五可知:ACTION_DOWN
传递到 View3
被其消费,后续序列事件本应该传递到 View3
。当 ACTION_MOVE
被 Linearlayout2
拦截之后,无论是否消费,View3
再也收不到 ACTION_MOVE
及其后续的事件序列,但是会在事件被一次拦截时收到 ACTION_CANCEL
,是否消费 ACTION_CANCEL
的结果会被当做此次传递的结果返回。此后,此次ACTION_MOVE
后续的事件序列往 Linearlayout2
下发。