Touch事件传递机制

概述

在Android UI 开发中,经常会涉及到与Touch(触摸)事件和手势,以及最经常使用的(OnClicklistener)也与touch事件相关.因此,理解Touch事件在View层级中的传递机制尤为重要.我们今天主要讨论onInterceptTouchEvent, onTouchEven, onTouchListener等一系列接口方法.

基础知识

首先我们介绍几个相关的类和方法:

  • MotionEvent 类:
    该类封装了一个Touch事件的相关参数,我们通常所说的一个Touch事件就是指一个motionEvent实例.即ACTION_DOWN(按下) ACTION_MOVE(移动) ACTION_UP(抬起) ACTION_CANCEL (取消)等.
  • ACTION_DOWN:
    按照常规的操作顺序,通常事件的触发流程都是DOWN -> UP ,或者DOWN -> MOVE -> UP.所以ACTION_DOWN事件通常都是一个事件的起点,也因此它在处理过程中被当做一个特殊的标识.
  • ACTION_MOVE:
    当手指按下后在屏幕上移动,就会产生ACTON_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.dipatchTouchEvent实际上是调用了DecorViewdispatchTouchEvnet方法,而DecorView实际上是一个FrameLayout,因此ActivitydispatchTouchEvent方法最终也是调用到了ViewGroupdispatchTouchEvent方法. 另外,由于View没有管理子View的能力,所以View.dispatchTouchEvent方法实际上不是用来向下分发事件,而是将事件分发给自己,调用了自己的事件相应方法去响应事件.后面我们会接触到一个变量mFirstTouchTarget`这个变量接直接决定要不要向下分发.
  • 事件拦截方法 public boolean onInterceptTouchEvent(MotionEvent ev)
    事件在ViewGroup的分发过程中,ViewGroup可以决定是否拦截事件而不对子View分发.该方法的返回值决定是否需要拦截的,返回ture表示拦截,false表示不拦截.该方法只定义在ViewGroup类中,所以只有ViewGroup有权拦截事件不对子View分发.
    上述方法和类的关系如下:
    [图片上传失败...(image-ba77d2-1520927267855)]

View中Touch事件的分发逻辑

View.diapatchEvent的源码如下:

// 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;
}

可以看出onTouchLIstener.touch的优先级要高于onTouchEvent.只有在onTouchLIstener没有被设置,或者onTouch的返回值为fasle时才会调用onTouchEvent.
由于onTouch方法是由View抛出到外部的setONTouchListener(OnTouchLIstener l)方法设置的.而onTouchEvent只能通过Override重写自己的逻辑.所以一般用onTouch来处理一些简单的逻辑,并且不
妨碍onTouchEven的调用.而在onTouchEven一般对View自身的逻辑进行编辑.

ViewGroup中Touch事件的分发逻辑

虽然ViewGroup是View的子类,但是因为ViewGroup涉及对子View的处理,所以其分发逻辑比View的分发逻辑要复杂一些.ViewGroup中重写了dispatchTouchEvent,逻辑与之前完全不同.

// ViewGroup.java
/**
* {@inheritDoc}
*/
@Override
public 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步:

  • 如果是DOWN事件,则清理之前的变量和状态,(因为如果是DOWN的话,那说明是一个触摸事件的起始,所以需要初始化之前的状态参数.)
  • 检查拦截的情况(当满足以下两种情况才会判断是否需要拦截,否则都当做拦截:1.是DOWN事件,2.mFirstTouchTarget不为空,此时检查事件是否应该被拦截.)
  • 如果检查后,事件既没有被取消又没有被拦截,则将DOWN事件以及其他初始事件(例如多点触摸的DOWN事件)进行事件分发.
  • 接下来将事件分发到touchTarget中或分发到自己身上.

下面我们来讨论几个问题,以帮助我们理解ViewGroup的事件分发逻辑:

  • ViewGroup在什么情况下可以拦截事件?
    我们知道,拦截是由onInterceptTouchEvent方法的返回值决定的.假设该ViewGroup没有被设置为不允许拦截(即正常情况),那么对于DOWN事件,onTnterceptTouchEvent方法肯定会被调用,另外,如果是MOVE, UP 或者是其他事件类型,只要满足mFirsstTouchTarget != null时也会调用 onInterceptTouchEvent
  • mfirstTouchTarget变量什么时候被赋值?它的作用是什么?
    mFirstTouchTarget是用来记录在DOWN事件中消费了事件的子View,它以链表的形式存在,通过next变量串起来.在DOWN事件中,如果通过点击的坐标找到了某个子View,且该子View消费了事件,那么链表就将这个View记录下来.这样在后续的MOVE, UP事件中,能根据这个链表,将事件分发给目标子View,而无需重复遍历子View去寻找事件的消费者.
  • onInterceptTouchEvent方法针对不同的类型的事件拦截会有什么不同的影响?
    从源码可知,如果在onINterceptouchEven方法中拦截了非DOWN事件,那么只会影响本次事件分发流程,把事件分发到自己onTouchEvent方法去处理.而如果onIntercepterTouchEvent方法中拦截的是DOWN事件,那么将导致自己在dispatch过程中找不到事件的消费者(即 mFirstTouchTarget == null),那么后续的MOVE, UP 事件将不会再询问是否需要拦截,而是分发到自己的onTouchEvent方法去处理.

Activity中Touch 事件的分发逻辑

下面是Activity.dispatchTouchEvent的源码:

**
* 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);
}

我们可以看到上述代码的逻辑结构十分简单,首先尝试调用getWindow().superDispatchTouchEvent(ev)方法,该方法返回false时才调用onTouchEvent方法.而window.superDIspatchTouchEvent方法,实际是调用了Window.superDispatchTouchEvent方法,实际上调用了Window 的DecorView的dispatchTouchEvent方法,由于DecorView是FrameLayout的子类,当然也就是一个ViewGroup,所以归根结底Activity.dispatchTouchEvent方法最终也是调用了ViewGroup.dispatchTouchEvent方法.

逻辑模拟

假设有一个Activity,他的界面内容是一个ViewGroup内还有一个Button.当点击Button的位置时,会产生一连串事件,想DOWN -> UP 或者 DOWN -> MOVE -> UP,这些事件的分发过程的时序如图:
[图片上传失败...(image-da10d9-1520927267855)]

onTouchEvent & onClick & onLongClick

上述三个响应方法都很常见,我们也经常使用,但是很多人对三者的执行次序和执行条件都比较模糊,下面我们就对此进行讨论:

  • 举例分析
  1. 当DWON事件传入,onTouchEvent的DOWN事件触发,此时若事件未被消化(即onTouchEvent返回false)长时间保持一个位置按压,超过onLongClick的时间线,则onLongClick被唤醒,随后按压结束传入UP事件,UP事件未被消化后(即onTouchEvent返回false),onClick执行.

  2. 当DWON事件传入,onTouchEvent的DOWN事件触发,此时若事件被消化(即onTouchEvent返回true),随后按压结束传入UP事件,UP事件被消化后(即onTouchEvent返回true).期间不会再执行onLongClickonClick

  3. 当DWON事件传入,onTouchEvent的DOWN事件触发,此时若事件被消化(即onTouchEvent返回true),随后按压结束传入UP事件,UP事件未被消化后(即onTouchEvent返回false).期间不会再执行onLongClickonClick

  4. 当DWON事件传入,onTouchEvent的DOWN事件触发,此时若事件未被消化(即onTouchEvent返回false),随后按压结束传入UP事件,UP事件被消化后(即onTouchEvent返回true).期间不会再执行onLongClickonClick
    这里有两种情况,1.长按则会在UP事件之前执行onLongClick 2.短按则会先执行UP过一段时间后执行onLongClick

  • 机制分析
    Touch事件中:Down事件的返回值决定了这个事件是不是一个Click事件.当返回值为true时说明此事件不是一个Click事件(此时不会激活onClick或者onLongClick),返回false说明该事件是一个Click事件,此时UP的返回值,(如果是true那么我们需要知道在DOWM和UP之间有没有足够的时间激活onLongClick,并且此时onCLick不会被激活.)(如果是false,有两种情况1.长按:DOWN -> onLongClick -> UP -> onCLick 2. 短按:DOWN -> UP -> onClick -> onLongClick)

文章来自
http://blog.csdn.net/eclipsexys/article/details/8785149
http://chanthuang.github.io/2016/08/30/Android-View-%E7%9A%84-Touch-%E4%BA%8B%E4%BB%B6%E4%BC%A0%E9%80%92%E6%9C%BA%E5%88%B6/

你可能感兴趣的:(Touch事件传递机制)