Android触摸事件分发那些事

144
作者 CaiyuanHuang
2017.02.19 00:14* 字数 3096

一、概述

在Android开发中,经常需要自定义View。自定义View大概可以分为两个步骤:绘制外观和处理触摸事件。处理触摸事件需要知道触摸事件的分发流程,本文将带着大家详细地了解触摸事件分发流程,以及在触摸事件分发流程中扮演重要角色的方法如:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent的详细讲解。

二、触摸事件是如何传递到View的?


一个界面的View组成示例

一个触摸动作由一个down、零个或者多个move、一个up事件组成。通过源码分析,当触摸动作发生时,down事件会被传递到Activity的dispatchTouchEvent方法,Activity调用PhoneWindow的superDispatchTouchEvent方法将触摸事件传递到了PhoneWindow,PhoneWindow又调用DecorView的superDispatchTouchEvent方法将触摸事件传递给了DecorView,从反方向的角度来看,后面一个方法的返回值会影响前面一个方法的返回值。DecorView继承于FrameLayout,FrameLayout继承于ViewGroup,它是一个Activity里面的所有控件的父容器。所以触摸事件的传递顺序为:Activity-->PhoneWindow-->DecorView。看到这里,你也许有疑问了:谁把触摸事件传递给了Activity?这里,笔者知识储备有限,就没有从底层去分析了,暂把它当做一个“黑盒”,知道它有发出触摸事件到Activity即可。当down事件来临时,PhoneWindow、DecorView中上述相关的方法只要其中一个返回ture,则Activity的dispatchTouchEvent方法也和会返回true,表示Activity里面的某个View需要处理触摸动作,后续的move、up事件会从“黑盒”中分发到Activity。如果Activity的dispatchTouchEvent方法返回false,表示Activity里面所有的View都不需要处理这个触摸动作,因此后续的move、up事件都不会再从“黑盒”中分发到Activity。

三、触摸事件是如何从DecorView传递到每个View的?

众所周知,Android视图中的View的结构它是一个树状结构。树根是DecorView,DecorView是一个ViewGroup,ViewGroup继承于View,它重写了View的dispatchTouchEvent方法,用于将触摸事件分发给它的子View。因为Android高版本的事件分发流程跟低版本的类似,因此笔者选了android-1.6_r1这个版本的源码进行分析,因为它比较简单,干扰代码较少。

ViewGroup的dispatchTouchEvent方法

//此方法里面的代码并不是源码的全部,为了便于分析触摸事件的分发流程,干扰代码被笔者删除了
public boolean dispatchTouchEvent(MotionEvent ev) {

        if (action == MotionEvent.ACTION_DOWN) {
            if (!onInterceptTouchEvent(ev)) {
                //onInterceptTouchEvent返回false,表示ViewGroup不拦截触摸事件,则从前到后遍历每个子控件
                for (int i = count - 1; i >= 0; i--) {
                    final View child = children[i];
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                            || child.getAnimation() != null) {
                     if (frame.contains(scrolledXInt, scrolledYInt)) {
                     //如果触摸点在这个子View的大小范围内  
                            if (child.dispatchTouchEvent(ev))  {
                            //记录引用这个子View
                                mMotionTarget = child;
                                return true;
                            }
                          }
                        }
                    }
            }

        }
        final View target = mMotionTarget;
        if (target == null) {
        //将事件分发给自己处理即View的dispatchTouchEvent方法
            return super.dispatchTouchEvent(ev);
        }
        //将事件分发给要处理触摸事件的View
        return target.dispatchTouchEvent(ev);
    }

ViewGroup的dispatchTouchEvent方法还依赖下面的两个方法:

ViewGroup的onInterceptTouchEvent

这个方法只有ViewGroup拥有,View中是没有这个方法的

  public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

super.dispatchTouchEvent(即View的dispatchTouchEvent方法)

ViewGroup继承于View,ViewGroup的super.dispatchTouchEvent方法就是View的dispatchTouchEvent方法

 /**
     * 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) {
        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                mOnTouchListener.onTouch(this, event)) {
            return true;
        }
        return onTouchEvent(event);
    }

从上面的代码可以看出,View的dispatchTouchEvent方法当mOnTouchListener=null时,它的返回值就会跟onTouchEvent方法的返回值一致,后续讨论事件分发时,先忽略mOnTouchListener的影响。ViewGroup的dispatchTouchEvent方法的返回值会受其onInterceptTouchEvent方法的返回值、子View的dispatchTouchEvent方法的返回值影响。在Android的触摸事件分发的设计中,这三个方法有什么用呢?dispatchTouchEvent被用来传递分发触摸事件(true表ViewGroup或者View自己,又或者是ViewGroup其子View需要处理触摸事件,否则为false),onInterceptTouchEvent被ViewGroup用来拦截触摸事件(true表示拦截,否则为false),onTouchEvent被用来处理触摸事件(true表示处理,否则为false)。

当down事件传递到ViewGroup时,ViewGroup会根据自己的onInterceptTouchEvent方法的返回值来决定是否拦截这个down事件,这时分成两种情况:

(1)如果拦截,那么它就会调用super.dispatchTouchEvent(即View的dispatchTouchEvent方法),看自己是否需要处理这个down事件即View的dispatchTouchEvent依赖的onTouchEvent方法是否返回ture?如果返回true,因为返回值向上影响的关系,就会导致Activity的dispatchTouchEvent方法返回true,这样move、up事件发生时就会传递到Activity,事件继续分发,当分发到刚才那个需要处理触摸事件的ViewGroup时,这时就不会询问是否要拦截了,而是直接把事件交给它处理;如果这个ViewGroup的dispatchTouchEvent方法返回false,那么它的父ViewGroup就会将down事件交给这个ViewGroup的兄弟节点(假设存在话),若其兄弟节点都不处理这个down事件,那么触摸事件就交给这个ViewGroup的父ViewGroup处理,就这样一层层的向上进行类似的处理,如果都不处理,也就是Activity的dispatchTouchEvent方法返回false,那么后续的move、up事件都不会传递到Activity中了。

(2)如果不拦截,它会根据从前到后(后面添加的在前面)的顺序遍历子View,如果触点落在某个子View的区域,接着会判断这个子View是否要处理这个触摸事件,如果不需要处理则继续遍历下一个子View,如果需要处理,那么它的dispatchTouchEvent方法会立即返回true,后续的子View就不会被遍历了,同时它会记录下这个目标子View,用mMotionTarget这个全局变量标记,读者想一想,通过多层的遍历,每个ViewGroup都拥有一个mMotionTarget记录了需要处理事件的子View,那么一条处理触摸事件的链路是不是很明显了?当后续的move、up事件到达时,它就会根据这条链路将事件分发到最终那个需要处理事件的子View;通过上述的遍历,如果所有的子View都不需要处理触摸事件,那么这个ViewGroup就会把事件交给自己,处理流程也跟(1)中向上回溯询问的方式一致。

示例分析


示例的视图结构

例一:所有子ViewGroup都不拦截、所有子View都不处理触摸事件

当触摸动作发生时,down事件会从底层发送到Activity,经过PhoneWindow,事件被传递到了DecorView(也就是ViewGroup)即①号节点。这时候就会执行ViewGroup的dispatchTouchEvent方法。因为传递过来的是down事件,所以会根据onInterceptTouchEvent方法的返回值来判断这个ViewGroup是否拦截这个触摸事件,假设不拦截即返回false,接下来就会从前到后(后面添加的在前面)挨个调用子View的dispatchTouchEvent方法,这里Android做了一个优化,如果手指触点没有落在这个子View上,表示没有点击到这个View,是不需要处理触摸事件的,这个示例中假设触点落在了所有View上。①号节点只有一个子View即②号节点,这时down事件被传递到了②号节点,②号节点也是一个ViewGroup,因此它也会仿照①号节点处理down事件的方式,如果②号节点没有拦截,则down事件会被传递到③号节点,③号节点中onTouchEvent的方法如果返回false,表示这个View不需要处理触摸事件,因此dispatchTouchEvent也会返回false(先不管mOnTouchListener相关的代码),表明③号节点的View也不需要处理触摸事件,这时触摸事件被传递到了④号节点,后续的节点处理触摸事件的流程也一样。当事件传递到⑥号节点,表明④号节点没有拦截down事件,⑤号节点没有处理down事件,如果⑥号节点也不处理down事件,此时④号节点就会调用super.dispatchTouchEvent,将down事件交给自己,如果onTouchEvent返回了false说明④号节点也不需要处理触摸事件,这时候②号节点就会调用自己的super.dispatchTouchEvent,看自己是否需要处理触摸事件,如果返回false,则①号节点的dispatchTouchEvent方法也会返回false。因此down事件的分发事件的顺序为:①→②→③→④→⑤→⑥。

总结:

当down事件分发时,如果没有被其中一个ViewGroup的onInterceptTouchEvent方法拦截,这时down事件就会依次向下传递,每个View都能收到down事件,当down事件到达最后一个View(视图树中最底层最右边的那个叶子节点)时,如果它不处理这个down事件,down事件就会交还给它的父亲节点ViewGroup处理,如果它的父亲节点ViewGroup也不处理,那么down事件就会交给它的祖父节点ViewGroup处理,就这样一层一层地向上回溯,如果都不处理down事件,在回溯的过程中,父辈的ViewGroup的onTouchEvent方法会被调用,最后传递到Activity,如果Activity也不处理down事件即dispatchTouchEvent返回false,后续的move、up事件也无需处理,因此move、up事件就不会再分发过来了

例二:其中一个子ViewGroup拦截触摸事件

假设当down事件在②号节点被拦截时,这时遍历子View进行事件分发的动作就不会发生了,因此后续的③、④、⑤、⑥节点都不会收到down事件。接着②号节点会把down事件交给自己,看自己是否处理。如果处理即onToucheEvent方法返回true,dispatchTouchEvent方法也会返回true,这时返回值向上依次影响,导致Activity的dispatchTouchEvent方法也返回true,表明这个视图界面中有View需要处理触摸事件,因此后续的move、up事件也会分发到目标控件中。

总结:

在down事件的分发过程中,如果其中一个ViewGroup把事件拦截了,那么它的兄弟节点和孩子节点都不会受到这个事件了,接下来它会把触摸事件交给自己,看自己是否需要处理,自己如果不处理,那就交给父辈的ViewGroup处理。

例三:所有子ViewGroup都不拦截触摸事件,其中一个子View处理触摸事件

假设当down事件到达③号节点时,如果它的onToucheEvent方法返回true,导致它的dispatchTouchEvent方法也会返回true,此时事件就不会再分发到④、⑤、⑥节点了。它的父ViewGroup的dispatchTouchEvent方法就会立即返回true,不会再调用其super.dispatchTouchEvent方法,依此类推,它的父辈的onToucheEvent方法都不会被调用了。因为父辈每个ViewGroup都用mMotionTarget变量标记了需要处理触摸事件的孩子节点,当后续的move、up事件到达时,它就会根据指定的路线直接将事件分到到目标控件上。

总结:

如果其中一个View的onTouchEvent方法返回了true,表明这个View需要处理触摸事件,那么它的兄弟节点和孩子节点也不会收到触摸事件,当然也就不会在把事件交给它的父辈来处理即父辈的onTouchEvent方法也不会被调用了,后续的move、up事件到达时,会直接按照指定的路线到达目标控件。

三、参考资料

  1. android-1.6_r1的ViewGroup源码
    https://android.googlesource.com/platform/frameworks/base/+/android-1.6_r1/core/java/android/view/ViewGroup.java

  2. android-1.6_r1的View源码
    https://android.googlesource.com/platform/frameworks/base/+/android-1.6_r1/core/java/android/view/View.java

更多

长按下图->识别图中二维码或者扫一扫关注我的公众号。


微信公众号
Android进阶