美文网首页
Touch 事件分发机制

Touch 事件分发机制

作者: xiazdong | 来源:发表于2015-09-22 07:49 被阅读807次

    原文:http://xiazdong.me/2015/09/19/touch-dispatch-mechanism/

    前言

    Touch 事件分发机制是面试中非常常见的问题,也是非常重要的问题。网上有很多关于这方面的文章,但是感觉写的不是特别清晰易懂。

    基本概念

    Touch 事件分发机制分发的是 MotionEvent 对象,取值如下:

    • ACTION_DOWN: 按下事件。
    • ACTION_MOVE: 移动事件。
    • ACTION_UP: 抬起事件。

    一个事件序列包含 ACTION_DOWN->ACTION_MOVE->...->ACTION_MOVE->ACTION_UP,即用户触摸屏幕,移动一些距离,然后抬起。

    1. 下面说的 view "处理" 了某个事件,表示 view 调用了 onTouchEvent()。
    2. 下面说的 view "消费" 了某个事件,表示 view 调用了 onTouchEvent() 并返回 true。因此某个 view 可以处理但不消费某个事件。

    Touch 事件分发机制涉及三个方法:

    • dispatchTouchEvent(MotionEvent ev): 如果某个触摸事件传递给了某个 View 或 ViewGroup(设为 v),则一定会调用 v.dispatchTouchEvent(),如果 v 是 ViewGroup,则内部会调用 onInterceptTouchEvent() 或 onTouchEvent();如果 v 是 View,则内部会调用 onTouchEvent()。
    • onInterceptTouchEvent(MotionEvent ev): 这个方法只有 ViewGroup 才有,判断是否要拦截该事件并且自己处理,如果返回 true,则拦截;如果返回 false,则不拦截。
    • onTouchEvent(MotionEvent ev): 处理 Touch 事件的核心方法,如果返回 true,表示消费了事件;如果返回 false,则表示没消费该事件。

    ViewGroup 的 dispatchTouchEvent(), onInterceptTouchEvent(), onTouchEvent() 的基本关系如下:

    public boolean dispatchTouchEvent(MotionEvent ev){
        boolean consume = false;
        if(onInterceptTouchEvent(ev)){  //是否拦截
            consume = onTouchEvent(ev); //如果拦截,则自己处理
        }
        else{  //如果没拦截,则事件分发给孩子
            consume = child.dispatchTouchEvent(ev);
        }
        return consume;
    }
    

    上面的代码只是基本概括了整个事件分发的核心流程,具体实现细节会在下面介绍。

    总体分发流程

    当用户发起触摸事件后,首先触摸事件从 Activity 的 dispatchTouchEvent() 开始,该方法实现如下:

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

    解释:

    • 其中 getWindow().superDispatchTouchEvent() 会调用 DecorView 的 dispatchTouchEvent() 开始分发,接着 DecorView 会调用根 View 进行事件分发。
    • 如果 getWindow().superDispatchTouchEvent() 返回 true,表示有子 View 消费了该触摸事件;如果返回 false,表示没有任何子 View 消费该事件,会调用 Activity 的 onTouchEvent(),即 Activity 自己处理 Touch 事件,并返回 false。

    View Touch 事件分发

    因为 ViewGroup 也是继承自 View,因此此处分两种情况讨论。

    • 如果是最底层的 View,一旦将触摸事件分发给他,就会调用下面的 dispatchTouchEvent();
    • 如果是 ViewGroup,则默认并不会调用下面的 dispatchTouchEvent(),而是会调用 ViewGroup 自己的 dispatchTouchEvent(),只有当 ViewGroup 在自己的 dispatchTouchEvent() 方法中经过判断,发现需要自己处理触摸事件时,才会通过 super.dispatchTouchEvent(ev) 的形式调用下面的 dispatchTouchEvent()。

    View 的 dispatchTouchEvent() 实现如下:

    public boolean dispatchTouchEvent(MotionEvent event) 
    {
        if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) 
        {
            return true;
        }
        if (onTouchEvent(event)) 
        {
            return true;
        }
        return false;
    }
    

    从上面看出:

    • onTouchEvent() 不一定会被调用。如果设置了 OnTouchListener, View 是 enabled,并且 OnTouchListener 的 onTouch() 返回 true,则不会调用 onTouchEvent()。
    • 如果 View 是 DISABLED,则 onTouch() 不会被调用。
    • 如果 OnTouchListener 的 onTouch() 或 View 的 onTouchEvent() 返回 true,则 dispatchTouchEvent() 返回 true;否则返回 false。

    接着我们看看 onTouchEvent() 的实现:

    public boolean onTouchEvent(MotionEvent event) {
        if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    performClick();   //执行 mClickListener.onClick() 方法
                    break;
                case MotionEvent.ACTION_DOWN:
                    ...
                    break;
                case MotionEvent.ACTION_MOVE:
                    ...
                    break;
            }
            return true;
        }
        return false;
    }
    

    从上面可以看出:

    • onTouchEvent() 内部在 ACTION_UP 事件中调用了 onClick()(即在一个事件序列中,只有在 ACTION_UP 才会调用 onClick(),但是 onTouch() 在每个事件都会被调用),即如果同时注册了 OnTouchListener 和 OnClickListener,则 OnTouchListener 优先级高于 OnClickListener,如果 onTouch() 返回 true,则 onTouchEvent() 不会执行,也就意味着 onClick() 不会执行。
    • 在 onTouchEvent() 中,只有 View 是 Clickable 的,才能进入 if 语句,而且一旦进入 if 语句就返回 true。比如 Button 是 Clickable 的,因此 onTouchEvent() 一定返回 true,ImageView 是不可点击的,因此 onTouchEvent() 一定返回 false。

    ViewGroup Touch 事件分发

    ViewGroup 的 dispatchTouchEvent() 比较复杂,下面我们分析一下。

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;
        // 1、如果是 ACTION_DOWN 动作,则清除标志位。
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();  // 清除 FLAG_DISALLOW_INTERCEPT 标记位
        }
    
        // 2、判断是否要拦截该事件
        final boolean intercepted;
        if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null){
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        }
        else{
            intercepted = true;
        }
    
        // 3、如果不拦截,则传给子 View
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {
            if (actionMasked == MotionEvent.ACTION_DOWN){
                  final View[] children = mChildren;
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final View child = children[childIndex];
                    //如果该子 View 不在触摸范围内,则略过
                    if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
                        continue;
                    }
                    //如果子 View 的 dispatchTouchEvent 返回 true,
                    //          则表示有子 View 处理了该事件,则设置 mFirstTouchTarget
                    //如果子 View 的 dispatchTouchEvent 返回 false,
                    //          则表示没有子 View 处理了该事件,则不设置 mFirstTouchTarget
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        //该方法中设置了 mFirstTouchTarget = child
                        newTouchTarget = addTouchTarget(child, idBitsToAssign); 
                        alreadyDispatchedToNewTouchTarget = true;
                        break;
                    }
                }
            }
        }
    
        // 4、判断是否有子 View 处理了事件
        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
        } 
        else {
            if (alreadyDispatchedToNewTouchTarget){
                 handled = true;
            }
            else{
                if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                    handled = true;
                }
            }
        }
    
        // 5、如果是一个事件序列的最后一个操作(ACTION_UP 或 ACTION_CANCEL),则把状态清空
        if (canceled 
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        }
    }
    

    上面的代码中多处调用了 dispatchTransformedTouchEvent(),其中第三个参数有 null(第 52 行) 或者 child(第 40 行、第 59 行)。这个方法的实现如下:

    public boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits){
        boolean handled = false;
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        return handled;
    }
    

    从上面代码可以看出,如果第三个参数传入 null,则调用 View 的 dispatchTouchEvent() 即自己处理事件;如果第三个参数传入 child,则将事件分发下去,即调用 child.dispatchTouchEvent()。

    解释:

    • 第 2 行: handled 变量作为 dispatchTouchEvent() 的返回值。
    • 第 4-7 行: 如果是 ACTION_DOWN 操作,则将其状态清空,包括 FLAG_DISALLOW_INTERCEPT。我们可以通过 requestDisallowInterceptTouchEvent() 将 FLAG_DISALLOW_INTERCEPT 设置为 true,表示该 ViewGroup 禁止拦截操作(即直接将 intercepted 设为 false,不调用 onInterceptTouchEvent()),但是这个设置对于 ACTION_DOWN 无效,因为第 4-7 行会将该状态清除,即使设置了该状态,ACTION_DOWN 操作还是会调用 onInterceptTouchEvent()。
    • 第 10-22 行: 每个 ViewGroup 都会带有 mFirstTouchTarget 变量,这个变量只有在 ACTION_DOWN 事件时才能设置,这个能从第 28 行的 if 语句看出来,因为设置 mFirstTouchTarget 是在第 40-45 行(只有第 28 行的 if 语句为 true 才能执行第 40-45 行代码),可以看出如果有某个子 View 消费了该事件(这里不一定是直接子 View 消费了该事件,比如有 View 关系: v1->v2->v3,当前调用了 v1.dispatchTouchEvent(),如果 v3 消费了该事件,则表示 v1 的某个子 View 消费了该事件,并将 v1 的 mFirstTouchTarget 设置为 v2,将 v2 的 mFirstTouchTarget 设置为 v3),这样才能使第 40 行的 dispatchTransformedTouchEvent() 返回 true,并设置 mFirstTouchTarget。
    • 第 25-48 行: 如果当前是 ACTION_DOWN 事件(第 28 行)并且没有拦截(第 27 行,onInterceptTouchEvent() 返回 false),则会将 ACTION_DOWN 事件分发给合适的在触摸点的直接子 View,在第 40 行的 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) 中会调用 child 的 dispatchTouchEvent()。如果 child.dispatchTouchEvent() 返回 false,表示 child 或 child 的子 View 没有人消费 ACTION_DOWN 事件,这样并不会给当前 ViewGroup 设置 mFirstTouchTarget。如果 child.dispatchTouchEvent() 返回 true,即表示 child 或 child 的子 View 有人消费 ACTION_DOWN 事件,则为当前 ViewGroup 设置 mFirstTouchTarget = child,并设置 alreadyDispatchedToNewTouchTarget = true,这个变量在后面会用到,表示是不是在这个方法中刚刚设置了 mFirstTouchTarget,因为只有 ACTION_DOWN 操作才能设置 mFirstTouchTarget,如果 alreadyDispatchedToNewTouchTarget == true && mFirstTouchTarget != null,则表示当前是 ACTION_DOWN 事件并且在该方法中刚刚设置了 mFirstTouchTarget;如果alreadyDispatchedToNewTouchTarget == false && mFirstTouchTarget != null,则表示 mFirstTouchTarget 并不是该方法中刚刚设置的,即当前不是 ACTION_DOWN 事件。
    • 第 51-53 行: 有两种情况会进入第 51 行的 if 语句,(1)当前是 ACTION_DOWN 操作,并且没有子 View 处理该事件 (2)当前不是 ACTION_DOWN 操作,并且在前面的 ACTION_DOWN 操作时没有子 View 处理该事件。第 52 行的第三个参数为 null,因此会执行 super.dispatchTouchEvent(),即执行 View 的 dispatchTouchEvent(),表示自己处理该事件。
    • 第 55-57 行: 因为只有当当前为 ACTION_DOWN 操作并且有子 View 处理了该事件时,alreadyDispatchedToNewTouchTarget 才为 true,这时直接将 handled 设为 true,不需要做额外的操作。
    • 第 59-61 行: 能够进入第 58 行的 else 语句意味着当前事件不是 ACTION_DOWN 并且在前面的 ACTION_DOWN 事件存在子 View 处理了该事件(即 mFirstTouchTarget != null,即在前面的 ACTION_DOWN 事件执行过第 40-46 行代码)。此时就执行 dispatchTransformedTouchEvent(),内部会调用 mFirstTouchTarget.dispatchTouchEvent()。
    • 第 66-69 行: 做收尾工作。

    一些结论的验证

    在网上有很多关于 Touch 事件的结论,这些结论其实都可以通过分析上面的代码得出。这里举几个例子:

    • "某个 view 一旦开始处理事件,如果不消费 ACTION_DOWN 事件,则同一事件序列中的其他事件不会再交给它来处理": 如果 view 处理但不消费 ACTION_DOWN 事件,则表示执行了第 40 行代码,但是返回 false(这个 view 作为 child 传入),设这个 view 的父 view 为 v0,此时就没设置 v0 的 mFirstTouchTarget。因此接下来的事件(比如 ACTION_MOVE) 一旦分发到 v0,因为他的 mFirstTouchTarget == null,因此会执行第 52 行代码,即 v0 自己处理该事件。
    • "某个 view 一旦决定拦截,那么这一事件序列都只能由它来处理,并且它的 onInterceptTouchEvent 不会被调用": 因为 view 拦截了事件,因此第 27 行的 if 语句进不去,也就设置不了 view 的 mFirstTouchTarget,接下来的事件分发给 view 时,执行到第 11 行的 if 语句,因为该事件不是 ACTION_DOWN 并且 view 的 mFirstTouchTarget == null,因此 if 语句返回 false,即执行第 21 行设置 intercepted = true,因此不会调用 onInterceptTouchEvent;接着执行到第 51 行,因为 view 的 mFirstTouchTarget 为 null,因此执行第 52 行代码,即自己处理该事件。

    参考文献

    相关文章

      网友评论

          本文标题:Touch 事件分发机制

          本文链接:https://www.haomeiwen.com/subject/uejicttx.html