Android View 的 Touch 事件传递机制

作者 : 开心源码 本文共8614个字,预计阅读时间需要22分钟 发布时间: 2022-05-12 共170人阅读

一、概述

在 Android UI 开发中,经常涉及与 touch(触摸)事件和手势,最经常使用的点击事件(OnClickListener)也与 touch 事件相关。因而,了解 touch 事件在 View 层级中的传递机制尤为重要。然而,onInterceptTouchEventonTouchEventonTouchListener 等一系列接口方法很容易让人混淆。

本文将详情 touch 事件的少量基础知识,并通过分析 Android FrameWork 源码来深入了解 touch 事件的分发机制。

注:

  1. 本文的源码分析基于 Android API Level 21,并省略掉部分与本文关系不大的代码。
  2. 在代码中加入了个人对源码的了解,以注释形式呈现。

二、基础知识

首先详情几个相关的类和方法:

  • 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 方法负责事件分发。ActivityViewGroupView 类中都定义了该方法,所以它们都具备事件分发的能力。
    Activity.dispatchTouchEvent 实际上是调用了 DecorViewdispatchTouchEvent 方法,而 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.onTouchonTouchEvent,并且当 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 事件传递机制

发表回复