目录

6. Android 校招复习篇二・事件分发

事件分发

工作原理

事实上,View 事件分发的本质是递归

递流程的方向遵循以下流程:

https://cdn.jsdelivr.net/gh/RebornQ/cdn.blog/img/reviews/View%20%E4%BA%8B%E4%BB%B6%E5%88%86%E5%8F%91%E6%B5%81%E7%A8%8B-%E9%80%92%E6%B5%81%E7%A8%8B.svg
View 事件分发流程-递流程

归流程的方向遵循以下流程:

https://cdn.jsdelivr.net/gh/RebornQ/cdn.blog/img/reviews/View%20%E4%BA%8B%E4%BB%B6%E5%88%86%E5%8F%91%E6%B5%81%E7%A8%8B-%E5%BD%92%E6%B5%81%E7%A8%8B.svg
View 事件分发流程-归流程
  • 每次完整的事件分发流程,都包含自上而下的“递”,与自下而上的“归”2个流程。

  • 每次完整的事件分发流程,都是针对一个事件(MotionEvent)完成的递归,而一个事件只对应着一个Action,例如:ACTION_DOWN

  • 一次用户触摸操作,我们称之为一个事件序列。一个事件序列会包含ACTION_DOWNACTION_MOVEACTION_MOVEACTION_UP等多个事件。

    也即一个事件序列,包含从ACTION_DOWNACTION_UP多次事件分发流程。

3个重要方法

事先分发包含 3 个重要方法: dispatchTouchEventonInterceptTouchEventonTouchEvent

大致流程

递流程

dispatchTouchEvent(ev)

在“递”的过程中,主要通过dispatchTouchEvent方法进行事件的向下分发。

因此“递”流程可以改进为:

https://cdn.jsdelivr.net/gh/RebornQ/cdn.blog/img/reviews/View%20%E4%BA%8B%E4%BB%B6%E5%88%86%E5%8F%91%E6%B5%81%E7%A8%8B-%E9%80%92%E6%B5%81%E7%A8%8BAdvanced.svg
View 事件分发流程-递流程Advanced

该方法主要执行以下操作(分两种情况):

  1. 如果 child 是 ViewGroup,那么实际执行的就是 ViewGroup 重写的 dispatchTouchEvent 方法。该方法内可以判断,是否在当前层级拦截当前事件、或者继续把事件下发给下一级。

  2. 如果 child 是不再有 child 的 View 或 ViewGroup,那么实际执行的就是 View 类实现的 super.dispatchTouchEvent 方法。该方法内可以判断,如果 View.enabled == true实现了 onTouchListeneronTouch() 返回 true,那么不执行 onTouchEvent( ),并直接返回 true 表示事件已被消费,然后步入“归”流程;否则执行 onTouchEvent( )。

    流程如下:

    https://cdn.jsdelivr.net/gh/RebornQ/cdn.blog/img/reviews/View%20%E4%BA%8B%E4%BB%B6%E5%88%86%E5%8F%91%E6%B5%81%E7%A8%8B-View-dispatchTouchEvent.svg
    View 事件分发流程-View#dispatchTouchEvent

    相关源码(仅贴出关键代码):

    1
    2
    3
    4
    5
    6
    7
    
    public boolean dispatchTouchEvent(MotionEvent event) {
            if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                    mOnTouchListener.onTouch(this, event)) {
                return true;
            }
            return onTouchEvent(event);
    }
    

    从流程图或源码可以看出:

    1. 若手动复写在onTouch()中返回true(即将事件消费掉,将不会再执行onTouchEvent()
    2. 若一个控件不可点击(即View.enabled == false),那么给它注册的onTouch()事件将永远得不到执行。因为这是逻辑与的判断,而判断 View 是否可点击在执行onTouch()事件之前。
onTouchEvent(ev)

onTouchEvent 方法的流程如下:

https://cdn.jsdelivr.net/gh/RebornQ/cdn.blog/img/reviews/View%20%E4%BA%8B%E4%BB%B6%E5%88%86%E5%8F%91%E6%B5%81%E7%A8%8B-View-onTouchEvent.svg
View 事件分发流程-View#onTouchEvent

分析:在onTouchEvent()中,如果View.clickabled == true并且实现了onClickListeneronLongClickListener,就会执行onClick()onLongClick()

onInterceptTouchEvent(ev)

事实上,在“递”的流程中,ViewGroup 可以在当前层级通过设置onInterceptTouchEvent()方法返回true,来拦截事件的下发,然后直接步入“归”流程。

注意

  1. 该拦截方法只存在于 ViewGroup,普通的 View 无该方法

  2. 如果某一层级的 ViewGroup 拦截了某个事件,那么后续的这一事件序列都会默认拦截,不再调用此方法

    onInterceptTouchEvent()方法只走一次,一旦走过,就会留下记号(mFirstTouchTarget == null),那么下一次直接根据这个记号来判断拦不拦截。

相关源码(仅贴出关键代码):

1
2
3
4
5
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    if (disallowIntercept || !onInterceptTouchEvent(ev)) { ... }
    ...
}

那么有同学就好奇了,disallowIntercept又是什么东西啊?

正所谓“上有政策,下有对策”。在ViewGroup可以拦截事件下发的同时,child也可以通过getParent.requestDisallowInterceptTouchEvent()方法来改变disallowIntercept的值,从而阻止上一级的下发拦截(即关闭拦截功能,使得原本被拦截的事件继续下发)。

说明:disallowIntercept = 是否禁用事件拦截功能(默认值 false)

归流程

总之,递流程走到没有 child 的层级,就意味着步入“归”流程。

如果该层级的super.dispatchTouchEvent(ev)没有返回true,那么将继续执行上一级的super.dispatchTouchEvent(ev),直到被某一级消费为止(也即返回true为止)。

这就是“归”流程。

总结

综上,整个大致流程如下图:

https://cdn.jsdelivr.net/gh/RebornQ/cdn.blog/img/reviews/View%20%E4%BA%8B%E4%BB%B6%E5%88%86%E5%8F%91%E6%B5%81%E7%A8%8B-%E9%80%92%E5%BD%92%E5%85%A8%E6%B5%81%E7%A8%8B.svg
View 事件分发流程-递归全流程

应用场景

滑动冲突解决

冲突场景

要点

外部拦截法

重写onInterceptTouchEvent(),根据冲突场景的规则来判断是否拦截。

内部拦截法

重写子 View 的dispatchTouchEvent() ,然后调用parent.requestDisallowInterceptTouchEvent(true)禁止父容器拦截事件,全部交给子 View 处理。

额外知识

onTouch()和onTouchEvent()的区别?

  1. 2个方法都是在View.dispatchTouchEvent()中调用,但onTouch() 优先于 onTouchEvent() 执行
  2. 若手动复写在onTouch()中返回true(即将事件消费掉,将不会再执行onTouchEvent()