Android的MotionEvent事件传递机制

  事件的分发,其实在我看来包括两种:一种是KeyEvent按键事件分发,另一种是MotionEvent触屏事件分发。只是我们一般把MotionEvent事件分发叫做View的事件分发。
  View事件分发,其实就是对MotionEvent对象的分发过程,即当一个MotionEvent产生以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。View事件的分发过程由三个很重要的方法来完成:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()。
  一次完整的事件传递主要包括三个阶段,分别是事件的分发、拦截和消费。在Activity、View和ViewGroup三者中进行事件传递。

触摸事件的类型

  触摸事件对应的是MotionEvent类,事件的类型主要有如下三种:

  • ACTION_DOWN:用户手指的按下操作,一个按下操作标志着一次触摸事件的开始;
  • ACTION_MOVE:用户手指按压屏幕后,在松开之前,如果移动的距离超过一定的阈值,那么会被判定为ACTION_MOVE操作。一般情况下,手指的轻微移动都会触发一系列的移动事件;
  • ACTION_UP:用户手指离开屏幕的操作,一次抬起操作标志着一次触摸事件的结束;
  • ACTION_CANCEL:用户手指滑出控件边界时的操作;
事件 触发场景 单词事件流中触发的次数
MotionEvent.ACTION_DOWN 在屏幕按下时 1次
MotionEvent.ACTION_MOVE 在屏幕上滑动时 0次或多次
MotionEvent.ACTION_UP 在屏幕抬起时 0次或多次
MotionEvent.ACTION_CANCEL 滑动超出控件边界时 0次或1次

事件传递的三个阶段

  在安卓系统中,拥有事件传递处理能力的类有以下三种:

  • Activity:拥有dispatchTouchEvent和onTouchEvent两个方法;
  • View:拥有dispatchTouchEvent和onTouchEvent两个方法;
  • ViewGroup:拥有dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法;

  事件传递的三个阶段:
1 . 分发:事件的分发对应着dispatchTouchEvent方法,在安卓系统中,所有的触摸事件都是通过这个方法来分发的。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent()方法和下级View的dispatchTouchEvent()方法的影响,表示是否消耗当前事件。方法如下:

public boolean dispatchTouchEvent(MotionEvent ev){
}

  在这个方法中,根据当前视图的具体实现逻辑,来决定是直接消费这个事件还是将事件继续分发给子视图处理,方法返回值为true表示事件被当前视图消费掉,不再继续分发事件;方法返回值为super.dispatchTouchEvent表示继续分发该事件。如果当前视图是ViewGroup及其子类,则会调用onInterceptTouchEvent方法判定是否拦截该事件。

2 . 拦截:事件的拦截对应着onInterceptTouchEvent方法,这个方法只在ViewGroup及其子类中才存在,在View和Activity中是不存在的。在ViewGroup类中,方法如下:

public boolean onInterceptTouchEvent(MotionEvent ev){
}

  这个方法通过返回的布尔值来决定是否拦截对应的事件,根据具体的实现逻辑,返回true表示拦截这个事件,不继续分发给子视图,同时交由自身的onTouchEvent方法进行消费;返回false或者super.onInterceptTouchEvent表示不对事件进行拦截,需要继续传递给子视图。如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用。

3 . 消费:事件的消费对应着onTouchEvent方法,方法如下:

public boolean onTouchEvent(MotionEvent event){
}

  该方法在dispatchTouchEvent()方法中调用。该方法返回布尔值表示当前视图是否处理事件。返回true表示当前视图可以处理对应的事件,事件将不会向上传递给父视图;返回值为false表示当前视图不处理这个事件,事件会被传递给父视图的onTouchEvent方法中进行处理。如果不消耗事件,在同一个事件序列中,当前View无法再接收到事件。

  上面这三个方法的关系,可以用下面的伪代码来表示:

public boolean dispatchTouchEvent(MotionEvent event){
    boolean consume = false;
    if(onInterceptTouchEvent(event)){
        consume = onTouchEvent(event);
    }else{
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}

  对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent()就会被调用,如果这个ViewGroup的onInterceptTouchEvent()方法返回true,就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent()方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent()方法返回false,就表示不拦截当前事件,这是当前事件就会继续传递给它的子元素,接着子元素的dispatchToucheEvent()方法就会被调用,如此反复直到事件被最终处理。

  当一个View需要处理事件时,如果它设置了onTouchListener,那么onTouchListener中的onTouch()方法就会被回调。这时事件如何处理还要看onTouch()方法的返回值,如果返回false,则当前View的onTouchEvent()方法会被调用;如果返回true,那么onTouchEvent()方法将不会被调用。由此可见,给View设置的onTouchListener,其优先级比onTouchEvent()方法要高。在onTouchEvent()方法中,如果当前设置的有onClickListener,其优先级最低,即处于事件传递的尾端。

  当一个点击事件产生后,它的传递过程遵循如下顺序:Activity -> Window -> View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View。顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况:如果一个View的onTouchEvent()方法返回false,那么它的父容器的onTouchEvent()方法将会被调用,依此类推。如果所有元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent()方法将会被调用。

  以下是一些总结:
1 . 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以ACTION_DOWN事件开始,中间含有数量不定的ACTION_MOVE事件,最终以ACTION_UP事件结束;

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

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

4 . 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN 事件(onTouchEvent()方法返回了false),那么同一事件序列中的其他事件都不会交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent()会被调用。也就是说,事件一旦交给了一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了。

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

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

7 . View没有onInterceptTouchEvent()方法,一旦有点击事件传递给它,那么它的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是可点击的,并且它收到了ACION_DOWN和ACION_UP的事件;

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

事件分发的源码解析

Activity对点击事件的分发过程

  点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件派发,具体的工作是由Activity内部的Window来完成的。Window会将事件传递给DecorView,DecorView是当前界面的底层容器(即setContentView()方法中所设置的layout布局的父容器),通过Activity.getWindow().getDecorView()方法可以获得。

  接下来,从Activity的dispatchTouchEvent()方法开始分析:

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

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-----------------last line for now----------------