View的事件分发机制分析

来源:互联网 发布:pycharm tensorflow 编辑:程序博客网 时间:2024/05/22 05:29

  在自定义View中,如果想要你的View能够处理各种屏幕事件,那么事件的分发机制是你必须要知道的,它是事件处理中的重点也是难点,后面将会从源码的角度来分析android中的事件分发。

  android的事件分发机制与3个方法密切相关:dispatchTouchEvent(MotionEvent event)、onInterceptTouchEvent(Motion Event event)和onTouchEvent(MotionEvent event),这3个方法有什么关系,它们是怎么执行的,同时又牵扯到一个onTouch(),这又是为什么?


一、MotionEvent

  MotionEvent,表示用户出发屏幕的一系列点击事件,它常用的事件类型有3种:

  • ACTION_DOWN:表示手指刚刚接触屏幕一瞬间触发的事件。
  • ACTION_MOVE:表示手指在屏幕上移动出发的一系列事件。
  • ACTION_UP:表示手指在屏幕上松开一瞬间触发的事件。

      通过MotionEvent对象,可以获取该事件的一切相关信息,如坐标。通过getX()和getY()获取按下的点到当前View左上角的坐标(即处理该事件的View)。或者可以通过getRawX()和getRawY()可以获取按下的点到屏幕左上角的坐标。

二、事件分发和处理的流程

  当事件被系统捕获到后,会将该事件交给栈顶的activity来处理(因为只有栈顶的activity才能与用户进行交互),然后activity将事件交给window对象,window对象又将事件交给顶层的ViewGroup来处理。接着该顶层ViewGroup会调用它的dispatchTouchEvent(MotionEvent event)方法,即开始事件向下时间分发,在该方法内部会调用onInterceptTouchEvent(Motion Event event)方法,表示分发事件的时候是否要进行拦截事件:

  • 如果返回true,表示要拦截该事件,即事件不会向下层子View进行分发,这时会判断是否设置类OnTouchListener监听器,如果有就调用监听器的onTouch()方法,否则就会调用该ViewGroup的onTouchEvent(MotionEvent)方法进行事件的处理。
  • 如果onInterceptTouchEvent(Motion Event event)返回false,那么在dispatchTouchEvent(MotionEvent event)方法中就会调用它的子View的dispatchTouchEvent(MotionEvent event)方法,这样依次循环直到该事件传到对应的View中进行处理。

下面以一张流程图来显示上面的执行过程:

      事件分发流程


三、下面以示例代码演示上述流程

为了方便,这里定义了两个ViewGroup类,都是继承RelativeLayout(不用重写onLayout()方法),还定义了一个View类继承自View类,代码如下所示:

ViewGroup1如下:

public class ViewGroup1 extends RelativeLayout {    private final String TAG = "ViewGroup1";    public ViewGroup1(Context context) {        super(context);    }    public ViewGroup1(Context context, AttributeSet attrs) {        super(context, attrs);    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        LogUtil.d(TAG,"dispatchTouchEvent");        return super.dispatchTouchEvent(ev);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        LogUtil.d(TAG,"onInterceptTouchEvent");        return super.onInterceptTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        LogUtil.d(TAG,"onTouchEvent");        return super.onTouchEvent(event);    }}

ViewGroup2如下:

public class ViewGroup2 extends RelativeLayout {    private final String TAG = "ViewGroup2";    public ViewGroup2(Context context) {        super(context);    }    public ViewGroup2(Context context, AttributeSet attrs) {        super(context, attrs);    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        LogUtil.d(TAG, "dispatchTouchEvent");        return super.dispatchTouchEvent(ev);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        LogUtil.d(TAG, "onInterceptTouchEvent");        return super.onInterceptTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        LogUtil.d(TAG,"onTouchEvent");        return super.onTouchEvent(event);    }}

View3如下:

public class View3 extends View {    private final String TAG = "View3";    public View3(Context context) {        super(context);    }    public View3(Context context, AttributeSet attrs) {        super(context, attrs);    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        LogUtil.d(TAG, "dispatchTouchEvent");        return super.dispatchTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        LogUtil.d(TAG, "onTouchEvent");        return super.onTouchEvent(event);    }}

布局文件如下:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <com.taiyang.test.ViewGroup1        android:layout_width="300dp"        android:layout_height="300dp"        android:background="#000000">        <com.taiyang.test.ViewGroup2            android:layout_width="200dp"            android:layout_height="200dp"            android:background="#0000ff">            <com.taiyang.test.View3                android:layout_width="100dp"                android:layout_height="100dp"                android:background="#ffffff"/>        </com.taiyang.test.ViewGroup2>    </com.taiyang.test.ViewGroup1></RelativeLayout>

界面如下所示:

          

白色区域表示:View3

蓝色区域表示:ViewGroup2

黑色区域表示:ViewGroup1

  • 情况一:当以上三个方法都调用父类的方法时,点击白色区域,打印结果如下:

             

      下面来一步步分析:

      1、ViewGroup1是ViewGroup2的父控件,ViewGroup2是View3的父控件。当在白色区域出发点击事件时,事件首先会传到ViewGroup1这个控件中(注:其实事件是从activity再到Window再到最顶层的DecorView中的,然后由DecorView再向下传递,这里先不讨论这些,因为向下传递时事件最终会到达ViewGroup1这里的,后面再分析activity到Window再到DecorView的情况),在事件到达ViewGroup1时就会调用它的dispatchTouchEvent()方法,从日志就可以看到,这个方法最先调用。

      2、接着在onTouchEvent()方法中调用onInterceptTouchEvent()方法,判断是否拦截事件,从日志中也可以看到该方法被调用了,这里其实返回的是false,因为调用的是父类方法,在父类方法中就直接返回了false,待会从源码中就可以看到这一点。

      3、因为ViewGroup1不拦截事件,所以这时事件就继续向下传递,即传递到ViewGroup2中(因为ViewGroup1就只有ViewGroup2这一个子View),接着调用ViewGroup2的dispatchTouchEvent()方法,日志中也看到是走的这一步。

      4、接着在ViewGroup2的dispatchTouchEvent()方法中调用它自己的onInterceptTouchEvent()方法,日志中这一步打印了,因为返回false(调用父类的方法,父类中返回false)。

      5、因为ViewGroup2不拦截事件,所以事件向下传递到View3中,这时调用了View3的dispatchTouchEvent()方法,因为View(不包含ViewGroup,ViewGroup是View的子类)是没有onInterceptTouchEvent()方法的,View不需要向下传递事件,它是没有子View的,所以也就不存在拦截事件的说法,这时就调用了View3的onTouchEvent()方法,从日志就可以看到。

      6、在onTouchEvent()方法中又调用了父类的方法,该方法的执行结果是返回false,不过父类的方法比较复杂,这里暂时不分析,因为返回了false,则表示不消耗该事件,这时事件又会向父控件传递,让父控件来消耗事件,从日志中可以看到这里调用了ViewGroup2的onTouchEvent()方法。

      7、因为ViewGroup2中同样调用了父类的方法,同样返回了false,这时事件又继续向父控件传递,交给了ViewGroup1来处理,日志最后也打印了该方法,同样该方法也是返回false,接着继续向上传递交给父控件来处理,不过这里没有做打印处理,但是该事件最后会传到顶层的ViewGroup的,即DecorView那里。

      经过这样的一步步的分析,我们对事件的分发处理就有了一个清晰的认识了,不过这里只是最基本的,下面继续讨论一些其他情况。

  • 情况二:如果ViewGroup1的dispatchTouchEvent()方法不调用父类的方法,之间就返回true或者false,点击屏幕,看打印结果:

1、返回true的情况:

        

2、返回false的情况:

        

  它们的区别在于:当dispatchTouchEvent()返回true的时候,即不调用父类方法,点击的时候该方法会执行多次,而直接返回false的时候(不走父类方法),只执行了一次,这是为什么呢?

  原因是:当点击屏幕的时候,点击的一瞬间会出发一个ACTION_DOWN事件,这时会执行dispatchTouchEvent()方法一次,如果它的返回值为true,那么该View或者ViewGroup会消耗此系列的事件,所以接着各种移动事件,即ACTION_MOVE和最后的ACTION_MOVE都会执行,所以该方法会执行多次。需要注意的是这里的一系列事件是指从手指接触屏幕开始触发的ACTION_DOWN事件一直到手指离开屏幕,即ACTION_UP事件,这一段事件内的所有事件即表示为一系列事件。但是如果该方法返回false,即表示该View或者ViewGroup不消耗此系列的事件,所有当后面的各种ACTION_MOVE和ACTION_UP事件到来时,该方法是不会执行的,因为不消耗此系列事件,所以返回false的时候只执行了一次也就是这个原因,这一次执行是出发ACTION_DOWN的时候执行的。因此在自定义View的时候,不建议覆写dispatchTouchEvent()方法,而是直接调用父类的方法,因为只有这样事件才能继续向下分发下去。父类中该方法比较复杂,后面再来分析它。

  • 情况三:onInterceptTouchEvent()的返回值为true或者false时,这种情况比较简单,如果为false,那么就继续向下分发事件,否则就调用自己的onTouchEvent()处理事件。因此父类的方法默认也是返回false的,即默认是向下传递的。需要注意的是:只有ViewGroup才有onInterceptTouchEvent()方法,因为只有它才能向子View拦截或者不拦截事件,而View是没有onInterceptTouchEvent()方法的,因为没有子View,所有不存在拦截事件的情况。如下是当onInterceptTouchEvent()返回true的情况,即不向下继续分发事件,这时就直接调用自己的onTouchEvent()方法进行处理。

            

  • 情况四:当View中设置了OnTouchListener监听器。它的优先级比onTouchEvent()要高,即先执行OnTouchListener的onTouch()方法,如果该方法的返回值返回true,那么就不会再执行onTouchEvent(),如果返回false,则会接着调用onTouchEvent()。

            

        如果OnTouchListener的onTouch()方法返回true,打印结果如图所示:

            

        从打印结果可以看到,ViewGroup1的onTouchEventListener执行到了,因为返回true,所以并没有执行onTouchEvent方法,同时因为返回true,表示要消耗此系列的事件,所以当松开的时候,事件并不会继续向下分发,而是事件传递到ViewGroup1的时候就直接处理掉了。所以dispatchTouchEvent()方法调用后接直接调用OnTouchListener()的onTouch方法了。

            

        如果OnTouchListener的onTouch()方法返回false,打印结果如图所示:

            

        当onTouch()方法返回false时,会接着调用onTouchEvent()方法,同时因为onTouchEvent()中默认调用父类的方法,所以此处不消耗该事件,并交由父View来进行处理,所以后续的该系列事件将不会接收到。

通过以上的分析,我们对View的事件分发处理机制就有了一个非常清晰的认识。

0 0
原创粉丝点击