Android 源码分析 - 事件分发机制

  之前分析了一下Android中的消息传递机制,不知道对各位有没有帮助!哈哈,别怪我写的太垃圾了......也不要说的太多的废话了,直接进入今天的主题--Android 事件分发机制。还是那样,文章如有错误,请各位指正,本文参考资料:
  1.任玉刚老师的《Android 开发艺术探索》
  2.徐宜生老师的《Android 群英传》
  注意,本文的所有代码都是 API 26,如果是其他的版本,会做特别说明!

1.概述

  我们还是继承一下《Android 消息处理机制》的格式,先来概述一下今天的内容,假装符合面向对象的继承特性。。。
  在事件传递机制中,必须讲解的三个方法:

  1.public boolean dispatchTouchEvent(MotionEvent ev)方法,这个方法作用主要是用来分发事件。也就是说,当一个事件传递当前View的dispatchTouchEvent方法里面,这个方法可以决定将事件分发到哪里去,这里的分发到哪里去表示有两个意思:1.将事件分发到子View(如果有子View的话);2.将事件分发到分发到自己的onTouchEvent方法里面去消耗。
  2.public boolean onInterceptTouchEvent(MotionEvent ev)方法,这个方法的作用是用来决定当前的View或者ViewGroup是否拦截这个事件,如果返回true的话,那么就表示拦截;反之,表示不拦截。前排预警一下,这个方法有很多的坑,不是返回一个true或者false那么简单。
  3.public boolean onTouchEvent(MotionEvent event)方法,这个方法是具体消耗事件的方法,如果返回true的话,表示当前的View已经将这个事件消耗了。

  可能大家看我写了这些,还是觉得一脸懵逼。这三个方法的意思大家都懂,说这些有什么用。大哥,不要急,我们来慢慢的分析。
  当前一个事件发生了,事件传递的流程是从上层依次传递到下层,直到这个事件被处理,例如:



  上图中,当在事件发生点发生了事件,它的传递顺序是:ViewGroupA ->ViewGroupB ->View。然后我们在结合上面的三个方法来更加形象的展示一下,事件分发的顺序:



  这里从图中可以看出来,事件是从ViewGroupA开始的,先调用A的dispatchTouchEvent方法,进行分发,同时还会调用A的onInterceptTouchEvent方法,如果onInterceptTouchEvent方法返回的是false,表示ViewGroupA不拦截此事件,于是将事件传递给ViewGroupB,ViewGroupB也进行跟ViewGroupA一样的操作。如果ViewGroupB也不进行拦截的话,那么首先就会传递到View的dispatchTouchEvent方法,由于View再没有子View了,所以不能进行向下分发,所以只能传递到View的onTouchEvent方法里面来。如果View消耗了这个事件的话,那么这个事件传递的流程就在这里结束,不会继续将事件传到ViewGroupB的onTouchEvent方法里面去;反之如果View不消耗这个事件的话,那么就继续往上传递。
  上面只分析了ViewGroup不对事件进行拦截的情况,下面来分析一下当一个ViewGroup拦截了事件的情况。例如:

  一旦,ViewGroupA对事件进行拦截,直接将事件传递给ViewGroupA的onTouchEvent方法里面去。
  这个大的流程差不多就是这样的,可能中间有非常多的细节没有提及到,但是不急,待会的源码分析有你们好受的!!!哈哈,开玩笑!

2.ViewGroup的事件分发

(1).DecorView

  当我们用手指在屏幕点击时,事件首先被传递到Activity的dispatchTouchEvent方法。对的哦!你没有看错,Activity也有dispatchTouchEvent方法。我们来看看Activity的dispatchTouchEvent方法代码:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

  可见,当Activity的dispatchTouchEvent方法接收到了一个事件之后,Activity会将这个传递到Window里面去,我们再去看看:

public abstract boolean superDispatchTouchEvent(MotionEvent event);

  哦豁,我们发现superDispatchTouchEvent所在的Window类是一个抽象类,怎么办?不急,在Window类解释中,google爸爸给我们这么说的(代码根据 api 26):

The only existing implementation of this abstract class is
android.view.PhoneWindow, which you should instantiate when needing a
Window.

  这里说的是,Window抽象类的唯一实现类在是android.view.PhoneWindow。然后我们到PhoneWindow里面去看看相应的方法:

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

  好嘛,又继续跳,然后我们就到了DecorView类的superDispatchTouchEvent方法里面来了。

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

DecorView又是什么鬼?DecorView其实我们界面的顶级容器,也就是我们视图树的根,是被添加到Window的。而DecorView作为顶级View,一般情况下,它内部会类似于LinearLayout的竖直布局,在这个布局里面有上下两个部分,上面是标题栏,下面是Content View部分,在Activity 的setContView所设置的布局文件就是添加到Content View的部分。如图:



  通常来说,我们可以通过如下代码来我们自己设置的ContentView对象

        ViewGroup viewGroup = getWindow().getDecorView().findViewById(android.R.id.content);

  从这里,我们知道DecorView肯定是一个ViewGroup对象,我们继续点击dispatchTouchEvent方法,发现进入到了ViewGroup的dispatchTouchEvent方法里面来了。
  好嘛,费了半天的劲,我们终于看到了重头戏了。好了好了,我们整装待发,准备好好的来看一下这个方法!不过我们先来总结,我们获取了哪些信息:

  1.一个事件首先会被传递到Activity的dispatchTouchEvent方法里面,然后最终会传递DecorView中去,最后通过DecorView调用ViewGroup的dispatchTouchEvent方法来进行事件的分发。
  2.DecorView是一个Activity的根本局,实际上他也是一个ViewGroup。

(2).ViewGroup对View事件的分发

  由于dispatchTouchEvent方法源代码太多了,所以我就不像消息机制那篇文章贴出完整的代码,在这里知识贴出部分代码来进行理解。
  首先,我们来看看这段代码:

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

  这段代码的作用是非常明显的,就是check当前的ViewGroup是否需要拦截当前的事件。我们发现在这段代码里面发现了另一个比较眼熟的方法onInterceptTouchEvent方法。从代码中,我们可以看出,ViewGroup判断一个事件是否需要判断实在dispatchTouchEvent方法里面对方法进行调用。
  然后我们再看看调用onInterceptTouchEvent方法的条件。首先,action为ACTION_DOWN的话,需要判断当前的是否拦截,这个非常好理解。但是mFirstTouchTarget是什么什么意思?实际上呢,这个从后面的代码逻辑中可以看出来,当ViewGroup的子元素成功处理一个事件的时候,mFirstTouchTarget会被赋值并指向该子元素。换一句话说,当ViewGroup不拦截事件,将事件交由给子元素来处理时,mFirstTouchTarget就不为null了。也就是说,当事件序列的开始--ACTION_DOWN来到时,这时候mFirstTouchTarget是为null(因为这是第一次来,所以事件还没有传递给它的子元素),如果此时ViewGroup在onInterceptTouchEvent返回为true的话,表示拦截这个事件序列,然后后面的ACTION_MOVE和ACTION_UP来到时,由于此时调用onInterceptTouchEvent方法的条件不符合,所以onInterceptTouchEvent不会再被调用。为什么这里调用onInterceptTouchEvent方法的条件不符合呢,因为第一次的down事件被ViewGroup拦截了,从而导致down事件没有被传递到子View,所以mFirstTouchTarget肯定为null,当ACTION_MOVE和ACTION_UP两个事件来到,actionMasked == MotionEvent.ACTION_DOW || mFirstTouchTarget != null肯定为false的!
  从而,我们从这段里面得到一个结论,一旦一个ViewGroup在onInterceptTouchEvent方法里面对ACTION_DOWN事件进行拦截,属于同一个事件序列的后续事件也会被拦截,同时onInterceptTouchEvent方法只会被调用一次,也就是对ACTION_DOWN进行拦截的那一次!
  说到这里,那么有没有办法对其他事件进行需求性的拦截呢?有的,这个问题,我们后续再讲!现在就讲的话,就不能显示我牛逼了!哈哈,开玩笑的,应该时时刻刻记住自己就是一个菜鸡!
  在刚刚的那段代码中,我们还发现有这个判断

                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                  ......
                }

  其中,我们需要关注的是FLAG_DISALLOW_INTERCEPT 标记位,这个标记位是通过ViewGroup里面的requestDisallowInterceptTouchEvent方法来设置的,一般用于子View。一旦FLAG_DISALLOW_INTERCEPT被设置了,也就是说,我们在子View里面调用父布局的requestDisallowInterceptTouchEvent方法,那么ViewGroup将无法拦截除ACTION_DOWN以外的其他点击事件。
  这里为什么时候是ACTION_DOWN以外的点击事件呢?这是因为,ACTION_DOWN事件会重置FLAG_DISALLOW_INTERCEPT标记位,导致子View设置的这个标记位无效。我们来看看代码:

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

  从dispatchTouchEvent的代码看来,上面这段代码在我们之前那段代码的前面,所以在ViewGroup在判断事件是否需要拦截之前,就会重置FLAG_DISALLOW_INTERCEPT,从而导致我们的子View调用requestDisallowInterceptTouchEvent方法失效!
  经过上面的代码,如果ViewGroup不对事件进行拦截,那么就会将这个事件分发到能够接收到这个事件的子View。

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

  根据任玉刚老师在《Android 开发艺术探索》中对这段代码的解释,一个子View是否能够接收到点击事件主要由两点来衡量:子View是否是否在播放动画和点击事件是否落在子View的的区域内。如果这两个事件能够满足的话,那么事件就会交给它来处理。
  这里将会详细的讲解一下,事件到底是怎么传递到子View。ViewGroup是通过dispatchTransformedTouchEvent来将事件分发到子View的!

                           resetCancelNextUpFlag(child);
                           if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

  这个是事件分发代码,其中,我们会发现,如果当前子View会消耗这个事件,也就是说dispatchTransformedTouchEvent方法返回true,那么将会将当前的View添加target的链表,而我们说的mFirstTouchTarget就是指向这个链表的头!这个就相当于完成的分发了吗?
  NO!NO!没有那么的简单,我们会发现前面有段代码:

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

  如果当遍历第一个子View的时候,这里的newTouchTarget就会返回的不是null,岂不是下面的dispatchTransformedTouchEvent根本就来不及调用。像这种情况,应该怎么办?我们发现,只要在这段代码里面break,最后会执行这段代码:

              // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                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;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }

  如果说,之前已经将事件分发下去了,alreadyDispatchedToNewTouchTarget && target == newTouchTarget这个条件肯定为true。所以,如果在dispatchTransformedTouchEvent方法之前break,从而导致跳出循环,alreadyDispatchedToNewTouchTarget肯定是为false的,因为这个变量在调用了dispatchTransformedTouchEvent方法之后会被置为true。这行代码在之前循环遍历子View里面。

                                alreadyDispatchedToNewTouchTarget = true;

  所以,只要在之前没有调用dispatchTransformedTouchEvent方法就break,肯定会进入else的代码里面。现在的关键是理解resetCancelNextUpFlag是什么意思?我们先来看看这个方法:

    /**
     * Resets the cancel next up flag.
     * Returns true if the flag was previously set.
     */
    private static boolean resetCancelNextUpFlag(@NonNull View view) {
        if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
            view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
            return true;
        }
        return false;
    }

  这里,我是看不懂代码的。但是可以从方法的注释来看他的意思,这个方法作用是,如果之前这个View的flag被重置过,那么就返回true,反之返回false。简而言之,相对于同一个View来说的话,如果第一次调用这个方法的话,返回的是false;反之则返回的true。
  所以,在这里,我们就可以理解到了,只要是在调用dispatchTransformedTouchEvent方法之前就break的话,resetCancelNextUpFlag返回的肯定是true。这个是为什么呢?因为只要getTouchTarget返回的不是null,表示的意思就是当前的View已经被添加到了mFirstTouchTarget所在的链表中,也就是说在当前这个事件之前,有可能有个事件传递到当前的这个View,并且执行了,所以被添加到链表中的。因为这段代码在dispatchTransformedTouchEvent方法为的true才执行的:

                                newTouchTarget = addTouchTarget(child, idBitsToAssign);

  从而得知,只要newTouchTarget不为null的话,resetCancelNextUpFlag方法返回的肯定是true。而这里cancelChild变量还由intercepted变量来决定,这个待会再细讲,因为变量太特么的坑了!
  这样我们就能得知,如果一个View对一个事件序列的事件进行处理,但是后续如果有一个事件不会处理的话,那这个View会收到一个ACTION_CANCEL类型的事件!
  以上就是ViewGroup对子View的事件分发大概的解释,不敢说特别详细!下面来总结一下:

  1.当一个事件传递到ViewGroup里面的话,首先会根据事件类型或者mFirstTouchTarget 是否null来判断是否调用onInterceptTouchEvent方法,当然这个过程中还要考虑FLAG_DISALLOW_INTERCEPT标记位。简而言之,当前DOWN事件来到时,ViewGroup首先询问onInterceptTouchEvent是否需要拦截。这里需要注意的是,如果有子View处理这个事件了,会导致mFirstTouchTarget不为null,从而可以形成一种父ViewGroup可以拦截非ACTION_DOWN事件的局面!还需要注意的是,整个询问拦截的过程还需要考虑子View调用requestDisallowInterceptTouchEvent方法来请求不要我的事件!哎,感觉子View好可怜,动不动就会ViewGroup折磨!!!!
  2.当ViewGroup不对事件进行拦截时,ViewGroup会将相应的事件传递到子View里面!
  3.如果整个事件序列的ACTION_DOWN没有子View来处理,最终会传递到ViewGroup方法里面处理。因为当mFirstTouchTarget为null时,会调用ViewGroup自己的onTouchEvent方法!但是这里需要的注意,整个事件序列,除了ACTION_DOWN会传递到子View的onTouchEvent之外,后续的事件都只会到达子View的dispatchTouchEvent方法,不会到达onTouchEvent方法里面。这个原因待会再讲View的方法来解释!
  4.当一个事件序列中间(记住这里是中间,开始的情况参考 3 ,结尾可以参考这个)的某个事件没有子View来处理的话,那么在TouchTarget链上的所有View都会收到一个ACTION_CANCEL事件,并且会将这些子View从链上recycle掉。从而得知,只要一个子View不对一个事件进行处理,那么在这个事件序列上的其他类型的事件都不会交给它来处理。
  5.如果一个事件序列从ACTION_DOWN开始,就被拦截了。这个事件序列的所有事件不会在传递的到子View。因为ACTION_DOWN来的时候,mFirstTouchTarget本来为空,由于onInterceptTouchEvent方法返回true,所以导致if (!canceled && !intercepted)语句进入不了,进而导致mFirstTouchTarget在整个事件序列都为空,所以一直在调用这个代码,从而导致整个事件序列的都不能传递下去:
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            }

(3).ViewGroup对View事件的拦截

  还记得我在前面挖的两个坑吗?第一个是在概述里面说的,前排预警一下,onInterceptTouchEvent方法有很多的坑,不是返回一个true或者false那么简单;第二个是在(2)里面的,有没有办法对其他事件进行需求性的拦截?
  到这里来看看,这两个坑好像就像是一个问题,都是关于onInterceptTouchEvent方法。
  其实在之前我们简单的介绍onInterceptTouchEvent方法的作用和使用,但是只是粗略的介绍,在这里将稍微详细的解释。

A.onInterceptTouchEvent方法的调用时机

  先说明一下,这里先不考虑FLAG_DISALLOW_INTERCEPT标记位的影响。
  在dispatchTouchEvent方法,我们知道,当一个事件序列的开始,也就是ACTION_DOWN来到时,会调用onInterceptTouchEvent方法来询问是否需要拦截此事件序列!这种情况下,应该非常容易的理解!
  另一种情况便是mFirstTouchTarget 不为null的时候。那mFirstTouchTarget不为null究竟是什么情况呢?我们从dispatchTouchEvent方法里面可以看出来,当一个事件被子View消耗了,那么会将当前的这个View封装成一个Target对象,然后添加到一个链表的链头,而mFirstTouchTarget则是指向这个链表的链头。也就是说,当前mFirstTouchTarget不为null的时候,表示在同一个事件序列,当前事件前面的事件被子View消耗掉了!mFirstTouchTarget不为null表示的就是这个意思!
  如上的情况下,我们可以形象的解释,将你的妈妈比喻为ViewGroup,而子View当成你,你开始打游戏表示一个事件序列的开始。如上的情况就是这样的,你开始打游戏的时候,你妈妈没有拦截你的行为,因此你可以顺利的打开游戏,开心的吃鸡,如果中途你妈妈叫你去打酱油,可是此时你正在决赛圈说你没空,你妈妈就生气了,把你的网线拔了,相当于是拦截你的行为,导致你的吃鸡梦想泡汤了!这个比喻能够说明上面的情况,也就是说,当子View在ACTION_MOVE的非常开心的时候,父ViewGroup有资格让子View不开心!哈哈哈哈!!!
  上面的解释就是,当不考虑FLAG_DISALLOW_INTERCEPT标记位时,onInterceptTouchEvent方法的调用时机。
  那么我们现在来考虑FLAG_DISALLOW_INTERCEPT标记位。
  首先说一下,标记位对事件序列的开始事件--ACTION_DOWN无效的!只有当子View在ACTION_MOVE的非常开心的时候,才有资格向父ViewGroup申请不要拦截我的事件!这个请求是有效的!
  如上便是onInterceptTouchEvent方法的调用时机。这里对onInterceptTouchEvent方法的调用时机做一个简单的总结:

  1.ViewGroup有资格一开始ACTION_DOWN,即使子View调用requestDisallowInterceptTouchEvent方法来申请不拦截也没有用的。一旦拦截了,整个事件序列就都失去了向下传递的能力,直接进入ViewGroup的onTouchEvent方法去处理。
  2.View有资格不拦截ACTION_DOWN,而是拦截ACTION_MOVE和ACTION_UP事件。还是跟拦截ACTION_DOWN的情况比较类似,但是还是有点区别!

B.onInterceptTouchEvent方法对非ACTION_DOWN的事件进行拦截

  如果ViewGroup只能对ACTION_DOWN进行拦截的话,这样也太暴力了!因为这样会导致整个事件序列都只能被传递ViewGroup。所以,ViewGroup对ACTION_MOVE和ACTION_UP事件还是有必要的。其实这种需求很好的实现,例如:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {

            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP: {
                return true;
            }
        }
        return super.onInterceptTouchEvent(ev);
    }

  是不是瞬间来了一句卧槽!这么简单。对!就是这么简单,但是简单的背后大有玄机所在了!例如:
  这是ViewGroup的代码:

public class MyViewGroup extends LinearLayout {
    public MyViewGroup(Context context) {
        super(context);
    }
    public MyViewGroup(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    public MyViewGroup(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("pby123", "1");
        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP: {
                return true;
            }
        }
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("pby123", "2");
        return true;
    }
}

  这是View的代码:

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }
    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("pby123","3");
        return true;
    }
}

  此时,我对View进行ACTION_DOWN和ACTION_UP的事件产生,然后打印的log却是这样的:



  我们发现,当ACTION_DOWN事件产生时,传递到子View很正常,但是我们对ACTION_UP事件进行拦截的,为什么还是会传递子View里面去呢?是不是onInterceptTouchEvent对ACTION_UP事件是无效的呢?瞎猜是没有用的,此时我们来看看dispatchTouchEvent的代码。(其实这种情况,我在分析dispatchTouchEvent的时候已经非常小声的说过了哦!!!!)

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

  上面这段代码,我们当ACTION_UP事件来到,由于mFirstTouchTarget不为null,最终会调用onInterceptTouchEvent来进行询问是否需要拦截,我们在onInterceptTouchEvent方法里面返回的是true,所以在intercepted肯定为true。然后代码往下走,最终进入这段代码:

                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                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;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }

  是不是感觉又回来了?又来分析这个方法了,我们知道cancelChild返回的肯定是true,所以dispatchTransformedTouchEvent这一步会给子View发送一个ACTION_CANCEL事件,然后就行将这个target回收了。
  到这里我们知道了,第二次ACTION_UP事件根本没有传递到子View里面,传递过去的是一个ACTION_CANCEL事件!大家如果不信的话,可以去试试!
  这里我们不满足只是ACTION_DOWN和ACTION_MOVE事件,我们使其也产生ACTION_MOVE事件。我们来看看这种情况下的log日志:



  哈哈没错,第二次之所以将事件传递给子View,那么是因为ACTION_MOVE事件被拦截了,从而传递过去一个ACTION_CANCEL事件过去,而不是ACTION_MOVE事件。
  好了,onInterceptTouchEvent方法分析的差不多了,现在该解决在留的两个问题。首先,onInterceptTouchEvent方法坑在于onInterceptTouchEvent方法的调用时机,待会再总结里面会总结一下,这里就不再多余的说了;其实onInterceptTouchEvent的坑还有就是ACTION_DOWN和ACTION_UP,谁又能想到传递子View的根本不是ACTION_UP事件呢?。其次,就是对非ACTION_DOWN的拦截,假设我们从ACTION_MOVE开始拦截,需要注意的是第一个ACTION_MOVE事件是不会传递子View,也不会传递到ViewGroup,只有经过这次的处理,后面的事件ViewGroup才算是能够接收到!
  又该对上面的知识点做一个总结:

  1.一个ViewGroup的调用时机是:1.ACTION_DOWN的来到;2.事件序列中间的ACTION_MOVE事件来到,需要注意是这样情况下,必须保证在同一个事件序列中, 当前事件的前面的事件有被子View消耗过的,也就是,mFirstTouchTarget不能为null。
  2.调用时机还需要的是:如果一个事件被拦截了,在这个事件序列里面,onInterceptTouchEvent不会再被调用。
  3.如果我们想要对非ACTION_DOWN事件进行拦截,必须保证同一个事件序列的前面所有事件都子View执行了。
  4.对非ACTION_DOWN事件进行拦截,是对下次的事件进行拦截,当前的事件会被变为ACTION_CANCEL传递到子View中去。

3.View对事件的处理

  由于View是没有子View的,所以View不能继续对事件继续的分发。相较于ViewGroup,View少了一个onInterceptTouchEvent方法。所以说,如果一个事件到达View,肯定会处理,注意的处理表达意思是:它可以调用onTouch或者onTouchEvent方法来处理,或者不处理,最后这个事件被它的ViewGroup分发ViewGroup自己进行处理。
  所以,View对事件的处理分成两种情况:一种是自己处理;一种是不处理,父ViewGroup会自己处理,处理的代码也是调用View的,因为ViewGroup继承于View。我们一个一个的分析。

(1).View事件处理流程

  事件首先会被传递View的dispatchTouchEvent方法里面,我们来看看,不要怕哦!View的dispatchTouchEvent代码非常的简单。

    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        return result;
    }

  以上的代码,我删除了部分我认为不重要的代码,是不是非常的简单?其实意思也非常的简单,首先如果设置了OnTouchListener监听的话,onTouch方法是否消耗该事件,如果消耗的话,事件传递就结束了;反之,则将事件传递到onTouchEvent方法里面去。
  从这里,我们可以看出,onTouch的优先级比onTouchEvent的高!
  我们再来看看onTouchEvent,由于onTouchEvent方法代码太长了,这里只看部分:

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }

  从以上的代码中,我们看出来,如果一个View的enable属性是Disable的话,它仍然能够消耗事件,只是不会做出任何的反应而已,正如注释所说的。
  我们继续往下看,我们发现switch-case语句被这段代码包裹:

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
  ......
}

  而clickable是什么呢?我们来看看:

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE

  也就是说,只要CLICKABLE、LONG_CLICKABLE或者CONTEXT_CLICKABLE其中一个为true的话,就会对事件进行消耗!
  在switch-case里面,我们不看ACTION_DOWN和ACTION_MOVE事件,我们来看看ACTION_UP事件有个非常眼熟的东西:

                                if (!post(mPerformClick)) {
                                    performClick();
                                }

  我们再来看看performClick方法里面有什么东西呢

    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

  哎呀,这个不是我们喜闻乐见的OnClickListener吗?开不开心,激不激动?哈哈哈!!!
  从这里,我们可以得出,在一个View中,onTouch的优先级是最高的,其次是onTouchEvent,最后才是onClick方法!

(2).ViewGroup对事件的处理

  ViewGroup对事件的处理在dispatchTransformedTouchEvent方法里面进行的,由于dispatchTransformedTouchEvent方法的代码比较长,这里只看他是怎么调用onTouchEvent方法:

            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }

  我们上面的代码中发现调用super.dispatchTouchEvent(event)方法,从而完成了自己对事件的处理,事件处理的流程跟View对事件的处理流程比较相似!

4.总结

  终于写完了,我们还是来对我们所有的内容做一个总结:

  1.如果一个事件序列的ACTION_DOWN事件被ViewGroup拦截,事件序列的后续事件不再会传递到子View,即使在ACTON_MOVE时,子View调用requestDisallowInterceptTouchEvent方法没用的。同时,在这种情况下,onInterceptTouchEvent方法只会被调用一次。
  2.在一个事件序列中,如果ACTION_DOWN事件没有被拦截,并且ACTION_DOWN事件被子View消耗了,则ViewGroup有资格去拦截事件序列剩下的事件。
  3.在一个事件序列中,如果子View不处理ACTION_DOWN事件,此事件序列的后续事件不会在传递到子View。记住,连子View的dispatchTouchEvent方法都不会到达!
  4.在一个事件序列中,如果子View开始处理一些事件,事件中途突然被ViewGroup拦截,被拦截的当前事件会转换成为ACTION_CANCEL事件传递到子View中去,ViewGroup真正获取事件从下一次事件(不是下一次事件序列!)开始。
  5.在View中,优先级最高的onTouch,其次是onTouchEvent,最好是onClick。onClick方法在ACTION_UP时刻回调!
  6.如果一个事件最后连ViewGroup都不处理的话,最终回到Activity的onTouchEvent方法里面来。
  7.当View的enable为Disable时,也会消耗事件,只是不会做出任何的反应。同时只要CLICKABLE、LONG_CLICKABLE或者CONTEXT_CLICKABLE其中一个为true的话,就会对事件进行消耗!
  8.View的requestDisallowInterceptTouchEvent方法只是在View消耗ACTION_DOWN事件的前提才有效!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容