Android知识Android技术知识Android开发

Android View 的 Touch 事件传递机制

2019-03-07  本文已影响7人  chanthuang

一、概述

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

本文将介绍 touch 事件的一些基础知识,并通过分析 Android FrameWork 源码来深入理解 touch 事件的分发机制。

注:

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

二、基础知识

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

小结:上述几个方法和类的关系如下:


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}
*/
@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 步:

我们从以下几点来总结一下 ViewGroup 的事件分发逻辑:

因此,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
上一篇下一篇

猜你喜欢

热点阅读