之前分析了一下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 isandroid.view.PhoneWindow, which you should instantiate when needing aWindow.

  这里说的是,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 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事件的前提才有效!

更多相关文章

  1. Android期末复习题
  2. Android使用KeyStore对数据进行加密的示例代码
  3. 《第一行代码--android》笔记·第一章:认识资源
  4. Android(安卓)实现个性的ViewPager切换动画 实战PageTransformer
  5. 解决:android Listview 拖动时背景为黑色问题
  6. 雷电android game学习笔记(1)
  7. Android中SharedPrerence的apply和commit方法
  8. Android仿iPhone圆角边框
  9. Android之Inflate()方法用途

随机推荐

  1. App测试中Android和IOS测试区别
  2. PC上安装android market软件并提取apk文
  3. android 中asynctask的一些研究
  4. Android贝塞尔曲线实现水波纹的效果
  5. Android自适应屏幕方向、大小和分辨率之
  6. 移植FFmpeg到android ics
  7. Windows Cocos2d-x(二) Visual Studio 编写
  8. android项目打包成lib而非jar包的方法
  9. Android Scripting Environment动手玩
  10. EditText获取焦点框的颜色改变