View 事件分发源码解析

1. 源码分析目标

在这里插入图片描述

上一篇文章中对 View 事件分发的规律进行了总结,总结了 View 事件流的分发规律以及不同拦截情况下的走向。其中有些总结我们可能只知道结论,但是并不知道为什么是那样的结论,比如,在父 View 中的 onInterceptTouchEvent 中拦截 move 时,首次 move 事件过来时,子 View 的 dispatchTouchEvent 中收到了一个动作 ACTION_CANCEL,而父 View 首次并没有收到第一次 move 事件,而 Activity 中收到了第一次的 move 事件?不通过源码我们很难理解为什么是这样。

下面整理一下问题点,阅读源码时我们主要来解决几个疑问:

  • 事件分发的 U 型结构是怎么产生的?
  • 子 View 什么情况下会收到 Cancel 事件?
  • 父 View 拦截 Move 事件后,为什么把第一个 Move 事件交给 Activity 了?
  • 为什么 Down 事件至关重要?
  • 什么是内部拦截法,什么是外部拦截法?
  • 为什么会有 onInterceptTouchEvent 和 requestDisallowInterceptTouchEvent?
  • 为什么会有 TouchTarget 链,什么情况下会有多个 TouchTarget?

2. Activity 之前的事件分发

在这里插入图片描述

在底层是通过管道机制来完成事件的监听和下发,首先当产生事件时,driver 向特定描述符写入事件后,会触发唤醒 epoll 工作,此时 eventHub 通过 read 方法从描述符中读取原始事件,然后通过简单封装成 rawEvent 并传递给 InputReader。InputReader 中循环线程会获取 eventHub 中的事件,然后将事件传递到 InputDispater 并最终传递到上层。上层中有 InputEventReceiver 来接收事件,它的实现类 WindowInputEventReceiver 负责接收,WindowInputEventReceiver 是在 ViewRootImpl 中的内部类,ViewRootImpl 我们就不陌生了,虽然不是 View,但是 View 中很多操作源头开始于此,如绘制,事件分发等。

在这里插入图片描述

在 ViewRootImpl 中主要将事件按照队列形式来处理,依次从队列中出去事件,每个事件又分为 InputStage 处理,可以理解为阶段处理。InputStage 主要是用来将事件的处理分成若干个阶段(stage)进行,事件依次经过每一个stage,如果该事件没有被处理(标识为FLAG_FINISHED),则该stage就会调用 onProcess 方法处理,然后调用 forward 执行下一个 stage 的处理;如果该事件被标识为处理则直接调用 forward,执行下一个 stage 的处理,直到没有下一个 stage(也就是最后一个SyntheticInputStage)。这里一共有 7 种 stage,各个 stage 间串联起来,形成一个链表。

其中对于我们关心的事件属于 MotionEvent,相应的 InputStage 实现类是 ViewPostImeInputStage,对于手指触碰的 TouchEvent,在 onProcess 方法中最终调用 processPointerEvent 处理,processPointerEvent 中又调用了 mView.dispatchPointerEvent(event),mView 是 DecorView,这样就越来越接近我们的视野范围。

final class ViewPostImeInputStage extends InputStage {
    
    ...
    private int processPointerEvent(QueuedInputEvent q) {
        final MotionEvent event = (MotionEvent)q.mEvent;
        mAttachInfo.mUnbufferedDispatchRequested = false;
        mAttachInfo.mHandlingPointerEvent = true;
        boolean handled = mView.dispatchPointerEvent(event);
        ...
        return handled ? FINISH_HANDLED : FORWARD;
    }
}

DecorView 中没有重写 dispatchPointerEvent 方法,而是走了父类 View 的 dispatchPointerEvent 方法,根据事件类型判断,如果是 TouchEvent,则回到
DecorView 中的 dispatchTouchEvent。

public final boolean dispatchPointerEvent(MotionEvent event) {
    if (event.isTouchEvent()) {
        return dispatchTouchEvent(event);
    } else {
        return dispatchGenericMotionEvent(event);
    }
}

DecorView 中是调用 Window.Callback 来处理的,在 Activity 启动过程中会创建 PhoneWindow,并将自己赋给 PhoneWindow,因为 Activity 实现了 Window.Callback 接口,所以就会转到 Activity 中的 dispatchTouchEvent 处理。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

接着调用 PhoneWindow 的 superDispatchTouchEvent,又回到 mDecor.superDispatchTouchEvent(event),辗转反侧还交给 mDecor 来处理,接着就来到我们熟悉的 ViewGroup 的 dispatchTouchEvent 中。

这里简单思考一下:

(1)ViewRootImpl 中收到事件后为什么不直接通过 DecorView 直接分发事件,而是绕『弯路』交给 Activity 处理,然后又转到 DecorView?

(2)Activity 中为什么不直接获取 DecorView,而是通过 Window 获取?

首选看下第一个问题,由于 Activity 中也会需要处理事件,如果在 DecorView 中处理,那么在 DecorView 中就需要回调 Activity 或者持有 Activity,这样显然不是很合理,耦合严重,此外,如果从 DecorView 开始处理,那么起点就是 Activity 了。Activity 是直接面向开发者的,起始点应该设在 Activity 中,Activity 是一个外壳,Activity 依赖 Window, Window 再对 View 管理,也就是 PhoneWindow 持有 DecorView。

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

再看下第二个问题:第一个问题中已经提到了,Activity 是一个外壳,Activity 直接依赖 Window,Window 是更高层的抽象,View 仅仅是 Window 管理的其中部分,可以比喻为,Activity 不会亲自干这种细活,细活应该交给 Window 管理。同时也达到解耦的目的。

2. Activity 之中的事件分发

一般来说,我们平时开发更多关注的是 View 的事件分发,很少在 Activity 中处理一些事件。而 Activity 中的事件分发也相对简单,没有过多的逻辑。在 Activity 中是事件分发的起点,内部 View 没有拦截的话,最后交给 Activity 的 onTouchEvent 处理。

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

接着是 View 的事件分发,事件从底层过来之后,按照从下到上的顺序分发,可以理解为在 Z 方向上下屏幕下到上完成事件的传递。一个完整的事件流由一个 Down 事件、若干个 Move 事件和一个 Up 事件组成。接下来就看下底层 View 是如果向上分发事件的。先看下 ViewGroup 的 dispatchTouchEvent 方法。

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        ...
        boolean handled = false;
        // 安全策略校验通过
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 1. down 事件时清除状态,清除事件传递链表
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
            
            // 2. 事件拦截检查
            final boolean intercepted;
            // 在初始 Down 事件时或者已经有子 View 拦截事件时,即 mFirstTouchTarget,下次有事件过来时,如 Move 和 Up 事件,作为 ViewGroup 需要看下是否拦截,即看 onInterceptTouchEvent 是否拦截
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // 子 View 在不拦截 Move 和 Up 事件时,所以当前 ViewGroup 拦截事件,意味着事件不再向下传递
                intercepted = true;
            }
            
            // 检查是否取消事件
            final boolean canceled = resetCancelNextUpFlag(this)
            || actionMasked == MotionEvent.ACTION_CANCEL;
            
            3.Down 事件时,ViewGroup 没有拦截的情况下,寻找拦截事件的子 View
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                ...
                
                // 在 Down 事件时寻找要拦截的子 View,ACTION_POINTER_DOWN 代表是多指触碰,后面会分析
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                 
                    ...

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        ...

                        // 从前到后遍历子View,找到需要拦截事件的子 view
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            ...
                            // 检查子View是否能够接收事件,并判断事件是否在 view 范围内
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            // 判断子 View 是否已经添加到拦截事件的链表上,即以 mFirstTouchTarget 为头的链表
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            // 向子 View 分发事件,看是否需要拦截
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                // 子 View 要拦截,即找到了拦截的子View,结束循环,因为每个事件只有一个View处理
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                }
                
                
            }
            
            // 4 没有子 view 拦截时,交给自己处理,即走自己的 onTouchEvent
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } 
            // 5 有子 view 拦截时,调用子 View 的 dispatchTouchEvent 方法,向上分发
            else {
                // 有子 view 拦截,遍历链表,将事件传递到子 View,这里有两种情况需要处理
                // (1) 如果 ViewGroup 拦截事件,那么向子 View 下发取消事件,并跳转链表的下一个节点
                // (2) 如果当前的 TouchTarget 是 newTouchTarget,代表已经分发事件了,即在上面已经对子 View 调用过 dispatchTransformedTouchEvent
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
            
        }
        return handled;
    }
            

ViewGroup 的 dispatchTouchEvent 方法相对较长,这里再分步骤详细说明一下(父View 用 ViewGroup 表示),事件下发时,从 ViewGroup 到子 View,也就是说Down、Move、Up 事件流是ViewGroup流向子View的,那么在这个过程中ViewGroup可以通过 onInterceptTouchEvent 拦截事件,拦截后就不再向子 View 传递,当然子 View 也可以设置,不让 ViewGroup 拦截,通过设置 requestDisallowInterceptTouchEvent 不让 ViewGroup 拦截。

(1) down 事件时清除状态,清除事件传递链表,也就是说 Down 事件时,重新记录事件的拦截情况,因为 Down 是事件流的开始

(2)actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null,在初始 Down 事件时或者已经有子 View 拦截事件时,即 mFirstTouchTarget 不为空,下次有事件过来时,如 Move 和 Up 事件,作为 ViewGroup 需要看下是否拦截,即看 onInterceptTouchEvent 是否拦截。

  • 对于 Down 事件时,如果 ViewGroup 拦截了,可以看 intercepted 使用的位置,会导致不会有子 View 收到事件,mFirstTouchTarget 为空,意味着事件只会在 dispatchTouchEvent 中处理,并返回上一层的 dispatchTouchEvent,ViewGroup 的 onTouchEvent 也不会被调用。
  • 如果不是 Down 事件,那么就会是 Move 或者 Up 事件,此时判断 mFirstTouchTarget 需要不为空才会有可能将事件向上分发,才会判断 onInterceptTouchEvent,为什么呢?上面分析了,如果 Down 事件时 ViewGroup 拦截了,mFirstTouchTarget 为空的,意味着 ViewGroup 在 Down 事件时不能拦截事件,子 View 需要拦截事件,这样后续子 View 才有可能收到 Move 和 Up 事件,这块也说明了 Down 事件对子 View 至关重要,决定是否可以收到后续事件的前提

(3)在 Down 事件时,ViewGroup 没有拦截的情况下,寻找拦截事件的子 View。这一步虽然代码不少,但是逻辑相对简单,就是遍历子 View,向子 View 分发事件,找到要拦截的那个子View,dispatchTransformedTouchEvent 方法会调用子 View 的 dispatchTouchEvent 方法,如果子 View 也是一个 ViewGroup,那么就会进行递归调用,若果是一个 View,会走 View 的 dispatchTouchEvent 方法,要么 dispatchTouchEvent 方法返回 true,要么内部 onTouchEvent 返回 true,或者 OnTouchListener 返回true,总之在 Down 时需要拦截到事件。

(4)在步骤 3 中如果没有找到拦截事件的子 View,mFirstTouchTarget 为空,此时事件就交给 ViewGroup 自己来处理,即通过调用 dispatchTransformedTouchEvent 方法,里面会调用 View 的 dispatchTouchEvent 方法。

(5)如果找到需要拦截事件的子 View,那么 mFirstTouchTarget 就不为空,此时如果 ViewGroup 不拦截事件,会将事件继续向子 View 分发。因为事件流总是由父 View 流向子 View,所以需要看父 View 是否拦截。分发时需要判断 Dwon 事件情况,因为这块的代码没有使用事件类型判断,如判断事件是否是 Move 或者 Up,而是使用mFirstTouchTarget不为空判断的,需要排除 Down 事件的情况,即 alreadyDispatchedToNewTouchTarget && target == newTouchTarget 成立时,因为前面已经调用过子 view 的 dispatchTouchEvent 方法,避免重复调用。

在 ViewGroup 的 dispatchTouchEvent 中多处调用了 dispatchTransformedTouchEvent 方法,那就看下 dispatchTransformedTouchEvent 中的主要处理逻辑。

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // 1 分发取消事件
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        // 对事件处理,判断是否是多指触摸的事件还是同一根手指事件
        // Calculate the number of pointers to deliver.
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        // If for some reason we ended up in an inconsistent state where it looks like we
        // might produce a motion event with no pointers in it, then drop the event.
        if (newPointerIdBits == 0) {
            return false;
        }

        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }

        // 2 事件自己处理,调用 View dispatchTouchEvent 方法,自己作为 view 处理事件
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        }
        // 3 向子 view 分发事件,调用子 View 的 dispatchTouchEvent 方法
        else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

主要有3个重要的处理:

(1)取消事件处理,可以是自身的取消事件,也可能是子 View 的取消事件处理,那么都是什么情况才是取消事件呢?在 dispatchTouchEvent 中找到调用 dispatchTransformedTouchEvent 方法中 cancel 参数是 true 的情况,

  • 对于子 View 情况,即 child 参数不为空,同时 cancel 参数为 true,也就是在上面 dispatchTouchEvent 分析的第 5 步中,mFirstTouchTarget 不为空,也就是在 down 事件时有子 View 拦截,在 Move 或者 Up 时 ViewGroup 拦截了事件,此时 Move 和 Up 不再分发给子 View,会给子 View 传递一个 Cancel 事件,一方面可以让子 View 相关状态复位,另一方面告知子 View 的事件流结束
  • 对于 ViewGroup 自身情况,dispatchTransformedTouchEvent 方法传递参数 child 为 null,cancel 为 true,也就是 ViewGroup 自己的子 View 没有拦截事件,自己作为下层父 View 的子 View,拦截了 Down 事件,在 Move 或者 Up 事件被自己的父 View 拦截时,cancel 为 true

(2)没有子 View 拦截事件时,即 mFirstTouchTarget 为 null,事件自己处理,走 View 中的 dispatchTouchEvent 方法

(3)有子 View 拦截事件时,事件向上层子 View 分发

从这块代码分析,可以得出,子 View 收到 Cancel 事件,首先是子 View 能够拦截到 Down 事件,在后续的事件被父 View 拦截时会收到一个 Cancel事件。

接着再看下 View 中到底是如何处理事件的

    public boolean dispatchTouchEvent(MotionEvent event) {
        ...

        final int actionMasked = event.getActionMasked();
        // down 事件时停止滚动
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            // 先调用 mOnTouchListener 处理,优先级比 onTouchEvent 高
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            // onTouchEvent 高
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        ...
        
        // UP 、CANCEL 结束事件处理,停止滑动
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

View 的 dispatchTouchEvent 事件相对简单,主要是对事件的响应处理,即调用 mOnTouchListener 或者 onTouchEvent 来对事件直接响应的可以看到 mOnTouchListener 优先级比 onTouchEvent 高,如果 mOnTouchListener 中返回 true,那么 onTouchEvent 中就不再收到事件处理,而 onTouchEvent 还有 mOnClickListener 和 mOnLongClickListener 处理,可以认为优先级比 onTouchEvent 低。

到这里事件流的一个整理流向分析基本已经完成,那么我们看下对于 Down 事件,是如何完成经典的 U 型走向的?

  • 其实所谓的 U 型,实际上就是分发函数递归向下层层调用,即 父 View1 dispatchTouchEvent --> 父 View2 dispatchTouchEvent —> 子 View dispatchTouchEvent,能一直向下调用的前提是,父 View 中的 onInterceptTouchEvent 没有拦截,均是走 dispatchTouchEvent 默认的 super 方法,简单来说就是走源码的 dispatchTouchEvent 实现,不是我们自己重写方法的实现
  • 那 U 型结构的另外一边呢,即回溯过程?为了保证完整的 U 型结构,对于子 View 仍旧走 dispatchTouchEvent 的默认实现,最终调用默认的 onTouchEvent,这样对于上层子 View dispatchTouchEvent 返回值是 false 的,对于父 View 中 mFirstTouchTarget 是 null,这样父 View 调用 dispatchTransformedTouchEvent 传递的 child 也为 null,所以也会走view dispatchTouchEvent 方法,然后走 onTouchEvent 方法,然后重复这个过程返回值层层向上回溯,最终会回到 Activity 的 dispatchTouchEvent 方法中,最后走 Activiy 的 onTouchEvent 方法。

对于一个 Down 事件,如果我们不做任何操作,Down 事件就会完成这样一个 U 型结构的流向。

在文章开头的一个问题:ViewGroup 拦截 Move 事件后,为什么把第一个 Move 事件交给 Activity 了?这个问题是有背景的,是在上一篇文章中的例子,ViewGroup 是 Activity 中布局的根布局,ViewGroup 的子 View 拦截 Down 事件,在 Move 事件过来时,被 ViewGroup 拦截,此时第一个 Move 事件传到了 Activity 中

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    // 拦截 Move 事件,intercepted = true
    ... 
    
    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                 // 拦截 Move 事件后向子 View 发动一个 Cancel 事件
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
                ...
            }
            predecessor = target;
            target = next;
        }
         return handled;
    }
}

我们跟踪一下事件流,ViewGroup 拦截事件后,向子 View 传递一个 Cancel 事件,即调用 dispatchTransformedTouchEvent 方法,然后调用子 View 的 dispatchTouchEvent 方法,子 View 中 cancel 事件一般返回 false,那么 handled 值也为 false,所以回到 Activity 中的 dispatchTouchEvent 方法时,会走 onTouchEvent(ev) 方法所以第一个的 Move 事件就流到了 Activity 中。对于后面的一系列 Move 事件,只会在 ViewGroup 中被消费,从上面的代码中看出,首次拦截后 mFirstTouchTarget 会被清除掉,所以后面会走 mFirstTouchTarget == null 分支,即 ViewGroup 自己消费 Move 事件,handled 为 true,返回 Activity 中时,也会直接返回 true,不再走 onTouchEvent(ev)。

下面看下 TouchTarget,为什么会有 TouchTarget 链,什么情况下会有多个 TouchTarget?

TouchTarget 是用来表示 View 对多个捕获事件的描述,简单来说,主要是能够处理多指触摸的情况,多只触摸情况下,多个手指可能触摸一个 View,也可能是多个 View,多指触摸一个 View 时,通过 TouchTarget 记录该 View 的多个 pointerIdBits,多指触摸多个 View 时,通过多个 TouchTarget 组成的链表来记录。链表的创建主要是在 Down 事件时通过 addTouchTarget(child, idBitsToAssign) 添加到链表上,链表头是 mFirstTouchTarget。对于 Down 事件,第一个 Down 事件是 ACTION_DOWN,后面其他手指的 Down 事件是 ACTION_POINTER_DOWN,不同 Down 事件都有 index 和 id,对于 ACTION_DOWN 的index始终是 0。所以多指触摸情况下会有 ACTION_DOWN 和 多个 ACTION_POINTER_DOWN 事件,多个 ACTION_POINTER_DOWN 事件如果是作用在 ViewGroup 中不同子 View 上就会有多个 TouchTarget 形成一个链表结构,而在 Move 事件时是没有 ACTION_POINTER_MOVE 的,只有 ACTION_MOVE。

  if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        final int actionIndex = ev.getActionIndex(); // always 0 for down
        final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                : TouchTarget.ALL_POINTER_IDS;
  }

什么是内部拦截法,什么是外部拦截法?内部和外部是以子 View 来说的,如果在子 View 内部处理滑动冲突,就是内部拦截法,如果在 ViewGroup 中处理事件拦截,就是外部拦截法,这里举一个例子,父 View 需要拦截左右方向的 Move 事件,子 View 需要拦截垂直方向的 Move 事件。

外部拦截法:

View
private float startX;
private float startY;
    
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_MOVE:
            float delX = Math.abs(ev.getX() - startX);
            float delY = Math.abs(ev.getY() - startY);
            if (delY < delX) {
                return true;
            }
            break;
        default:
            break;
    }
    return super.onInterceptTouchEvent(ev);
}View

@Override
public boolean onTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
         return true;
        case MotionEvent.ACTION_MOVE:
        // do some thing
           return true;
        default:
            break;
    }
    return super.onTouchEvent(ev);
}

内部拦截法

View
    
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_MOVE:
            return true;
        default:
            break;
    }
    return super.onInterceptTouchEvent(ev);
}View

private float startX;
private float startY;

@Override
public boolean onTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
         getParent(). requestDisallowInterceptTouchEvent(true);
         return true;
        case MotionEvent.ACTION_MOVE:
            float delX = Math.abs(ev.getX() - startX);
            float delY = Math.abs(ev.getY() - startY);
            if (delY < delX) {
                getParent(). requestDisallowInterceptTouchEvent(false);
                return true;
            }
            break;
        default:
            break;
    }
    return super.onTouchEvent(ev);
}

以上就是内部拦截和外部拦截法的简单实例,外部拦截法,就是外部ViewGroup通过 onInterceptTouchEvent 来决定是否拦截事件,拦截后,事件不再向子 View 分发。内部拦截法是子 View 通过 getParent(). requestDisallowInterceptTouchEvent(false、true);决定是否让 ViewGroup 拦截,简单来说就是看将判断逻辑放在哪。

那么从设计角度看为什么有 onInterceptTouchEvent 和 requestDisallowInterceptTouchEvent?首先看 onInterceptTouchEvent 方法,有这样一个场景,ViewGroup 是能够上下滑动的,内部子 View 是可以点击的,假设没有 onInterceptTouchEvent 方法,那么手指在 子 View 上垂直方向移动时,应该能够上下滑动,才符合操作习惯,但是事件默认都是先给到子 View,Move 达到子 View 时再传给 父 View 也不是不行,但是较麻烦,最直接的方式还是在 ViewGroup 向子 view 分发时直接将 Move 拦截,这样逻辑更加清晰。

requestDisallowInterceptTouchEvent 虽然看上去可有可无,但是有些场景还是需要有它来处理,比如 ViewPager 中多个 Page,每个 Page 中有 ScrollView 可以上下滑动,开始阶段一直上下滑动,手指不抬起来,变成水平滑动,此时应该不触发 ViewPager 切换更为合理,但是没有 requestDisallowInterceptTouchEvent 处理一下,ViewPager 会在水平滑动时拦截 Move 事件,导致 ScrollView 对事件处理中途被改变了。所以这就是源码中设置 onInterceptTouchEvent 和 requestDisallowInterceptTouchEvent 这两个钩子方法的意义。

3. View 中 onTouchEvent 分析

上面分析了 View 中 dispatchTouchEvent 方法,内部优先级是 mOnTouchListener > onTouchEvent, mOnTouchListener 没什么可说的,是由用户自定义重写的接口,接下来看下 onTouchEvent 方法。

    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        // 是否可点击或者长按
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        // 是否view设置了DISABLED状态,如果是不再走后面的逻辑,但是如果 clickable,仍旧可以消费事件,只是不响应点击事件
        if ((viewFlags & ENABLED_MASK) == DISABLED
                && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        // 如果设置了 View 的事件代理,则由代理完成 onTouchEvent,常用来扩大 View 的点击热区
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        // 如果可点击,根据事件来处理逻辑
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    // 如果不可点击则移除 tap 检测和长按检测
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    // 是否是按下状态
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // 获取焦点
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }
                        // 设置按下状态,一般是背景色半透明,让用户感知到是按下状态
                        if (prepressed) {
                            setPressed(true, x, y);
                        }

                        // 在 UP 事件状态,如果没有执行长按情况下,才有可能执行点击事件
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // 移除长按事件的延迟处理
                            removeLongPressCallback();

                            // 注意这里是 !focusTaken,不是没有焦点才执行点击事件,而是本身已经有了焦点才执行动作,也就是不需要重新获取焦点,如在有键盘情况下,点击 // View 是不响应事件的,而是先关闭键盘,这种就属于先要获取焦点。
                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }

                        // 延时取消按下状态
                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;
                    // 不可点击时,检查是否可以长按,因为有可能是 (mViewFlags & TOOLTIP) == TOOLTIP) 状态
                    if (!clickable) {
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // 使用 CheckForTap 做一个延迟检测,避免处于可滑动的父 View 中时,和滑动事件冲突
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // 触发长按检测,长按长按时间 400ms 响应长按事件
                        setPressed(true, x, y);
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    ...
                    
                    // 移出 View 时,取消 tap 检测和长按检测,并取下按下状态
                    if (!pointInView(x, y, touchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }

                    ...

                    break;
            }

            return true;
        }

        return false;
    }

onTouchEvent 方法代码虽然很多,但主要就是处理点击和长按事件,在不同事件下对这两种事件进行响应处理。对于单击事件相对明确,单击事件是在 Up 时才响应的,长按是有个时长,超过 400ms 时才响应,否则会取消响应,以及一些按压状态处理。

(1)在 Up 事件时,如果不可点击则移除 tap 检测和长按检测,其中 tap 检测是为了防止长按事件和属于滑动容器中时和滑动事件互斥,tap 检测延迟的时长是 100ms,超过 100ms 则说明是长按时间,再延迟 400-100 = 300ms 后执行长按事件。

(2)down 事件时主要是触发长按事件的延迟检测,其中也有上面提到的 tap 检测,也就是说长按是由时间来决定的,点击事件是由 UP 事件决定的。

(3)move 事件时检查触摸位置是否还在需要响应事件的 View 范围内,不在的话,就需要取消 tap 检测和长按检测

(4)取消事件不用多说,同样会取消 tap 检测和长按检测,以及按压状态等

4 总结

以上就是对 View 分发事件源码的一个解析,主要针对开篇的几个问题,在分析过程中也做出了回答。本质上 View 的事件在一个时刻只由一个 View 来处理,所以就会有拦截过程,事件由底层向上分发,中间设置了一些钩子函数,onInterceptTouchEvent 和 onTouchEvent,给开发者定制的机会。当然还有更加复杂的场景,比如嵌套滑动的场景,使用 NestedScrollingChild 和 NestedScrollingParent以及后面更新的 2 和 3 代接口,主要来更好的解决嵌套滑动场景,简单来说就是使得 Move 事件能够在一个事件流过程中,能够被不同的 View 消费,消费多少滑动距离,都能够做到精细控制,后面再针对嵌套滑动情形进行分析。

参考

Android Input输入事件处理流程分享

ViewGroup事件分发总结-TouchTarget

Android 多点触控详解

【透镜系列】看穿 > 触摸事件分发 >

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值