Android开发艺术探索知识回顾——第3章 View的事件体系:3、View的事件分发机制、滑动冲突

3.4 View的事件分发机制

上面几节介绍了 View 的基础知识以及 View 的滑动,本节将介绍 View 的一个核心知识点:事件分发机制。事件分发机制不仅仅是核心知识点更是难点,不少初学者甚至中级开发者面对这个问题时都会觉得困惑。

另外,View 的另一大难题滑动冲突,它的解决方法的理论基础就是事件分发机制,因此掌握好 View 的事件分发机制是十分重要的。本节将深入介绍 View 的事件分发机制,在 3.4.1 节会对事件分发机制进行概括性地介绍,而在 3.4.2 节将结合系统源码去进一步分析事件分发机制。

3.4.1 点击事件的传递规则

在介绍点击事件的传递规则之前,首先我们要明白这里要分析的对象就是 MotionEvent即点击事件,关于 MotionEvent 3.1节中已经进行了介绍。

何为点击事件的事件分发?

所谓点击事件的事件分发,其实就是对 MotionEvent 事件的分发过程,即当一个 MotionEvent 产生了以后,系统需要把这个事件传递给一个具体的 View,而这个传递的过程就是分发过程。

点击事件分发过程的三个重要方法

点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEventonlnterceptTouchEvent onTouchEvent。下面我们先介绍一下这几个方法。

public boolean dispatchTouchEvent(MotionEvent ev)

用来进行事件的分发。如果事件能够传递给当前 View,那么此方法一定会被调用,返回结果受当前 View 的 onTouchEvent 和下级 View dispatchTouchEvent 方法的影响,表示是否消耗当前事件。

public boolean onInterceptTouchEvent(MotionEvent event)

在上述方法内部调用,用来判断是否拦截某个事件,如果当前 View 拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

public boolean onTouchEvent(MotionEvent event)

在 dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View 无法再次接收到事件。

上述三个方法到底有什么区别呢?它们是什么关系呢?其实它们的关系可以用如下伪代码表示:

public boolean dispatchTouchEvent(MotionEvent ev) {
      boolean consume = false;
      if (onInterceptTouchEvent(ev)) {
          consume = onTouchEvent(ev);

      } else {
          consume = child.dispatchTouchEvent(ev);
     }

     return consume;
}

通过伪代码,大致了解点击事件的传递规则

上述伪代码已经将三者的关系表现得淋漓尽致。通过上面的伪代码,我们也可以大致了解点击事件的传递规则:对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这时它的 dispatchTouchEvent 就会被调用,如果这个 ViewGroup onlnterceptTouchEvent 方法返回 true,就表示它要拦截当前事件,接着事件就会交给这个 ViewGroup 处理,即它的 onTouchEvent 方法就会被调用;

如果这个 ViewGroup 的 onlnterceptTouchEvent 方法返回 false,就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的 dispatchTouchEvent 方法就会被调用,如此反复直到事件被最终处理。

当 View 设置了 OnTouchListener

当一个 View 需要处理事件时,如果它设置了 OnTouchListener,那么 OnTouchListener 中的 onTouch 方法会被回调。这时事件如何处理还要看 onTouch 的返回值,如果返回false,则当前 View 的 onTouchEvent 方法会被调用;如果返回true,那么 onTouchEvent 方法将不 会被调用。

由此可见,给 View 设置的 OnTouchListener,其优先级比 onTouchEvent 要高。 在 onTouchEvent 方法中,如果当前设置的有OnClickListener,那么它的 onClick 方法会被调用。可以看出,平时我们常用的 OnClickListener,其优先级最低,即处于事件传递的尾端。

当一个点击事件产生后,它的传递过程遵循的顺序

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity -> Window -> View,即事件总是先传递给 Activity,Activity 再传递给Window,最后 Window 再传递给顶级 View顶级 View 接收到事件后,就会按照事件分发机制去分发事件。

考虑一种情况,如果一个 View 的 onTouchEvent 返回 false,那么它的父容器的 onTouchEvent 将会被调用,依此类推。 如果所有的元素都不处理这个事件,那么这个事件将会最终传递给 Activity 处理,即 Activity 的 onTouchEvent 方法会被调用。

这个过程其实也很好理解,我们可以换一种思路,假如点击事件是一个难题,这个难题最终被上级领导分给了一个程序员去处理(这是事件分发过程),结果这个程序员搞不定onTouchEvent 返回了 false),现在该怎么办呢?难题必须要解决,那只能交给水平更高的上级解决(上级的 onTouchEvent 被调用),如果上级再搞不定,那只能交给上级的上级去解决,就这样将难题一层层地向上抛,这是公司内部一种很常见的处理问题的过程。从这个角度来看,View 的事件传递过程还是很贴近现实的,毕竟程序员也生活在现实中。

关于事件传递机制的一些结论

关于事件传递的机制,这里给出一些结论,根据这些结论可以更好地理解整个传递机制,如下所示。

1)同一个事件序列是指:从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以 down 事件开始,中间含有数量不定的 move 事件,最终以 up 事件结束。

(2)正常情况下,一个事件序列只能被一个 View 拦截且消耗。这一条的原因可以参考(3)因为一旦一个元素拦截了某此事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个 View 同时处理,但是通过特殊手段可以做到,比如一个 View 将本该自己处理的事件通过 onTouchEvent 强行传递给其他 View 处理。

(3)某个 View 一旦决定拦截,那么这一个事件序列都只能由它来处理 ( 如果事件序列能够传递给它的话),并且它的onlnterceptTouchEvent 不会再被调用。这条也很好理解,就是说当一个 View 决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个 View 的 onlnterceptTouchEvent 去询问它是否要拦截了。

(4)某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件 ( onTouchEvent 返回了 false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的 onTouchEvent 会被调用。意思就是事件一旦交给一个 View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短期内上级就不敢再把事情交给这个程序员做了,二者是类似的道理。

(5)如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。

(6)ViewGroup 默认不拦截任何事件。Android 源码中 ViewGroup onlnterceptTouchEvent 方法默认返回 false。

(7)View 没有 onlnterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。

(8)View 的 onTouchEvent 默认都会消耗事件 ( 返回 true ),除非它是不可点击的 ( clickable longClickable 同时为 false )。 View longClickable 属性默认都为 false,clickable 属性要分情况,比如 Button 的 clickable 属性默认为 true,而 TextView 的 clickable 属性默认为 false。

(9)View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个 View 是 disable 状态的,只要它的 clickable 或者 longClickable有一个为 true,那么它的 onTouchEvent 就返回 true。

(10)onClick 会发生的前提是当前 View 是可点击的,并且它收到了 down 和 up 的事件。

(11)事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子 View,通过requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。

3.4.2 事件分发的源码解析

上一节分析了 View 的事件分发机制,本节将会从源码的角度去进一步分析、证实上面的结论

1、Activity对点击事件的分发过程

点击事件用 MotionEvent 来表示,当一个点击操作发生时,事件最先传递给当前 Activity,由 Activity 的 dispatchTouchEvent 来进行事件派发,具体的工作是由 Activity 内部的 Window 来完成的

Window 会将事件传递给 decorview,decorview 一般就是当前界面的底层容器 ( 即 setContentView 所设置的 View 的父容器 ),通过Activity.getWindow.getDecorView() 可以获得。我们先从 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 所附属的 Window 进行分发,如果返回 true,整个事件循环就结束了,返回 false 意味着事件没人处理,所有 View onTouchEvent 都返回了 false,那么 Activity onTouchEvent 就会被调用。

Window 是如何将事件传递给 ViewGroup 

接下来看 Window 是如何将事件传递给 ViewGroup 的。通过源码我们知道,Window是个抽象类,而Window的 superDispatchTouchEvent方法也是个抽象方法,因此我们必须找到 Window 的实现类才行。

源码:Window#superDispatchTouchEvent

public abstract boolean superDispatchTouchEvent(MotionEvent event);

Window 的唯一实现是 PhoneWindow

那么到底 Window 的实现类是什么呢?其实是 PhoneWindow,这一点从 Window 的源码中也可以看出来,在 Window 的说明中,有这么一段话:

Abstract base class for a top-level window look and behavior policy. 
An instance of this class should be used as the top-level view added to the window manager. 
It provides standard UI policies such as a background, title area, default key processing, etc.

The only existing implementation of this abstract class is android.
policy.PhoneWindow, which you should instantiate when needing a Window. 
Eventually that class will be refactored and a factory method added for 
creating Window instances without knowing about a particular implementation.

上面这段话的大概意思是:Window 类可以控制顶级 View 的外观和行为策略,它的唯一实现位于 android.policy.PhoneWindow 中,当你要实例化这个 Window 类的时候,你并不知道它的细节,因为这个类会被重构,只有一个工厂方法可以使用。尽管这看起来有点模糊,不过我们可以看一下 android.policy.PhoneWindow 这个类,尽管实例化的时候此类会被重构,仅是重构而已,功能是类似的。

由于 Window 的唯一实现是 PhoneWindow,因此接下来看一下 PhoneWindow 是如何处理点击事件的,如下所示。

源码:PhoneWindow#superDispatchTouchEvent

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

PhoneWindow 传递给了 DecorView,DecorView 是什么

到这里逻辑就很清晰了,PhoneWindow 将事件直接传递给了 DecorView,这个 DecorView 是什么呢?请看下面:

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

    @Override
    public final View getDecorView(){
        if(mDecor == null){
            installDesor():
        }
        return mDecor;
    }
}

我们知道,通过  ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)  这种方式就可以获取Activity 所设置的 View,这个 mDecor 显然就是 getWindow().getDecorView() 返回的 View,而我们通过 setContentView 设置的 View 是它的一个子 View

目前事件传递到了 DecorView 这里,由于 DecorView 继承自 FrameLayout 且是父 View,所以最终事件会传递给 View。换句话来说,事件肯定会传递到 View,不然应用如何响应点击事件呢?

不过这不是我们的重点,重点是事件到了 View 以后应该如何传递,这对我们更有用。从这里开始,事件已经传递到顶级 View 了,即在Activity 中通过 setContentView 所设置的View另外顶级 View 也叫根 View,顶级 View 一般来说都是 ViewGroup。

2、顶级View对点击事件的分发过程

关于点击事件如何在 View 中进行分发,上一节已经做了详细的介绍,这里再大致回顾一下。点击事件达到顶级 View ( —般是一个ViewGroup ) 以后,会调用 ViewGroup dispatchTouchEvent 方法,然后的逻辑是这样的:

如果顶级 ViewGroup 拦截事件,即 onlnterceptTouchEvent 返回 true,则事件由 ViewGroup 处理。这时如果 ViewGroup mOnTouchListener 被设置,则 onTouch 会被调用,否则 onTouchEvent 会被调用。也就是说,如果都提供的话,onTouch 会屏蔽掉 onTouchEvent。 

在 onTouchEvent 中,如果设置了 mOnClickListener,则 onClick 会被调用。如果顶级 ViewGroup 不拦截事件,则事件会传递给它所在的点击事件链上的子 View,这时子 View 的 dispatchTouchEvent 会被调用。到此为止,事件已经从顶级 View 传递给了下一层 View,接下来的传递过程和顶级 View 是一致的,如此循环,完成整个事件的分发。

首先看 ViewGroup 对点击事件的分发过程,其主要实现在 ViewGroup 的 dispatchTouchEvent 方法中,这个方法比较长,这里分段说明。先看下面一段,很显然,它描述的是当前 View 是否拦截点击事情这个逻辑。

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

从上面代码我们可以看出,ViewGroup 在如下两种情况下会判断是否要拦截当前事件:事件类型为 ACTION_DOWN 或者 mFirstTouchTarget != nullACTION_DOWN 事件好理解,那么 mFirstTouchTarget != null 是什么意思呢?

这个从后面的代码逻辑可以看出来,当事件由 ViewGroup 的子元素成功处理时,mFirstTouchTarget 会被赋值并指向子元素,换种方式来说,当 ViewGroup 不拦截事件并将事件交由子元素处理时 mFirstTouchTarget != null。反过来,一旦事件由当前 ViewGroup 拦截时, mFirstTouchTarget != null 就不成立。

那么当 ACTION_MOVE ACTION_UP 事件到来时,由于 (actionMasked == MotionEvent. ACTION DOWN || mFirstTouchTarget != null) 这个条件为 false,将导致 ViewGroup 的 onlnterceptTouchEvent 不会再被调用,并且同一序列中的其他事件都会默认交给它处理。

当然,这里有一种特殊情况,那就是 FLAG_DISALLOW_INTERCEPT 标记位,这个标记位是通过 requestDisallowInterceptTouchEvent方法来设置的,一般用于子 View 中。 FLAG_DISALLOW_INTERCEPT 一旦设置后, ViewGroup 将无法拦截除了 ACTION_DOWN 以外的其他点击事件。为什么说是除了 ACTION_DOWN 以外的其他事件呢?

这是因为 ViewGroup 在分发事件时,如果是 ACTION_DOWN 就会重置 FLAG_DISALLOW_INTERCEPT 这个标记位,将导致子 View 中设置的这个标记位无效。 因此,当面对 ACT1ON_DOWN 事件时,ViewGroup 总是会调用自己的 onlnterceptTouchEvent 方法来询问自己是否要拦截事件,这一点从源码中也可以看出来。

在下面的代码中,ViewGroup 会在 ACTION_DOWN 事件到来时做重置状态的操作,而在 resetTouchState 方法中会对 FLAG_DISALLOW INTERCEPT 进行重置,因此子 View 调用 request- DisallowInterceptTouchEvent 方法并不能影响 ViewGroup ACTION DOWN 事件的处理。

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

从上面的源码分析,我们可以得出结论:当 ViewGroup 决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的onlnterceptTouchEvent 方法,这证实了 3.4.1节 末尾处的第3条结论。FLAG_DISALLOW_INTERCEPT 这个标志的作用是让 ViewGroup不再拦截事件,当然前提是 ViewGroup 不拦截 ACTION_DOWN 事件,这证实 了 3.4.1节 末尾处的第11条结论。那么这段分析对我们有什么价值呢?

总结起来有两点:

第一点,onlnterceptTouchEvent 不是每次事件都会被调用的,如果我们想提前处理所有的点击事件,要选择dispatchTouchEvent 方法,只有这个方法能确保每次都会调用,当然前提是事件能够传递到当前的 ViewGroup

另外一点,FLAG_DISALLOW_INTERCEPT 标记位的作用给我们提供了一个思路,当面对滑动冲突时,我们可以是不是考虑用这种方法去解决问题?关于滑动冲突,将在3.5节进行详细分析。

接着再看当 ViewGroup 不拦截事件的时候,事件会向下分发交由它的子 View 进行处理,这段源码如下所示。

                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                   ? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)
                                   ? children[childindex] : preorderedList.get(childlndex);
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                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;
                            }

上面这段代码逻辑也很清晰,首先遍历 ViewGroup 的所有子元素,然后判断子元素是否能够接收到点击事件。

是否能够接收点击事件主要由两点来衡量:子元素是否在播动画和点击事件的坐标,是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理。

可以看到,dispatchTransformedTouchEvent 实际上调用的就是子元素的 dispatchTouchEvent 方法,在它的内部有如下一段内容,而在上面的代码中 child 传递的不是null,因此它会直接调用子元素的 dispatchTouchEvent 方法,这样事件就交由子元素处理了,从而完成了一轮事件分发。

if (child == null) {
      handled = super.dispatchTouchEvent(event);
} else {
     handled = child.dispatchTouchEvent(event);
}

如果子元素的 dispatchTouchEvent 返回 true,这时我们暂时不用考虑事件在子元素内部是怎么分发的,那么 mFirstTouchTarget 就会被赋值同时跳出 for 循环,如下所示。

newTouchTarget = addTouchTarget(child, idBitsToAssign); 
alreadyDispatchedToNewTouchTarget = true;
break;

这几行代码完成了 mFirstTouchTarget 的赋值并终止对子元素的遍历。如果子元素的 dispatchTouchEvent 返回 false,ViewGroup 就会把事件分发给下一个子元素(如果还有下一个子元素的话)。

其实 mFirstTouchTarget 真正的赋值过程是在 addTouchTarget 内部完成的,从下面的 addTouchTarget 方法的内部结构可以看出,mFirstTouchTarget 其实是一种单链表结构。 mFirstTouchTarget 是否被赋值,将直接影响到 ViewGroup 对事件的拦截策略,如果 mFirstTouchTarget 为 null,那么 ViewGroup 就默认拦截接下来同一序列中所有的点击事件,这一点在前面已经做了分析。

private TouchTarget addTouchTarget(View child, int pointerIdBits) {
          TouchTarget target = TouchTarget.obtain(child, pointerldBits);
          target.next = mFirstTouchTarget;
          mFirstTouchTarget = target;
          return target;
}

如果遍历所有的子元素后事件都没有被合适地处理,这包含两种情况:

第一种是 ViewGroup 没有子元素;

第二种是子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了 false,这一般是因为子元素在 onTouchEvent 中返回了 false,在这两种情况下,ViewGroup 会自己处理点击事件,这里就证实了 3.4.1 节中的第4条结论,代码如下所示。

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

     }

注意上面这段代码,这里第三个参数 child 为 null,从前面的分析可以知道,它会调用 super.dispatchTouchEvent(event), 很显然,这里就转到了 View dispatchTouchEvent 方法,即点击事件开始交由 View 来处理,请看下面的分析。

4、View对点击事件的处理过程

View 对点击事件的处理过程稍微简单一些,注意这里的 View 不包含 ViewGroup先看它的 dispatchTouchEvent 方法,如下所示。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

被开发耽误的大厨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值