0%

View 的事件体系(四):View 的事件分发机制

1. 事件分发的概念

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

2. 点击事件分发过程中三个重要方法的区别及联系

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

  • public boolean onInterceptTouchEvent(MotionEvent event): 在上述方法内部调用,用来判断是否拦截某个事件,【如果当前 View 拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用】(拦截了局部即拦截了整体,不用多次调用多次拦截),返回结果表示是否拦截当前事件

  • public boolean onTouchEvent(MotionEvent event): 在 dispatchTouchEvent(MotionEvent event) 方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,【如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件】(如果局部不处理,则整体不再发送,事件序列类似事务的原子性,提高事件分发的效率

  • 伪代码表示三个方法的联系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public boolean dispatchTouchEvent(MotionEvent event) {
    boolean consume = false;
    if (onInterceptTouchEvent(event)) {
    consume = onTouchEvent(event);
    } else {
    consume = child.dispatchTouchEvent(event);
    }

    return consume;
    }

3. onTouch()onTouchEvent()onClick() 三个方法的优先级

  • 当一个 View 需要处理事件时,如果它设置了 OnTouchListener,那么 OnTouchListener 中的 onTouch() 方法会被回调

    • 这时事件如何处理还要看 onTouch() 方法的返回值,如果返回 false,则当前 View 的 onTouchEvent() 方法会被调用;如果返回 true,那么 onTouchEvent() 方法将不会被调用
    • 因此,给 View 设置的 OnTouchListener,其优先级比 onTouchEvent() 要高,这样做的好处是方便外界处理点击事件
  • onTouchEvent() 方法中,如果当前设置的有 OnClickListener,那么它的 onClick() 方法会被调用。由此可看出,平时常用的 OnClickListener,其优先级最低 ,即处于事件传递的尾端

4. 一个点击事件产生后,它的正/反传递顺序

  • Activity -> Window -> View

    • 即事件总是先传递给 Activity,Activity 再传递给 Window,最后 Window 再传递给顶级 View
    • 顶级 View 接收到事件后,就会按照事件分发机制去分发事件(如果 View 设置有代理,那么还会执行 TouchDelegate 的 onTouchDelegate() 方法)
    • 这里的顶级 View 一般是 decor view,decor view 一般就是当前界面的底层容器(即 setContentView() 所设置的 View 的父容器),通过 Activity.getWindow().getDecorView() 可以获得
  • 考虑一种情况(类比工作中,下级解决不了的问题逐级回溯给上级处理)

    • 如果一个 View 的 onTouchEvent() 方法返回 false,那么它的父容器onTouchEvent() 方法将会被调用,依次类推
    • 如果所有的元素都不处理这个事件,那么这个事件将会最终传递给 Activity 处理,及 Activity 的 onTouchEvent() 方法会被调用

5. 分析源码,事件传递机制的一些结论

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

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

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

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

  5. 如果 View 不消耗除 MotionEvent.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
    • 通过 setClickable()setLongClickable() 方法可以分别改变 View 的 CLICKABLELONG_CLICKABLE 属性
    • setOnClickListener() 会自动将 View 的 CLICKABLE 设为 true,setOnLongClickListener() 会自动将 View 的 LONG_CLICKABLE 设为 true
  9. View 的 enable 属性不影响 onTouchEvent() 的默认返回值。哪怕一个 View 是 disable 状态的,只要它的 clickable 和 longClickable 有一个为 true,那么它的 onTouchEvent() 就返回 true

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

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

6. Window 传递事件给顶级 View 即 ViewGroup 的原理

  • 根据源码,Window 是个抽象类,Window 的 superDispatchTouchEvent() 方法也是个抽象方法
  • Window 类可以控制顶级 View 的外观和行为策略,它的唯一实现位于 android.policy.PhoneWindow,当要实例化 Window 类的时候,你并不知道它的细节,因为这个类会被重构,只有一个工厂方法可以使用
  • PhoneWindow 将事件直接传递给 DecorView,通过 ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0) 这种方式就可以获取 Activity 所设置的 View
-------------------- 本文结束感谢您的阅读 --------------------