Android开发中的事件分发机制梳理

本文为事件分发的学习总结。
《Android开发艺术探索》一书中对事件分发做了很详细的介绍。
大神博客:http://blog.csdn.net/singwhatiwanna

View的事件分发机制

  • 事件的传递机制,指的就是事件的分发,也就是对MotionEvent事件的分发过程。

MotionEvent类

  • MotionEvent:手指接触屏幕后产生的事件,封装成了MotionEvent类
  • 典型的事件类型(MotionEvent类中的int型常量):
    • ACTION_DOWN:手指刚接触屏幕 (按下) 值为:0
    • ACTION_UP:手指在屏幕上松开的一瞬间 (抬起) 值为:1
    • ACTION_MOVE:手指在屏幕上移动 值为:2
  • 事件序列:手指接触屏幕后产生的一系列事件,就称为事件序列
    • 点击屏幕后离开松手,事件序列:DOWN–>UP
    • 点击屏幕滑动一会再松手,事件序列:DOWN–>MOVE–>MOVE–>…–>MOVE–>UP
  • 事件分发:当MotionEvent产生之后,系统要把这个事件传递给一个具体的view,这个传递的过程就是分发的过程

事件分发的主要方法

  • 事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent

dispatchTouchEvent

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

onInterceptTouchEvent

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

onTouchEvent

  • 在dispatchTouchEvent方法中调用,用来处理事件,返回结果表示是否消耗当前事件,如果不消耗(返回false),则在同一个事件序列中,当前View无法再次接收到后续的事件队列
  • onTouchEvent默认的返回值由clickable和longClickable共同决定,只要有一个为true,onTouchEvent的返回值就是true,longClickable的默认值都为false,clickable的默认值分情况,Button为true,TextView为false。

dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent的关系

//有一个LinearLayout对象:demoLayout
//当demoLayout被点击时,先执行demoLayout的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent event){
    boolean consume(是否消耗) = false;(不消耗)
    boolean intercept(是否拦截此事件) = onInterceptTouchEvent(event//(该方法也是demoLayout的方法,并且该方法会返回一个boolean的值来判断是否要拦截此次事件)
    if(intercept){
        //如果拦截此事件,那么执行demoLayout自己的onTouchEvent方法
        consume(是否消耗) = onTouchEvent(event);
        //(调用的也是demoLayout的onTouchEvent方法,并且该方法会返回一个boolean的值来判断是否消耗当前事件)
    }else{
        //如果不拦截此事件,那么事件会传递到demoLayout的子视图的dispatchTouchEvent方法中
        consume(是否消耗) = childView.dispatchTouchEvent(event);
        //(这时候的dispatchTouchEvent方法就是childView的方法了,不再是demoLayout自己的了)
        //而childView调用了dispatchTouchEvent后,又会走一遍childView自己的流程
        //当所有都走完(一层一层下去,一层一层返回来后),下面的return consume才会执行
        //如果所有的子View都没有消耗该事件,那么会执行demoLayout的onTouchEvent方法
        //若onTouchEvent返回false,则后续的事件(比如抬起)都不会被响应
    }
    return consume;

Android开发中的事件分发机制梳理_第1张图片

事件的传递过程

  • 对应外层的根ViewGroup,举例为demoLayout。
  • 事件产生后,因为demoLayout在外层,所以demoLayout先接受到这个事件对象event,并且将这个event对象传递给它(demoLayout)本身的dispatchTouchEvent方法。在dispatchTouchEvent中会调用onInterceptTouchEvent方法,
    • 如果onInterceptTouchEvent返回true,那么说明这次触摸事件,demoLayout本身要响应,也就是拦截该事件,不再向下(子视图)传递。
    • 如果onInterceptTouchEvent返回false,那么说明这次触摸事件,demoLayout不响应,不拦截,会将该事件event传递到子视图中(向下传递),接着子视图的dispatchTouchEvent就会被调用,直到事件被最终处理,整个分发过程才结束。

案例演示:自定义RelativeLayout

  • 建立类EventRelativeLayout类,继承RelativeLayout
  • 复写前两个构造方法
  • 建立一个String类型的TAG,用来标识log日志
  • 复写方法dispatchTouchEvent(),在该方法中输出log
  • 复写方法onInterceptTouchEvent(),在该方法中输出log
  • 复写方法onTouchEvent(),在该方法中输出log
  • ev.getAction输出的是事件的类型
public class EventRelativeLayout extends RelativeLayout {

    private static final String TAG = "EventRelativeLayout";

    public EventRelativeLayout(Context context) {
        (context);
    }

    public EventRelativeLayout(Context context, AttributeSet attrs) 
    {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "dispatchTouchEvent: 
                ---父容器的分发事件"+ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(TAG, "onInterceptTouchEvent: 
                ---父容器的拦截事件"+ev.getAction() );
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: 
                ---父容器的触摸事件发生"+event.getAction());
        return super.onTouchEvent(event);
    }
}

案例演示:自定义Button

  • 建立类EventButton类,继承Button
  • 复写前两个构造方法
  • 建立一个String类型的TAG,用来标识log日志
  • 复写方法dispatchTouchEvent(),在该方法中输出log
  • 复写方法onTouchEvent(),在该方法中输出log
  • ev.getAction输出的是事件的类型
public class EventButton extends Button {

    private static final String TAG = "EventRelativeLayout";

    public EventRelativeLayout(Context context) {
        (context);
    }

    public EventRelativeLayout(Context context, AttributeSet attrs) 
    {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "dispatchTouchEvent: ---"+ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent: ---"+event.getAction());
        return super.onTouchEvent(event);
    }
}

案例演示:在xml文件中使用

  • 在layout布局文件中建立一个EventRelativeLayout
  • 在该EventRelativeLayout中建立一个EventButton
  • 运行程序,点击EventButton,观察日志(注意:没做任何绑定组件监听事件的东西,只是单纯的在xml中使用)

Android开发中的事件分发机制梳理_第2张图片

案例演示:log日志

  • 按log出现顺序分析
  • EventRelativeLayout: dispatchTouchEvent: —父容器的分发事件0
    • 先执行了父容器的分发事件方法,获得的事件为0,也就是按下操作
  • EventRelativeLayout: onInterceptTouchEvent: —父容器的拦截事件0
    • 然后执行父容器的拦截方法,操作的事件与分发方法中的是一个
  • EventButton: dispatchTouchEvent: —分发事件0
    • 然后执行button的分发方法
  • EventButton: onTouchEvent: —触摸事件发生0

    • 然后执行button的onTouchEvent方法
  • EventRelativeLayout: dispatchTouchEvent: —父容器的分发事件1

    • 先执行了父容器的分发事件方法,获得的事件为1,也就是抬起操作
  • EventRelativeLayout: onInterceptTouchEvent: —父容器的拦截事件1
    • 然后执行父容器的拦截方法,操作的事件与分发方法中的是一个
  • EventButton: dispatchTouchEvent: —分发事件1
    • 然后执行button的分发方法
  • EventButton: onTouchEvent: —触摸事件发生1
    • 然后执行button的onTouchEvent方法

案例演示:分析log日志

  • 点击button后,出现了两次流程,并且一个事件为0,一个为1
  • 可以看出,点击操作会被拆分成两个事件,一个是按下,一个是抬起

案例演示:将EventRelativeLayout的拦截事件修改为返回true

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(TAG, "onInterceptTouchEvent: 
                            ---父容器的拦截事件"+ev.getAction());
        return true;
    }

Android开发中的事件分发机制梳理_第3张图片

  • 运行程序,点击Button,观察log日志
  • 发现这回的log只有Layout的ACTION_DOWN的log,抬起的log没有出现
  • Button的log也没有出现
  • 并且多了一个onTouchEvent的log
  • 说明EventRelativeLayout将这次事件拦截了,事件并没有向下传递

案例演示:为什么只有按下的log出现了呢?

  • 在xml文件中,设置EventRelativeLayout的属性clickable为true
  • 运行程序,点击button后观察log

Android开发中的事件分发机制梳理_第4张图片

案例演示:更改clickable属性为true后的log日志

  • EventRelativeLayout: dispatchTouchEvent: —父容器的分发事件0
    • 先执行了父容器的分发事件方法,获得的事件为0,也就是按下操作
  • EventRelativeLayout: onInterceptTouchEvent: —父容器的拦截事件0
    • 然后执行父容器的拦截方法,操作的事件与分发方法中的是一个
  • EventRelativeLayout: onTouchEvent: —父容器的触摸事件发生0
    • 执行了父容器的onTouchEvent方法
  • EventRelativeLayout: dispatchTouchEvent: —父容器的分发事件1
    • 先执行了父容器的分发事件方法,获得的事件为1,也就是抬起操作
  • EventRelativeLayout: onTouchEvent: —父容器的触摸事件发生1
    • 执行了父容器的onTouchEvent方法

案例演示:分析log日志

  • 更改clickable属性为true后,发现抬起事件的日志出现了
  • clickable属性:是否能响应点击事件
  • 这个属性,一般的View的默认值都为false,Button的默认值为true
  • 因为EventRelativeLayout将这次事件拦截了,所以该事件不会向下传递,会执行onTouchEvent
  • 但是因为EventRelativeLayout的属性clickable默认为false,EventRelativeLayout不可响应点击事件,所以接收不到后续的事件队列(抬起的动作)
  • 而将clickable属性改为true后,EventRelativeLayout可以响应点击事件了,所以可以接收到抬起动作的事件,又由于onInterceptTouchEvent方法返回true,说明该事件队列已经被拦截了,会执行onTouchEvent(按下事件的),只要拦截了某事件,后续的事件队列就不需要再做一次判断了,所以能看到分发事件的日志,但是看不到onInterceptTouchEvent的log日志,就直接onTouchEvent(抬起事件的)了

案例演示总结

当父容器EventRelativeLayout不拦截事件时

* 点击Button,事件会传递到子视图Button中,因为Button的clickable的默认值为true,调用Button的onTouchEvent方法。
    * 该方法的默认返回值为true(因为Button的clickable默认为true),那么会消耗掉该事件,并且可以接收后续的事件序列,并且父容器的

onTouchEvent方法不会被执行
* 如果将该方法的返回值改为false,表示不会消耗该事件,会将该事件传递给父容器的onTouchEvent,并且不会再接受其后续事件队列。
* 因为其父容器clickable为true,说明可以接收到(抬起)后续的事件队列。
* 但是不会调用父容器的拦截事件方法(因为子View已经拒绝了一次,后续事件都不需要再传递给子View,自然不需要多做一次是否拦截的判断)
* 如果将Button的clickable设置为false,那么调用Button的onTouchEvent方法时该方法会返回false(则不可以接收后续的事件序列)。
* 会一层层回调父容器的onTouchEvent方法

当父容器EventRelativeLayout拦截事件时

* 点击Button,事件会被父容器EventRelativeLayout拦截,执行父容器的onTouchEvent方法
    * 若父容器EventRelativeLayout的clickable属性为false,执行onTouchEvent方法
        * 因clickable属性为false,则onTouchEvent也会返回false,无法接收到后续的抬起事件
    * 若父容器EventRelativeLayout的clickable属性为true,执行onTouchEvent方法
        * 因clickable属性为true,则onTouchEvent也会返回true,可以接收后续的抬起事件

Android开发中的事件分发机制梳理_第5张图片

当EventButton的onTouchEvent的返回值被修改时

  • 如果对EventButton绑定组件设置了onClickListener
  • 那么该onClickListener的onClick方法不会被回调
  • 因为回调onClick方法是在父类的onTouchEvent中执行的
  • 如果将返回值super.onTouchEvent(event)修改,那么不会调用父类的onTouchEvent
  • 也就不会去回调onClick方法

事件分发总结

  • 当一个点击事件产生后,它的传递过程遵循如下规则:Activity->Window->View,即事件总是先传递给Activity,Activity再传递给Window,Window在传递给顶级View-DecorView,DecorView接收到事件后,就会按照事件分发机制去分发事件。
  • 如果一个View-A的onTouchEvent返回false(能传递到A的onTouchEvent,说明A的onInterceptTouchEvent方法返回true,并且说明A的父View的onInterceptTouchEvent返回flase),那么说明A的dispatchTouchEvent会返回false,那么最后事件就被父view处理,如果所有View都不处理这个事件,那么事件最后会由Activity处理。

事件分发模拟举例

  • 技术总监接到了一个任务,(Activity接收到了一个手指点击的事件)
  • 总监将任务分配给了副总监,等待副总监回复(Activity将事件传递到了Window)
  • 副总监将任务分配给了Android主管,等待主管回复(Window将事件传递给了DecorView)

    • Android主管将任务分配给了项目组长,等待项目组长回复(DecorView将事件传递给了ContentView,也就是Activity绑定的xml文件中的根视图)(R.layout.activity_main中最外层的那个视图,假设叫:eventView)
      • 项目组长扫了一圈项目里的员工(遍历eventView中的子视图),找到了带你的那个前辈relativeLayout,因为前辈擅长任务的这个方向,等待前辈回复(点击事件的坐标落在该子元素的区域中)
        • 前辈瞅了你一眼,把任务交给了你,等待你回复(事件传递到了relativeLayout中的子view)
          • 你一看任务好简单,写了三天搞定了,上级觉得很满意,以后有任务还交给你(子视图响应了事件,消耗了事件,onTouchEvent返回true,可接收后续事件)
          • 你一听,让老子做这玩意?不干了,离职了(onTouchEvent返回false,则接收不到后续事件)
          • 你以为任务好简单,写了三天啥也没写出来(子视图不相应该事件,不消耗该事件)
        • 前辈说你好菜,新手就是新手,看我教你吧(事件传递到了relativeLayout)
          • 前辈就是前辈,一天就搞定了(relativeLayout消耗了事件)
          • 半天后,前辈说:这个任务看似简单,实则复杂,再拖就交不了工了。(relativeLayout不消耗该事件,onTouchEvent返回false,向上传递)找项目组长去了
      • 项目组长寻思你(前辈)是这个方向最擅长的了,我还是去找主管吧(eventView也不响应)
    • 主管都多长时间不敲代码了,直接把任务还给了副总监
    • 副总监要管的事情太多,就和技术总监说这个任务开发人员完不成
    • 总监说那拉倒吧

    • 如果技术总监想了想,觉得下面的人应该写不出来(判断是否拦截任务,若拦截),那么后续的任务会再判断是否要拦截,直接执行总监的onTouchEvent

你可能感兴趣的:(Android)