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 会将事件传递给 decor view,decor view 一般就是当前界面的底层容器 ( 即 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 是否拦截点击事情这个逻辑。

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章