Android View 的 Touch 事件传递机制
一、概述
在 Android UI 开发中,经常涉及与 touch(触摸)事件和手势,最经常使用的点击事件(OnClickListener)也与 touch 事件相关。因而,了解 touch 事件在 View 层级中的传递机制尤为重要。然而,onInterceptTouchEvent
、onTouchEvent
、onTouchListener
等一系列接口方法很容易让人混淆。
本文将详情 touch 事件的少量基础知识,并通过分析 Android FrameWork 源码来深入了解 touch 事件的分发机制。
注:
- 本文的源码分析基于 Android API Level 21,并省略掉部分与本文关系不大的代码。
- 在代码中加入了个人对源码的了解,以注释形式呈现。
二、基础知识
首先详情几个相关的类和方法:
MotionEvent 类:
该类封装了一个 Touch 事件的相关参数,我们通常所说的一个 Touch 事件,就是指一个MotionEvent
类的实例。一个MotionEvent
可以分为多种类型,即ACTION_DOWN
(按下)、ACTION_MOVE
(移动)、ACTION_UP
(抬起)和ACTION_CANCEL
(取消)等。ACTION_DOWN:
按照常规的操作顺序,通常的 Touch 事件触发的流程都是 DOWN → UP,或者者 DOWN → MOVE → UP。所以ACTION_DOWN
事件通常都是一系列连续操作事件的起点,也因而它通常在解决程序中被作为一个特殊的标识。ACTION_MOVE:
当手指按下后在屏幕上移动,就会产生ACTION_MOVE
事件,并且通常会随着手指移动而连续产生很多个。在移动过程中,可以根据MotionEvent
类的坐标信息,得到手指在屏幕上移动的位置。ACTION_UP:
UP 是一系列手势操作的结束点,程序会在收到ACTION_UP
事件时做少量收尾性的工作,例如恢复 View 的点击状态,值得一提的是,View 的 click 事件就是在ACTION_UP
时加以判断满足其余条件之后被触发的。ACTION_CANCEL:
CANCEL 事件不是由客户触发的,而是系统经过逻辑判断后对某个 View 发送“取消”消息时产生的。收到 CANCEL 事件时,View 应该负责将自己的状态恢复。事件分发方法
public boolean dispatchTouchEvent(MotionEvent ev)
:
事件由上一层的 View 传递到下一层 View 的过程称为事件分发。dispatchTouchEvent
方法负责事件分发。Activity
、ViewGroup
、View
类中都定义了该方法,所以它们都具备事件分发的能力。Activity.dispatchTouchEvent
实际上是调用了DecorView
的dispatchTouchEvent
方法,而DecorView
实际上是一个 FrameLayout,因而 Activity 的dispatchTouchEvent
最终也是调用到了 ViewGroup 的dispatchTouchEvent
方法。
另外,因为 View 没有管理子 View 的能力,所以View.dispatchTouchEvent
方法实际上不是用来向下分发事件,而是将事件分发给自己,调用了自己的事件响应方法去响应事件。事件响应方法
public boolean onTouchEvent(MotionEvent event)
:
该方法负责响应事件,并且返回一个 boolean 型,表示能否消费掉事件,返回 true 表示消费,false 表示不消费。Activity、View、ViewGroup 都有这个方法,所以它们都具备事件响应的能力,并且通过返回值来表示事件能否已经消费。事件阻拦方法
public boolean onInterceptTouchEvent(MotionEvent ev)
:
事件在 ViewGroup 的分发过程中,ViewGroup 可以决定能否阻拦事件而不对子 View 分发。该方法的返回值决定能否需要阻拦的,返回 true 表示阻拦,false 表示不阻拦。该方法只定义在 ViewGroup 类中,所以只有 ViewGroup 有权阻拦事件不对子View 分发。
小结:上述几个方法和类的关系如下:
Android-View-Touch-image1.png
三、View 中 Touch 事件的分发逻辑
先来看 View.dispatchTouchEvent
的源码:
// View.java/*** Pass the touch screen motion event down to the target view, or this* view if it is the target.** @param event The motion event to be dispatched.* @return True if the event was handled by the view, false otherwise.*/public boolean dispatchTouchEvent(MotionEvent event) { boolean result = false; // ... if (onFilterTouchEventForSecurity(event)) { ListenerInfo li = mListenerInfo; // 只需该 View 设置了 onTouchListener,并且该 View 是 enabled, // 则调用 onTouchListener.onTouch if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } // 只有 onClickListener.onTouch 返回 false, // onTouchEvent 才会被调用,并将其返回值返回 if (!result && onTouchEvent(event)) { result = true; } } // ... return result;}
可以看出,View 的事件分发过程主要涉及两个方法:mOnTouchListener.onTouch
和 onTouchEvent
,并且当 mOnTouchListener
存在时,mOnTouchListener.onTouch
调用的优先级比较高。
什么时候 mOnTouchListener
会存在?通过 View 的源码可看到 mOnTouchListener
是在 View 的 setOnTouchListener(OnTouchListener l)
方法中被设置的。所以,当我们通过 setOnTouchListener(OnTouchListener l)
方法设置了 onClickListener,并在 onClickListener.onTouch
方法中返回 true 消费了事件之后,onTouchEvent
将不会再被调用。
可见,mOnTouchListener.onTouch
是由外部 set 到 View 里去的,而 onTouchEvent
只能通过 Override 去重写自己的逻辑,且 View 的 onTouchEvent
方法自身已经有不少逻辑。所以 mOnTouchListener.onTouch
适用于增加不太复杂的 touch 逻辑,并且可以不妨碍 onTouchEvent
的正常调用;而 onTouchEvent
更适用于用 Override 的形式来改变 View 本身 touch 逻辑。
四、ViewGroup 中 Touch 事件的分发逻辑
尽管 ViewGroup 是 View 的子类,但是由于 ViewGroup 涉及对子 View 的解决,所以其事件分发逻辑比 View 的分发逻辑会复杂许多。ViewGroup 中重载了 dispatchTouchEvent
方法,逻辑也完全与之前不一样。
看 ViewGroup.dispatchTouchEvent
的源码:
// ViewGroup.java/*** {@inheritDoc}*/@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) { // ... // 该变量记录事件能否已被解决 boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // Step1.假如是 DOWN 事件,则清除之前的变量和状态 if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } // Step2.检查阻拦的情况 final boolean intercepted; // 只有满足以下两种情况,才可能去判断能否需要阻拦,否则都当作阻拦: // 1.假如是 DOWN 事件 // 2.在之前的 DOWN 事件分发过程中已经找到并记录下了响应 touch 事件的目标 View if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 假如该 View 被设置为不允许阻拦,则跳过阻拦判断 // (注:调用 requestDisallowInterceptTouchEvent 方法能设置该变量) final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { // 允许阻拦,则调用 onInterceptTouchEvent 判断能否需要阻拦 intercepted = onInterceptTouchEvent(ev); } else { // 否则不允许阻拦(注意此时不会调用 onInterceptTouchEvent) intercepted = false; } } else { // 假如不是 DOWN 事件,且之前没有找到响应 touch 事件的目标 View, // 则该 View 继续阻拦事件 intercepted = true; } // 该变量记录能否需要取消掉这次事件 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; // Step3.分发 DOWN 事件或者其余初始事件(例如多点触摸的 DOWN 事件) // 假如既不取消,又不阻拦 if (!canceled && !intercepted) { // 假如是 DOWN 事件或者其余两种特殊事件(先只看 DOWN 事件) if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // 遍历所有子 View final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { // 找到事件的坐标(x,y)对应的子 View if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; } // ... // 调用 dispatchTransformedTouchEvent 方法将事件分发给子 View, // 该方法会调用子 View 的 dispatchTouchEvent 方法继续分发事件 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 假如该方法返回 true,代表子 View 消费了该事件 // 记录接受该事件的子 View,记录在以 mFirstTouchTarget 开头的链表中,具体看 addTouchTarget 方法的源码 newTouchTarget = addTouchTarget(child, idBitsToAssign); // 标记已经成功分发了事件 alreadyDispatchedToNewTouchTarget = true; // 退出循环 break; } } } } } // 到目前为止,在 (不阻拦 && 不取消 && 是 DOWN 事件) 的前提下,已经在子 View 中寻觅过一次事件的响应者。 // 假如有子 View 消费了事件,那么事件已经通过 dispatchTransformedTouchEvent 方法分发到了该子 View 中, // 并且 alreadyDispatchedToNewTouchTarget = true, // 并且将响应者记录在局部变量 newTouchTarget 和 成员变量 mFirstTouchTarget 链表中。 // Step4.接下来将事件分发到 touchTarget 中或者分发到自己身上。 if (mFirstTouchTarget == null) { // mFirstTouchTarget == null 意味着之前的程序没有找到事件的消费者,那么事件将传递给自己, // 注意:是通过调用 dispatchTransformedTouchEvent 方法,并将该方法的第3个参数设为 null,代表传递给自己。 // 而该方法中,当第3个参数为 null 时,会调用了 super.dispatchTouchEvent 方法,而 ViewGroup 的父类就是 View,所以就是走了 View 的事件分发流程将事件传递给自己。 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // 接下来通过遍历 mFirstTouchTarget 链表,将事件分发到 touchTarget 中, // 注意上面用 newTouchTarget 变量记录了已被分发的 View,这里不会重复分发。 TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } } target = next; } } } // ... return handled;}
ViewGroup 的 dispatchTouchEvent
逻辑显然比 View 的逻辑复杂得多,主要分为以下 4 步:
- Step1. 假如是 DOWN 事件,则清除之前的变量和状态
- Step2. 检查阻拦的情况
- Step3. 分发 DOWN 事件或者其余初始事件(例如多点触摸的 DOWN 事件)
- Step4. 接下来将事件分发到 touchTarget 中或者分发到自己身上。
我们从以下几点来总结一下 ViewGroup 的事件分发逻辑:
ViewGroup 在什么情况下可以阻拦事件?
我们知道,阻拦是由onInterceptTouchEvent
方法的返回值决定的。假设该 ViewGroup 没有被设置为不允许阻拦(即正常情况下),那么对于 DOWN 事件,onInterceptTouchEvent
方法一定会被调用。另外,假如是 MOVE、UP 或者其余事件类型,只需满足mFirstTouchTarget != null
时也会调用onInterceptTouchEvent
。mFirstTouchTarget
变量会在什么时候被赋值?它的作用是什么?mFirstTouchTarget
是用来记录在 DOWN 事件中消费了事件的子 View,它以链表的形式存在,通过 next 变量串起来。在 DOWN 事件中,假如通过点击的坐标找到了某个子 View,且该子 View 消费了事件,那么链表中就将这个子 View 记录了下来。这样在后续的 MOVE、UP 事件中,能直接根据这个链表,将事件分发给目标子 View,而无需重复再遍历子 View 去寻觅事件的消费者。onInterceptTouchEvent
方法针对不同类型的事件进行阻拦,会有什么影响?
从上面的源码可知,假如在onInterceptTouchEvent
方法中阻拦了非 DOWN 的事件,那么只会影响本次事件的分发流程,把事件分发到自己的onTouchEvent
方法去解决。而假如onInterceptTouchEvent
方法中阻拦的是 DOWN 事件,那么将导致在 dispatch 过程中找不到事件的消费者(即mFirstTouchTarget == null
),那么后续的 MOVE、UP 事件将不会再讯问能否需要阻拦,而是直接分发到自己的onTouchEvent
方法去解决。
因而,DOWN 事件在 ViewGroup 的事件阻拦、分发过程中是一个特殊的角色,对其解决的结果将直接影响后续事件的分发流程。
五、Activity 中 Touch 事件的分发逻辑
理解完 View 和 ViewGroup 的事件分发逻辑后,再来看 Activity 的分发逻辑就简单多了。
看 Activity.dispatchTouchEvent
的源码:
// Activity.java/*** Called to process touch screen events. You can override this to* intercept all touch screen events before they are dispatched to the* window. Be sure to call this implementation for touch screen events* that should be handled normally.** @param ev The touch screen event.** @return boolean Return true if this event was consumed.*/public boolean dispatchTouchEvent(MotionEvent ev) { // ... if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev);}
非常简单,先尝试调用 window.superDispatchTouchEvent
方法,改方法返回 false 时才调用 onTouchEvent
方法。而 window.superDispatchTouchEvent
方法,实际上是调用了 Window 的 DecorView 的 dispatchTouchEvent
方法,因为 DecorView 是 FrameLayout 的子类,当然也就是一个 ViewGroup,所以归根究竟 Activity.dispatchTouchEvent
方法最终也是调用了 ViewGroup.dispatchTouchEvent
方法。
至此为止,我们将 View、ViewGroup、Activity 的事件分发流程都理解完了。可以想象,当客户触发了一个触摸事件,Android 系统会将其传递到当前触摸的 Activity.dispatchTouchEvent
方法中,接着,就由 Activity、ViewGroup、View 的 dispatchTouchEvent
方法不断递归调用,把事件传递给某个目标 View,而后再逐层返回。
六、例子
最后,我们再通过一个例子来回顾一下整个分发过程。
假设有一个 Activity,他的界面内容是一个 ViewGroup,ViewGroup 内还有一个 Button。当点击 Button 的位置时,会产生一连串事件,像 DOWN → UP 或者者 DOWN → MOVE → MOVE → UP,这些事件分发过程的时序图如下:
Android-View-Touch-image2.png
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Android View 的 Touch 事件传递机制