6【Android 12】输入事件在App层的分发流程(二) —— MotionEvent处理流程

发布于:2022-12-25 ⋅ 阅读:(938) ⋅ 点赞:(0)

在这里插入图片描述

6 MotionEvent处理流程

6.1 ViewRootImpl.ViewPostImeInputStage.processPointerEvent

        private int processPointerEvent(QueuedInputEvent q) {
            final MotionEvent event = (MotionEvent)q.mEvent;

			......
            boolean handled = mView.dispatchPointerEvent(event);

			......
            return handled ? FINISH_HANDLED : FORWARD;
        }

这里先将QueuedInputEvent中的mEvent向下转为MotionEvent类型,但从MotionEvent的注释来看,MotionEvent是用来报告移动(鼠标,笔,手指,轨迹球)事件的对象,覆盖的输入源似乎比pointer类型多。

这里的mView是View层级结构的根VIew,对于Activity来说就是DecorView,对于非Activity窗口来说就是该窗口的自定义View,这里只分析最常见的Activity窗口。由于View.dispatchPointerEvent声明为final,所以这里直接调用的是View.dispatchPointerEvent方法。

6.2 View.dispatchPointerEvent

    /**
     * Dispatch a pointer event.
     * <p>
     * Dispatches touch related pointer events to {@link #onTouchEvent(MotionEvent)} and all
     * other events to {@link #onGenericMotionEvent(MotionEvent)}.  This separation of concerns
     * reinforces the invariant that {@link #onTouchEvent(MotionEvent)} is really about touches
     * and should not be expected to handle other pointing device features.
     * </p>
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public final boolean dispatchPointerEvent(MotionEvent event) {
        if (event.isTouchEvent()) {
            return dispatchTouchEvent(event);
        } else {
            return dispatchGenericMotionEvent(event);
        }
    }

这个方法用来分发点触事件。

分发touch相关的点触事件到View.onTouchEvent,其他所有的事件发送给View.onGenericMotionEvent。这种将touch事件和其他点触事件分开处理的做法,强调了MotionEvent就是和touch相关的,不应该再去处理其他点触事件。

我们的关注点在touch相关的事件分发流程,那么这里调用的是DecorView.dispatchTouchEvent方法。

6.3 DecorView.dispatchTouchEvent

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }

这里的mWIndow即PhoneWindow,PhoneWIndow的callback是Activity.attach的时候通过PhoneWindow.setCallback设置的,传入的是Activity自己。

我们分析一般流程,即Activity对应的DecorView的事件分发流程。

6.4 Activity.dispatchTouchEvent

    /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

这个方法用来处理触屏事件。你可以复写这个方法从而在所有触屏事件发送给窗口之前将其拦截。

1)、当down流程的时候触发onUserInteraction回调。如果你想知道当你的Activity正在运行时,用户已经以某种方式与设备进行了交互,那么就执行这个方法。onUserInteraction和onUserLeaveHint用来帮助Activity智能管理状态栏通知,特别是,帮助Activity确定一个合适的时机来取消掉通知。

2)、调用PhoneWindow.superDispatchTouchEvent方法,将输入事件发送给View层级结构,分析重点。

3)、如果前面两步没有处理MotionEvent,那么调用Activity.onTouchEvent来处理。这个方法最有用的地方是用来处理那些落在窗口之外,没有任何View可以接收的touch事件。比如当触摸到Dialog以外的区域的时候,判断是否要移除掉Dialog。

6.5 PhoneWindow.superDispatchTouchEvent

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

这里的mDecor是DeocrView类型的成员变量。

6.6 DecorView.superDispatchTouchEvent

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

一直念叨着View层级结构,直到这里才算真正走到View层级结构的根View了。

由于DecorView的直接父类FrameLayout没有重写dispatchTouchEvent方法,因此这里调用的是ViewGroup.dispatchTouchEvent。

6.7 ViewGroup.dispatchTouchEvent

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
		......

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }
      
		......
      
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // Check for interception.
            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); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

            // If intercepted, start normal event dispatch. Also if there is already
            // a view that is handling the gesture, do normal event dispatch.
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
                    && !isMouseEvent;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                // If the event is targeting accessibility focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                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;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x =
                                isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                        final float y =
                                isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        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);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (ViewDebugManager.DEBUG_MOTION) {
                                Log.d(TAG, "(ViewGroup)dispatchTouchEvent to child 3: child = "
                                        + child + ",childrenCount = " + childrenCount + ",i = " + i
                                        + ",newTouchTarget = " + newTouchTarget
                                        + ",idBitsToAssign = " + idBitsToAssign);
                            }
                            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);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                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();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                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;
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

		......
        return handled;
    }

这个方法太长了,分几部分去分析。

6.7.1 accessibility focus

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

这里的accessibility应该是无障碍辅助功能相关的内容。ViewRootImpl可以通过setAccessibilityFocus方法来设置一个View作为accessibility focusd View,那么这个View就拥有了accessibility focus,拥有accessibility focus的View在事件分发的时候会被特殊处理。

这里通过MotionEvent.isTargetAccessibilityFocus判断以个MotionEvent是否是一个以accessibility focusd View为目标的事件,根据当前事件的相关flag是否包含FLAG_TARGET_ACCESSIBILITY_FOCUS:

    /**
     * Private flag indicating that this event was synthesized by the system and should be delivered
     * to the accessibility focused view first. When being dispatched such an event is not handled
     * by predecessors of the accessibility focused view and after the event reaches that view the
     * flag is cleared and normal event dispatch is performed. This ensures that the platform can
     * click on any view that has accessibility focus which is semantically equivalent to asking the
     * view to perform a click accessibility action but more generic as views not implementing click
     * action correctly can still be activated.
     *
     * @hide
     * @see #isTargetAccessibilityFocus()
     * @see #setTargetAccessibilityFocus(boolean)
     */
    public static final int FLAG_TARGET_ACCESSIBILITY_FOCUS = 0x40000000;

FLAG_TARGET_ACCESSIBILITY_FOCUS表示该事件是由系统合成的而且应该被首先发送给accessibility focused View。在发送时,此类事件不会被accessibility focused view之前的View的处理,而且当事件到达accessibility focused View之后,相关标志会被清除,普通的事件分发会被执行。这个确保了平台可以点击任何有accessibility focuse的View,这在语义上等同于要求View去执行一个点击辅助动作,但更通用的是,没有正确实现点击动作的View仍然可以被激活。

那这里的逻辑就符合了注释中的情况,此时当目标为accessibility focused Vie的事件到达accessibility focused View之后,FLAG_TARGET_ACCESSIBILITY_FOCUS标志被清除。

6.7.2 调用onFilterTouchEventForSecurity检查安全性

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
        	......
        }

		......
        return handled;

调用View.onFilterTouchEventForSecurity方法进行安全方面的检查,如果onFilterTouchEventForSecurity返回false,那么就直接返回false,表示当前ViewGroup不处理此次MotionEvent。

看下View.onFilterTouchEventForSecurity方法具体内容:

    /**
     * Filter the touch event to apply security policies.
     *
     * @param event The motion event to be filtered.
     * @return True if the event should be dispatched, false if the event should be dropped.
     *
     * @see #getFilterTouchesWhenObscured
     */
    public boolean onFilterTouchEventForSecurity(MotionEvent event) {
        //noinspection RedundantIfStatement
        if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
            // Window is obscured, drop this touch.
            return false;
        }
        return true;
    }

这个方法用来基于安全因素过滤掉touch事件。

如果当前View的mViewFlags包含FILTER_TOUCHES_WHEN_OBSCURED,并且当前touch事件包含FLAG_WINDOW_IS_OBSCURED,那么就丢弃掉此次事件。

    /**
     * Indicates that the view should filter touches when its window is obscured.
     * Refer to the class comments for more information about this security feature.
     * {@hide}
     */
    static final int FILTER_TOUCHES_WHEN_OBSCURED = 0x00000400;

FILTER_TOUCHES_WHEN_OBSCURED表示如果它的窗口被遮挡,那么当前View需要过滤掉touch事件。

    /**
     * This flag indicates that the window that received this motion event is partly
     * or wholly obscured by another visible window above it. This flag is set to true
     * if the event directly passed through the obscured area.
     *
     * A security sensitive application can check this flag to identify situations in which
     * a malicious application may have covered up part of its content for the purpose
     * of misleading the user or hijacking touches.  An appropriate response might be
     * to drop the suspect touches or to take additional precautions to confirm the user's
     * actual intent.
     */
    public static final int FLAG_WINDOW_IS_OBSCURED = 0x1;

FLAG_WINDOW_IS_OBSCURED表示当前正在接收MotionEvent的窗口被在它之上的另一个可见窗口部分或者完全遮挡。这个标记看代码应该是InputDispatcher在发送事件的时候会去设置:InputDispatcher首先会为本次要发送的事件寻找一个目标窗口,而在将该事件发送给目标窗口之前,会额外再判断目标窗口之上是否有其他的可见窗口能够遮挡目标窗口,如果有,那么会把FLAG_WINDOW_IS_OBSCURED这个标记加到本次输入事件中。

一个对安全性敏感的App可以检查这个标志,以识别恶意应用程序可能为了误导用户或劫持触摸事件而掩盖其部分内容的情况。一个适当的响应可能是丢弃可疑的触摸事件,或者采取额外的预防措施来确认用户的实际意图。

6.7.3 调用onInterceptTouchEvent尝试拦截当前事件

            // Check for interception.
            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); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

			......
                
            if (!canceled && !intercepted) {
                .....
            }

如果判断当前ViewGroup需要拦截本次事件,那么intercepted会被置为true,那么就不会再去走相关逻辑:

            if (!canceled && !intercepted) {
                .....
            }

if语句里会继续将事件向子View发送。

接下来分几部分分析拦截事件的具体内容。

6.7.3.1 拦截事件的两种情况

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                    ......
                        intercepted = onInterceptTouchEvent(ev);
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

首先进行两项判断:

1)、当前事件动作类型是否是MotionEvent.ACTION_DOWN。

2)、mFirstTouchTarget是否不为空。mFirstTouchTarget的含义需要结合后面的分析才能了解,mFirstTouchTarget不为空,说明在本次gesture之前的事件分发中,我们找到了可以接收事件的目标子View。

拦截事件的情况之一是,如果两项条件满足其一,那么后面会调用ViewGroup.onInterceptTouchEvent来尝试拦截本次事件。如果事件被当前ViewGroup拦截,intercepted会被置为true,那么事件将不会发给ViewGroup的子View。

拦截事件的另一种情况,如果这两项都不满足,说明本次事件的行为不是ACTION_DOWN,且在ACTION_DOWN流程中也没有找到一个目标子View可以接收ACTION_DOWN事件,那么说明当前ViewGroup中的所有子View都没有满足接收本次事件的条件,直接将intercepted置为true,表示本次事件将会被当前VIewGroup拦截,直接走6.7.5。这里说明了,必须有子View能够接收ACTION_DOWN事件,这样接下来的事件,不管是ACTION_POINTER_DOWN还是ACTION_MOVE之类的,才能继续发送给子VIew,否则当前ViewGroup会消费此次事件,不再发送给子View。

另外从这里的判断也能看出,拦截事件是在所有事件行为流程中都可能会发生,不只ACTION_DOWN。

这部分需要结合整体才能更好理解。

6.7.3.2 FLAG_DISALLOW_INTERCEPT

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

对于一般流程来说,手势的起点都是MotionEvent.ACTION_DOWN,因此接下来继续判断,当前ViewGroup的mGroupFlags是否包含FLAG_DISALLOW_INTERCEPT这个标志。

FLAG_DISALLOW_INTERCEPT标志的含义是:

    /**
     * When set, this ViewGroup should not intercept touch events.
     * {@hide}
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123983692)
    protected static final int FLAG_DISALLOW_INTERCEPT = 0x80000;

如果设置了这个标志,那么当前ViewGroup不应该拦截touch事件。

子View可以通过调用ViewGroup.requestDisallowInterceptTouchEvent方法,来阻止其父ViewGroup(不止是其直接父ViewGroup)通过ViewGroup#onInterceptTouchEvent拦截touch事件。父ViewGroup会递归调用ViewGroup.requestDisallowInterceptTouchEvent方法,保证子View所在的View层级结构中的该子View的所有父ViewGroup都能应用到这个标志。

6.7.3.3 ViewGroup.onInterceptTouchEvent

                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                }

如果FLAG_DISALLOW_INTERCEPT标志没有被子View请求,那么调用ViewGroup.onInterceptTouchEvent尝试拦截当前事件。

    /**
     * Implement this method to intercept all touch screen motion events.  This
     * allows you to watch events as they are dispatched to your children, and
     * take ownership of the current gesture at any point.
     *
     * <p>Using this function takes some care, as it has a fairly complicated
     * interaction with {@link View#onTouchEvent(MotionEvent)
     * View.onTouchEvent(MotionEvent)}, and using it requires implementing
     * that method as well as this one in the correct way.  Events will be
     * received in the following order:
     *
     * <ol>
     * <li> You will receive the down event here.
     * <li> The down event will be handled either by a child of this view
     * group, or given to your own onTouchEvent() method to handle; this means
     * you should implement onTouchEvent() to return true, so you will
     * continue to see the rest of the gesture (instead of looking for
     * a parent view to handle it).  Also, by returning true from
     * onTouchEvent(), you will not receive any following
     * events in onInterceptTouchEvent() and all touch processing must
     * happen in onTouchEvent() like normal.
     * <li> For as long as you return false from this function, each following
     * event (up to and including the final up) will be delivered first here
     * and then to the target's onTouchEvent().
     * <li> If you return true from here, you will not receive any
     * following events: the target view will receive the same event but
     * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
     * events will be delivered to your onTouchEvent() method and no longer
     * appear here.
     * </ol>
     *
     * @param ev The motion event being dispatched down the hierarchy.
     * @return Return true to steal motion events from the children and have
     * them dispatched to this ViewGroup through onTouchEvent().
     * The current target will receive an ACTION_CANCEL event, and no further
     * messages will be delivered here.
     */
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

渣翻:

实现此方法来拦截所有触屏移动事件。这允许你在事件被发送给子View的时候监控这些事件,并且随时取得当前gesture的控制权。

使用方法的时候要注意,因为它和View.onTouchEvent有一个相当复杂的交互,而且使用View.onTouchEvent方法需要以正确的方式实现它,ViiewGroup.onInterceptTouchEvent也是一样。事件将会按照以下顺序被接收:

1)、你将会在这里接收到down事件。

2)、这个down事件要么将会被当前ViewGroup的一个子View处理,要么将会被发送给当前ViewGroup的onTouchEvent去处理。这意味着你需要应该实现onTouchEvent方法并且返回true,这样你才能继续接收到当前gesture的余下事件(而不是寻找一个父View来处理当前事件)。而且,通过在onTouchEvent方法中返回true,你将不会在onInterceptTouchEvent中收到接下来的事件,而且所有的touch事件的处理将会像通常一样在onTouchEvent中进行。

3)、只要你从这个方法中返回了false,那么接下来的每一个事件(直到并且包括最终的ACTION_UP),将会被第一时间发送到这里,然后才会被发给target的onTouchEvent方法。

4)、如果你在这里返回true,你将不会接收到任何接下来的事件:target View将会接收到同样的事件,但是附上了MotionEvent.ACTION_CANCEL,而且以后的事件都将会发送给你的onTouchEvent方法,不会再在这里出现。

6.7.4 DOWN行为事件的发送

                // If the event is targeting accessibility focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                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;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x =
                                isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                        final float y =
                                isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        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);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            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);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                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();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }

6.7.4.1 寻找包含accessibility focus View的子View

            if (!canceled && !intercepted) {
                // If the event is targeting accessibility focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    .....
                }
            }

如果当前事件没有被cancel,也没有被6.7.3拦截,那么开始进行事件的分发。

首先,如果当前事件是一个以accessibility focus View为目标的事件,那么尝试从当前ViewGroup中找到那个包含了accessibility focus View的子View。因为我们把accessibility focus View的优先级放的比较高,所以我们希望找到这个View并且第一时间把事件传给它,让它先处理事件。如果它不能处理当前事件,那么我们就清除掉MotionEvent中有关accessibility focus的相关标志,然后再像往常一样向子View发送事件。因为这类和accessibility focus有关事件比较稀有,因此我们先寻找accessibility focus View来避免一直持有accessibility focus相关的状态。

     /**
     * Finds the child which has accessibility focus.
     *
     * @return The child that has focus.
     */
    private View findChildWithAccessibilityFocus() {
        ViewRootImpl viewRoot = getViewRootImpl();
        if (viewRoot == null) {
            return null;
        }

        View current = viewRoot.getAccessibilityFocusedHost();
        if (current == null) {
            return null;
        }

        ViewParent parent = current.getParent();
        while (parent instanceof View) {
            if (parent == this) {
                return current;
            }
            current = (View) parent;
            parent = current.getParent();
        }

        return null;
    }

从findChildWithAccessibilityFocus的实现来看,这个返回的VIew可能不是accessibility focus View,而是包含accessibility focus View的父View。

其实这部分逻辑不只在DOWN流程中才执行,但是childWithAccessibilityFocus只在DOWN流程中才用到,那么是不是放在DOWN流程中比较合适?

6.7.4.2 遍历所有子View前的准备

                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;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x =
                                isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                        final float y =
                                isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            ......
                        }
                    }
                }

1)、首先6.7.4的标题并不准确,这里要处理的事件包括三个:MotionEvent.ACTION_DOWN,可以将MotionEvent拆分发送给多个子View的MotionEvent.ACTION_POINTER_DOWN(这个涉及到多点触摸,当第一根手指按下时,会上报MotionEvent.ACTION_DOWN,后续如果有另外的手指按下,上报的就是MotionEvent.ACTION_POINTER_DOWN)和指针移动事件MotionEvent.ACTION_HOVER_MOVE(这个类型的事件就不讨论了)。

2)、这里的idBitsToAssign对应的多点触控相关的内容,通过MotionEvent.getPointerId可以返回每一根手指对应的唯一独特ID。

3)、调用View.buildTouchDispatchChildList提前构建一个有序的子View队列,该队列根据Z轴顺序和View的绘制顺序排列,以Z轴顺序优先,Z轴上的位置越高,在队列中就越靠近队尾。

6.7.4.3 遍历所有子View,将事件发送给子View

                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            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);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                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);
                        }

分成几部分来看。

6.7.4.3.1 优先处理accessibility focus View
                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

这里我们看到,首先直接遍历所有子View找到包含了accessibility focus View的子View,让这个子View优先处理这个事件。

如果这个子View处理不了当前事件,那么我们再执行一遍正常的分发,那么我们就有可能做了两次遍历。

6.7.4.3.2 过滤掉无效子View
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

1)、View.canReceivePointerEvents表示当前View是否可以接收点触事件,能够接收的条件是,当前子View可见,或者不可见但是在执行一个动画。如果这两个条件都不满足,那么认为当前View无法接收输入事件。

2)、ViewGroup.isTransformedTouchPointInView用来判断当前事件的坐标是否落在这个ViewGroup的范围之内,如果没有自然也不要向这个View发送本次事件。

6.7.4.3.3 目标View的复用
                            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;
                            }

分析完6.7.4.3.5再回过来看这里,那么这里是说,如果在之前的事件分发(ACTION_DOWN或ACTION_POINTER_DOWN)中,已经找到了一个子View可以接收之前的事件,并且将这个子View封装为一个TouchTarget对象了,那么对于本次事件分发(ACTION_POINTER_DOWN),直接将当前事件的pointerId也加入到这个TouchTarget的pointerIdBits中。接着break跳出当前对所有子View的遍历,这样,就不用再去挨个判断哪个子View满足接收当前事件的相关条件了,直接复用上一次的ACTION_DOWN流程或者是ACTION_POINTER_DOWN流程找到的TouchTarget。我老是忍不住拿这个TouchTarget和InputDispatcher分发事件的时候用到的TouchState做比较。InputDIspatcher也是用了TouchState用来保存所有可以接收输入事件的窗口,这两者的作用很相似,都是用来减少重复的搜寻目标窗口或者目标View的工作。毕竟如果在上一次事件发送的时候,如果我们已经找到了一个可以接收事件的目标View,那么后续事件发送的时候,直接复用这个目标View,那会减少很多重复的工作。

这种情形应该也是和多点触控相关的,毕竟每个gesture的第一次ACTION_DOWN流程中,都会重置mFirstTouchTarget,因此如果是单点触控,那么走到这里mFirstTouchTarget应该是为空的,调用getTouchTarget也只会返回NULL。

另外6.7.4.3.2也保证了执行到这里的时候,当前事件的坐标是落在这个可以复用的子View区域中的,不然不加限制,让之前事件分发流程中找到的目标View可以无脑接收到接下来所有的ACTION_POINTER_DOWN,这个逻辑就有点奇怪了。

6.7.4.3.4 调用dispatchTransformedTouchEvent往子View分发事件
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
							......
                            }

这里调用了ViewGroup.dispatchTransformedTouchEvent继续往子View分发事件。

    /**
     * Transforms a motion event into the coordinate space of a particular child view,
     * filters out irrelevant pointer ids, and overrides its action if necessary.
     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
     */
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        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) {
            Log.i(TAG, "Dispatch transformed touch event without pointers in " + this);
            return false;
        }

        // If the number of pointers is the same and we don't need to perform any fancy
        // irreversible transformations, then we can reuse the motion event for this
        // dispatch as long as we are careful to revert any changes we make.
        // Otherwise we need to make a copy.
        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);
        }

        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } 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;
    }

这个方法虽然很长,但是并不复杂,核心思想是:

判断传入的子View是否为空,如果为空,那么调用当前基类View的dispatchTouchEvent方法,表示当前ViewGroup自己处理当前事件。否则,调用子View的dispatchTouchEvent方法将事件继续分发给子View。在当前流程中,传入的子View都不为空。

如果要把事件传给子View,那么需要额外判断:

1)、当前事件是否被标记为cancel。cancel流程下,我们不需要再将事件进行任何转换或者过滤,因为重要的点在于事件行为(即ACTION_CANCEL),而不是事件内容(事件的坐标之类的)。

2)、需要在将事件发送给子View之前,将事件转换到子View的坐标系统中。如果当前事件的pointerId和我们希望的pointerId相同,那么我们不需要对事件执行任何特殊的不可逆转的转换,然后只要我们注意恢复我们对当前事件做出的任何修改,就可以复用当前事件。否则我们需要复制一份当前事件的拷贝防止事件转换影响到原事件。

关于这里的pointerId进一步的解释如下:

        // Calculate the number of pointers to deliver.
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

这里的传参desiredPointerIdBits代表的是坐标落在要接下来接收事件的子View的区域里的那些pointer(在一次多点触摸中,多个手指很有可能分别落在不同的子View区域中的),oldPointerIdBits代表的如果是当前event中包含的所有pointer,那么newPointerIdBits代表的就是oldPointerIdBits经过desiredPointerIdBits过滤后,子View接收的那几个pointer。

6.7.4.3.5 将可以接收当前事件的子View封装为TouchTarget
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

如果在6.7.4.3.4中找到一个可以处理当前事件的View,即dispatchTransformedTouchEvent返回了true,那么进行以下处理:

1)、记录当前事件的按下时间,处理当前事件的子View在ViewGroup的mChildren数组中的索引,以及当前事件的x,y坐标。

2)、调用ViewGroup.addTouchTarget方法将处理当前事件的子View以及事件对应的pointerId打包封装为一个TouchTarget对象。

    /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        if (ViewDebugManager.DEBUG_MOTION) {
            Log.d(TAG, "addTouchTarget:child = " + child + ",pointerIdBits = " + pointerIdBits
                    + ",target = " + target + ",mFirstTouchTarget = " + mFirstTouchTarget
                    + ",this = " + this);
        }
        
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

由于在多点触控中,每一根手指都对应一个pointerId,那么TouchTarget的作用就是,保存可以处理当前事件的子View对象(通过TouchTarget的成员变量child保存),以及,这个View对象具体可以处理哪几根指头(通过TouchTarget的pointerIdBits成员变量保存),也是TouchTarget的注释所表达的意思:

    /* Describes a touched view and the ids of the pointers that it has captured.
     *
     * This code assumes that pointer ids are always in the range 0..31 such that
     * it can use a bitfield to track which pointer ids are present.
     * As it happens, the lower layers of the input dispatch pipeline also use the
     * same trick so the assumption should be safe here...
     */
    private static final class TouchTarget {
        ......

        // The touched child view.
        @UnsupportedAppUsage
        public View child;

        // The combined bit mask of pointer ids for all pointers captured by the target.
        public int pointerIdBits;
        
        .......
    }

最后这里通过:

        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;

构建出了一个TouchTarget的链表,每调用一次ViewGroup.addTouchTarget,就会在链表头插入一个新的元素,mFirstTouchTarget始终指向链表第一个元素。

6.7.4.3.6 对找不到目标View的事件的处理
                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }

对于单点触控,如果mFirstTouchTarget不为空,那么newTouchTarget应该也是不会为空的,所以这个部分处理的还是多点触控的逻辑。

这部分逻辑发生的条件可能是,假设,现在第一根手指按下,对应ACTION_DOWN流程,此时找到了一个可以处理事件的子View,那么可以基于该子View创建一个TouchTarget,并且mFirstTouchTarget也会指向这个TouchTarget。此时第二根手指按下,对应ACTION_POINTER_DOWN流程,但是如果这时找不到一个可以处理当前事件的子View,那么newTouchTarget也就仍然为空,但是mFirstTouchTarget却不空,那这部分的逻辑就会执行:

将这个pointerID加入到最近一次加入到TouchTarget链表中的TouchTarget,表示最近一次添加的TouchTarget将会处理本次无View认领的MotionEvent事件。

6.7.5 由当前ViewGroup处理本次事件

            // Dispatch to touch targets.            
			if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                ......
            }

这里mFirstTouchTarget为空,说明当前事件可能被cancel了,或者可能被当前ViewGroup拦截了,或者子View中没有任何一个能够处理这个事件。

如果发生以上三种情况之一,我们就调用dispatchTransformedTouchEvent方法。这里传入的child参数为NULL,那么这一步的目的是将当前ViewGroup看作一个普通的View,调用VIew.dispatchTouchEvent,由当前ViewGroup去处理当前事件:

            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                ......
                handled = child.dispatchTouchEvent(event);
                ......
            }

之前在遍历子View的时候也调用过dispatchTransformedTouchEvent方法,但是由于当时传入的参数chid都不为空,所以事件其实是发送给ViewGroup的子View去处理了。

    /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }
        boolean result = false;

		......

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

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

		.....

        return result;
    }

这个方法可以分成三个部分。

6.7.5.1 accessibility focus

        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

accessibility focus相关的部分在6.7.1中也有提到,如果MotionEvent.isTargetAccessibilityFocus返回true,表示当前事件是面向accessibility focus View的。如果当前View不是accessibility focus View,那么不要让这个View处理当前事件,直接返回false。否则,清除掉当前事件的相关标志位,并且开始正常的事件分发流程。

6.7.5.2 OnTouchListener.onTouch

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

如果ListenerInfo类型的成员变量mListenerInfo中,OnTouchListener类型的成员变量mOnTouchListener不为空,那么首先调用mOnTouchListener.onTouch方法去处理当前事件。

成员变量mListenerInfo在盗用View.getListenerInfo的时候赋值化:

    @UnsupportedAppUsage
    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }

ListenerInfo的成员变量mOnTouchLIstener在调用View.setOnTouchListener的时候赋值:

    /**
     * Register a callback to be invoked when a touch event is sent to this view.
     * @param l the touch listener to attach to this view
     */
    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

这个很常用就不用再赘述用如何使用了。

最后看下OnTouchListener的定义:

    /**
     * Interface definition for a callback to be invoked when a touch event is
     * dispatched to this view. The callback will be invoked before the touch
     * event is given to the view.
     */
    public interface OnTouchListener {
        /**
         * Called when a touch event is dispatched to a view. This allows listeners to
         * get a chance to respond before the target view.
         *
         * @param v The view the touch event has been dispatched to.
         * @param event The MotionEvent object containing full information about
         *        the event.
         * @return True if the listener has consumed the event, false otherwise.
         */
        boolean onTouch(View v, MotionEvent event);
    }

OnTouchListener是一个接口,定义了一个触摸事件被发送给当前View的时候调用的回调。这个回调将会在触摸事件发送给View之前被调用,也就是View.onTouchEvent。

onTouch方法会在一个触摸事件发送给一个View的时候被调用。这允许listener在目标View之前可以得到一个回应触摸事件的先机。

那这里的代码就说明了,当一个触摸事件发送给某一个View的时候,如果这个View注册了OnTouchListener,那么先由OnTouchListener.onTouch处理当前事件。如果OnTouchListener.onTouch处理了这个触摸事件,那么这个事件就不再发送给View.onTouchEvent。

6.7.5.3 View.onTouchEvent

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

如果上一步没有处理当前事件,那么把MotionEvent发送给当前View的onTouchEvent方法中去处理。

    /**
     * Implement this method to handle touch screen motion events.
     * <p>
     * If this method is used to detect click actions, it is recommended that
     * the actions be performed by implementing and calling
     * {@link #performClick()}. This will ensure consistent system behavior,
     * including:
     * <ul>
     * <li>obeying click sound preferences
     * <li>dispatching OnClickListener calls
     * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
     * accessibility features are enabled
     * </ul>
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        ......
    }

实现这个方法用来处理触屏运动事件。

如果此方法用于检测单击操作,建议通过实现和调用performClick来执行这些操作。这将确保一致的系统行为,包括:

遵守点击声音偏好,分发OnClickListener调用,处理AccessibilityNodeInfo.ACTION_CLICK。

返回true说明当前事件被处理。

唯一需要所说的点,就是如果你通过View.setOnClickListener注册了一个监听对点击动作的OnClickListener:

    /**
     * Register a callback to be invoked when this view is clicked. If this view is not
     * clickable, it becomes clickable.
     *
     * @param l The callback that will run
     *
     * @see #setClickable(boolean)
     */
    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

那么在ACTION_UP动作的时候,会调用View.performClickInternal -> View.performClick -> OnClickListener.onClick来回调OnClickListener的onClick方法。

6.7.6 将事件发送给TouchTarget去处理

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                ......
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                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;
                }
            }

6.7.6.1 将事件发送给目标子View

                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }

不同于6.7.5,如果mFirstTouchTarget不为空,那么说明在之前的DOWN流程中找到了可以处理MotionEvent的TouchTarget,或者说目标子View,那么这里遍历TouchTarget链表,对于每一个TouchTarget中保存的子View,都调用ViewGroup.dispatchTransformedTouchEvent将事件发送给目标子View去处理。这也就是说,如果事件在ACTION_DOWN的时候被ViewGroup拦截,mFirstTouchTarget就会为NULL,那么事件就不会发送给当前ViewGroup的子View去处理了。

这也是一种目标子View的复用,只不过这种复用是针对ACTION_MOVE、ACTION_UP来说的,对于这些行为的事件,不用再去遍历当前ViewGroup的所有子View去寻找可以处理当前输入事件的目标View,而直接复用DOWN流程中找到的目标子View即可。6.7.4.3.3中也讲到了子View的复用,那里的复用指的是ACTION_POINTER_DOWN流程复用ACTION_DOWN和ACTION_POINTER_DOWN流程中找到的目标子VIew。

6.7.6.2 避免重复分发

                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    }

要知道ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_MOVE、ACTION_UP等都会走到这里,而对于ACTION_DOWN、ACTION_POINTER_DOWN来说,在6.7.4.3.4中,可能已经调用了ViewGroup.dispatchTransformedTouchEvent把这些事件分发给子View去处理了,那么在6.7.4.3.5中alreadyDispatchedToNewTouchTarget会被置为true,也会基于目标子View构建一个newTouchTarget,所以在这里就不用重复调用ViewGroup.dispatchTransformedTouchEvent了。

但是有一个问题,对于当前事件,如果它可以已经在6.7.4.3.4中找到了目标View,并且目标View被保存到newTouchTarget中。但是如果TouchTarget链表中有多个TouchTarget,那么在遍历到newTouchTarget之前,它还是可能会被发送给其他TouchTarget中保存的子View,即使这些子View不是当前事件的目标View。那么这里是否应该直接将找到了目标View的事件直接发送给目标View,不用再遍历了?

6.7.6.3 cancel情况的处理

                        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;
                        }

想象这样一种情况,如果当前ViewGroup没有在ACTION_DOWN的时候拦截事件,而是选择在ACTION_MOVE的时候拦截事件,那么首先由于mFirstTouchTarget不为NULL,代码就会执行到这里,然后这里的cancelChild就会置为true,接下来在ViewGroup.dispatchTransformedTouchEvent方法中,子View收到事件的action会被设置为ACTION_CANCEL:

        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;
        }

最后,目标子View对应的TouchTarget将会从当前ViewGroup的TouchTarget链表中移除,那么目标子View只会收到两次事件,ACTION_DOWN和ACTION_CANCEL。

6.7.7 在gesture结束后清除TouchTarget

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }

由于TouchTarget保存了每一次gesture中可以接收事件的目标子View,那么在每一次gesture的结束,都调用ViewGroup.resetTouchState将本次gesture保存的一些状态清除。如果只是某一根手指抬起,ACTION_POINTER_UP,那么只将该手指从TouchTarget中移除。

6.7.8 ViewGroup.dispatchTouchEvent小结

从以上的分析,可以大概总结,ViewGroup.dispatchTouchEvent的事件分发流程关键节点主要是两个:

1)、调用ViewGroup.onInterceptTouchEvent拦截MotionEvent,如果拦截成功,那么事件不会再下发给它的子View去处理。

2)、调用ViewGroup.dispatchTransformedTouchEvent继续分发事件,这基于目标子View的情况可能有三种分支:

2.1)、没有目标子View,当前ViewGroup调用View.dispatchTouchEvent自己处理事件。

2.2)、目标子View是一个ViewGroup,调用目标子View的ViewGroup.dispatchTouchEvent继续分发事件。

2.3)、目标子View不是一个ViewGroup,调用目标子View的View.dispatchTouchEvent处理事件。

为了验证上面分析的内容,自己写一个demo App进行检验,App的层级结构是:

    View Hierarchy:
      DecorView@a063fe8[MainActivity]
        android.widget.LinearLayout{48fccda V.E...... ........ 0,0-1200,1824}
          android.view.ViewStub{bb516fb G.E...... ......I. 0,0-0,0 #10201c4 android:id/action_mode_bar_stub}
          android.widget.FrameLayout{1921601 V.E...... ........ 0,48-1200,1824 #1020002 android:id/content}
            com.test.inputinviewhierarchy.MyLayout{3addca6 V.E...... ........ 0,0-1200,1776}
              com.test.inputinviewhierarchy.MyTextView{8a5b6e7 VFED..C.. ........ 0,100-1200,200 #7f08016f app:id/text1}
        android.view.View{9f37294 V.ED..... ........ 0,1824-1200,1920 #1020030 android:id/navigationBarBackground}
        android.view.View{c09113d V.ED..... ........ 0,0-1200,48 #102002f android:id/statusBarBackground}

忽略不必要的部分:

    View Hierarchy:
      DecorView@a063fe8[MainActivity]
        android.widget.LinearLayout{48fccda V.E...... ........ 0,0-1200,1824}
          android.widget.FrameLayout{1921601 V.E...... ........ 0,48-1200,1824 #1020002 android:id/content}
            com.test.inputinviewhierarchy.MyLayout{3addca6 V.E...... ........ 0,0-1200,1776}
              com.test.inputinviewhierarchy.MyTextView{8a5b6e7 VFED..C.. ........ 0,100-1200,200 #7f08016f app:id/text1}

其中MyLayout是我自己写的Activity加载的布局结构,继承RelativeLayout,是一个ViewGroup。MyTextView继承TextView,是一个非ViewGroup的普通View。

分别看下MyLayout主动拦截MotionEvent,或者MyTextView不处理MotionEvent,会导致什么样的结果。

6.7.8.1 默认流程

默认流程即没有经过任何特殊处理,MyLayout默认不拦截MotionEvent,MyTextView默认处理当前事件。

在这里插入图片描述

由于MyTextView处理了本次事件,那么在每一级的ViewGroup中,都可以构建一个TouchTarget链表出来,根据6.7.6的分析,后续ACTION_MOVE和ACTON_UP 等,直接发送给TouchTarget去处理即可,当前ViewGroup无需自己处理。

6.7.8.2 MyTextView不处理事件

这里修改MyTextView的逻辑,不让它处理本次事件:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // return super.onTouchEvent(event);
        return false;
    }

那么事件分发的流程是:

在这里插入图片描述

1)、ACTION_DOWN:由于MyLayout的唯一子View不处理本次事件,导致在MyLayout这一级中,构建不出TouchTarget链表,因此就只能由MyLayout调用View.dispatchTouchEvent来处理本次事件,即6.7.5的内容。但是MyLayout继承自RelativeLayout,本身也是无法处理MotionEvent的,其它的Layout也是这样的情况,所以View层级结构中,自下到上,每一级父View都会通过View.dispatchTouchEvent -> View.onTouchEvent尝试去处理本次事件,但是在它们的View.onTouchEvent都返回了false,所以事件最终没有得到处理,传给了下一个InputStage:SyntheticInputStage。

2)、ACTION_MOVE、ACTION_UP:由于在ACTION_DOWN中,找不到一个目标View可以处理本次事件,那么每一级ViewGroup中的TouchTarget链表自然也就没有建立起来。根据6.7.5,此时,最顶层的DecorVIew将不会向下发送事件,而是选择自己处理。

6.7.8.3 MyLayout拦截ACIION_DOWN并处理

这里修改MyLayout的逻辑,在ViewGroup.onInterceptTouchEvent中,判断当前事件action为ACTION_DOWN的时候拦截事件并且消费掉该事件:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
//        return super.onInterceptTouchEvent(ev);
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            return true;
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
//        return super.onTouchEvent(event);
        return true;
    }

那么事件分发的流程是:

在这里插入图片描述

和6.7.8.1相比,区别在于MyLayout选择拦截ACTION_DOWN,那么事件将不会再发送给MyLayout的子View,MyTextView,MyLayout变成了本次gesture分发的最底层VIew。

6.7.8.4 MyLayout拦截ACIION_DOWN不处理

这里修改MyLayout的逻辑,在ViewGroup.onInterceptTouchEvent中,判断当前事件action为ACTION_DOWN的时候拦截事件但是不作处理:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
//        return super.onInterceptTouchEvent(ev);
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            return true;
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

那么事件分发的流程是:

在这里插入图片描述

和6.7.8.2相比,区别在于MyLayout选择拦截ACTION_DOWN,那么事件将不会再发送给MyLayout的子View,MyTextView,MyLayout变成了本次gesture分发的最底层VIew。

和6.7.8.3相比,区别在于ACTION_DOWN流程中,在View层级结构中自下而上,每一级ViewGroup都会通过View.dispatchTouchEvent -> View.onTouchEvent尝试去处理本次事件,但是由于每一级ViewGroup的默认onTouchEvent实现都是不处理事件的,所以事件最终没有得到处理,传给了下一个InputStage:SyntheticInputStage。后续事件中由于TouchTarget链表在每一级中都没有构建起来,所以事件直接在DecorView这一级中被消费。

6.7.8.5 MyLayout拦截ACTION_MOVE并处理

这里修改MyLayout的逻辑,在ViewGroup.onInterceptTouchEvent中,判断当前事件action为ACTION_DOWN的时候拦截事件并且消费掉该事件:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
//        return super.onInterceptTouchEvent(ev);
        if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            return true;
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
//        return super.onTouchEvent(event);
        return true;
    }

那么事件分发的流程是:

在这里插入图片描述

1)、ACTION_DOWN:该流程和6.7.8.1一样,最终在每一级ViewGroup中都构建了一个TouchTarget链表。

2)、ACTION_MOVE:虽然ACTION_MOVE被MyLayout拦截,但是此时并不是像6.7.8.3一样,本次事件不会再发送给MyTextView。事件仍然会发送给MyTextView,因为在ACTION_DOWN流程中,我们基于MyTextView构建了一个TouchTarget对象,并且把mFirstTouchTarget指向了该对象。但是事件的action从ACTION_MOVE被转化为ACTION_CANCEL,随后以MyTextView为目标View的TouchTarget被移除,即6.7.6.3的分析。

3)、后续的ACTION_MOVE、ACTION_CANCEL:由于第2步中,在MyLayout一级中,没有了子View可以处理事件,那么最终事件会由MyLayout处理。

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到