Android触摸事件派发(一) ViewGroup的dispatchTouchEvent()

ViewGroup的派发事件代码主要由dispatchTouchEvent(MotionEvent ev)方法实现,如下

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        // 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 (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                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;
                    }
                }
            }

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

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }

  mInputEventConsistencyVerifier是用来调试使用的,正式版本里面为null
9-10行是处理辅助功能的,这个需要在设置里开启辅助功能才生效,暂不分析这块的逻辑。
13行声明的变量代表事件是否被消费,初始为false。
14行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;
    }

  onFilterTouchEventForSecurity方法主要过滤的是当前Window被遮挡的情况下的触摸事件。
  接着回到ViewGroup里面的处理事件,获取到事件action,并且与MotionEvent.ACTION_MASK做&操作,因为ACTION_POINTER_DOWN和ACTION_POINTER_UP的action里面包括pointer index。

事件开始,进行初始化

19到24行,在事件为ACTION_DOWN的情况下,做一些初始化处理。事件正常可能由ACTION_DOWN,ACTION_MOVE,ACTION_POINTER_DOWN(多于一个手指时),ACTION_POINTER_UP(多于一个手指时),ACTION_UP组成。而ACTION_DOWN是事件的开始
,这个时候设置状态和初始化。会调用cancelAndClearTouchTargets()和resetTouchState()

    /**
     * Cancels and clears all touch targets.
     */
    private void cancelAndClearTouchTargets(MotionEvent event) {
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }

            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            clearTouchTargets();

            if (syntheticEvent) {
                event.recycle();
            }
        }
    }

  cancelAndClearTouchTargets方法通过名字可以知道取消和清理触摸对象的,开始判断成员变量mFirstTouchTarget不等于null,重点说一下mFirstTouchTarget,他是一个链表,连接所有的需要处理事件的当前ViewGroup的子View,这个链表是在事件为ACTION_DOWN和ACTION_POINTER_DOWN的时候来确定的,后续在发生的事件就会由mFirstTouchTarget连接的对象来处理。正常情况下,一个事件结束的时候,mFirstTouchTarget应该为null。cancelAndClearTouchTargets方法在mFirstTouchTarget不等于null的情况下,会合成一个ACTION_CANCEL事件发送到对象。之后调用clearTouchTargets()方法,

    /**
     * Clears all touch targets.
     */
    private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }

  clearTouchTargets()用来清除所有的触摸对象,所有的触摸对象会调用recycle(),并且在最后会将mFirstTouchTarget 置为null。

        public void recycle() {
            if (child == null) {
                throw new IllegalStateException("already recycled once");
            }

            synchronized (sRecycleLock) {
                if (sRecycledCount < MAX_RECYCLED) {
                    next = sRecycleBin;
                    sRecycleBin = this;
                    sRecycledCount += 1;
                } else {
                    next = null;
                }
                child = null;
            }
        }
    }

  触摸对象有个缓存,缓存的最大容量是MAX_RECYCLED,它的值是32,recycle()方法会在缓存容量不到MAX_RECYCLED的时候,将该TouchTarget对象放进缓存容量,在将对象的child成员变量置为null。
cancelAndClearTouchTargets再最后会检查Motion对象如果是合成的,调用Motion对象的recycle()。
dispatchTouchEvent()方法在ACTION_DOWN下面接着调用resetTouchState(),
resetTouchState()代码如下

    /**
     * Resets all touch state in preparation for a new cycle.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

  该方法是为了新的事件循环,重设所有的触摸状态。也会调用clearTouchTargets(),接着调用resetCancelNextUpFlag方法,

    /**
     * Resets the cancel next up flag.
     * Returns true if the flag was previously set.
     */
    private static boolean resetCancelNextUpFlag(@NonNull View view) {
        if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
            view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
            return true;
        }
        return false;
    }

  该方法检查View的cancel next up flag,如果View设置过,将该标志清除,并且返回true。如果没有该标志返回false。cancel next up flag会在事件的后续处理中,用来判断ACTION_CANCEL事件。
通过在View的代码中搜索PFLAG_CANCEL_NEXT_UP_EVENT标志的设置位置,发现在performButtonActionOnTouchDown()和onStartTemporaryDetach()中会设置该标志。performButtonActionOnTouchDown()是在执行鼠标右键的时候,onStartTemporaryDetach()是在需要临时将View从父控件detach的时候执行。
resetTouchState()接着会清除FLAG_DISALLOW_INTERCEPT(禁止拦截)标志,还会将mNestedScrollAxes设置为SCROLL_AXIS_NONE。

获取拦截状态

  接着回到dispatchTouchEvent()方法中,28到42行用来判断是否拦截事件,判断拦截事件是在事件为ACTION_DOWN或者触摸对象链表不为null的情况下。如果当前控件设置了禁止拦截状态,那么得到的拦截状态就会为false。如果没有设置,则会调用onInterceptTouchEvent()来得到拦截状态intercepted。onInterceptTouchEvent()方法是用来设置是否拦截事件的,并且这个方法只在ViewGroup类型的控件中存在。这个方法默认对触摸事件是不拦截的。
  接下来的46到48行,通过注释来看,是说当前控件是拦截状态或者已经有一个处理手势的View,就清除辅助功能标记的状态,做正常事件的派发。
  接下来判断是不是取消状态,设置canceled变量的值,就是通过上面已经说过的resetCancelNextUpFlag函数来做判断,或者当前的事件Action就是ACTION_CANCEL。
  55到57行的代码来判断事件是否分解。如果当前控件的mGroupFlags有FLAG_SPLIT_MOTION_EVENTS标志,并且当前事件不是鼠标事件,就可以分解。事件分解是用来处理多点触控的。这个标志在HONEYCOMB版本及之后默认添加在mGroupFlags中。
  接下来定义两个变量newTouchTarget和alreadyDispatchedToNewTouchTarget,newTouchTarget是新找到的触摸对象,alreadyDispatchedToNewTouchTarget是一个boolean,代表当前的事件是否已经分发给了新触摸对象。

寻找触摸事件派发对象

寻找触摸事件派发对象的代码单独提出来如下:

            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 (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                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;
                    }
                }
            }

  以上代码主要是为了寻找触摸事件派发对象的。
  在不是取消状态并且不是拦截状态的情况下,开始寻找当前事件的触摸对象。注意,这两个状态是个大前提,分析代码的时候,要考虑这点。
  9行代码,在事件是isTargetAccessibilityFocus的情况下,先找到accessibility focus的View。
  再接着向下12到14行代码,就是在三种情况下去寻找触摸事件。 1、事件类型是ACTION_DOWN  2、split为true并且事件类型为ACTION_POINTER_DOWN  3、事件类型是ACTION_HOVER_MOVE。
  第一种对应第一根手指按下的情况,第二种对应第二根手指按下的情况(ViewGroup初始化的时候,在AndroidManifest文件中配置的targetSdkVersion大于等于API 11,默认设置FLAG_SPLIT_MOTION_EVENTS flag,所以目标targetSdkVersion API 11往上的split为true),第三种对应鼠标悬浮移动的情况。只考虑触摸事件,可以先不管第三种情况。
  先获取到当前事件pointer index,再通过它获取到id的bit位值。因为split为true,所以idBitsToAssign里对应id的位为1。调用removePointersFromTouchTargets()先清除触摸事件派发对象链中对象接收该id的标志位,如果派发对象的接收id的标志位都是0,将触摸对象从对象链中删除。
  事件的pointer index从0开始,如果事件是ACTION_DOWN,则其pointer index为0,并且在多点触控中,如果手指抬起,该值会变化。事件的id也是从0开始,该值不会随着手指抬起发生变化。所以查找特定手指的事件,需要通过事件的id来做依据。
  newTouchTarget代表新的触摸对象,当前是null,如果现在当前ViewGroup对象的孩子控件数量不为0,就去寻找。先得到坐标x,y值,可以看到如果是鼠标事件,使用getXCursorPosition()和getYCursorPosition()得到的x,y的坐标值。接着调用buildTouchDispatchChildList()获得子View集合preorderedList,这个集合是按照子View的Z坐标的位置从小到大排序的。看一下该方法的代码

    public ArrayList<View> buildTouchDispatchChildList() {
        return buildOrderedChildList();
    }
    …………
        ArrayList<View> buildOrderedChildList() {
        final int childrenCount = mChildrenCount;
        if (childrenCount <= 1 || !hasChildWithZ()) return null;

        if (mPreSortedChildren == null) {
            mPreSortedChildren = new ArrayList<>(childrenCount);
        } else {
            // callers should clear, so clear shouldn't be necessary, but for safety...
            mPreSortedChildren.clear();
            mPreSortedChildren.ensureCapacity(childrenCount);
        }

        final boolean customOrder = isChildrenDrawingOrderEnabled();
        for (int i = 0; i < childrenCount; i++) {
            // add next child (in child order) to end of list
            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
            final View nextChild = mChildren[childIndex];
            final float currentZ = nextChild.getZ();

            // insert ahead of any Views with greater Z
            int insertIndex = i;
            while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
                insertIndex--;
            }
            mPreSortedChildren.add(insertIndex, nextChild);
        }
        return mPreSortedChildren;
    }

  该方法首先调用了另外的一个函数buildOrderedChildList(),继续进入方法里面,如果子View的数量小于等于1或者子View的Z坐标值都是0,该方法返回null。意味着不用管Z坐标值,要么按照子View的添加顺序,要么按照自定义顺序来寻找触摸对象。这里要说下ViewGroup自定义顺序,需要调用setChildrenDrawingOrderEnabled()函数,并且要重写getChildDrawingOrder(int childCount, int drawingPosition)函数。前者用来设置mGroupFlags的FLAG_USE_CHILD_DRAWING_ORDER标志,后者用来得到顺序号。buildOrderedChildList()接下来就是按照子View Z坐标值按照从小到大的顺序放到成员变量mPreSortedChildren中并作为结果返回。
  返回到前面函数32行,如果刚才buildTouchDispatchChildList()返回的结果preorderedList不为null,那么变量customOrder的值就为false,因为结果集已经按照Z轴坐标大小排过序了,后面直接就按照变量preorderedList集合里面的顺序来查找。
  如果刚才buildTouchDispatchChildList()返回的结果preorderedList为null,并且调用过setChildrenDrawingOrderEnabled(),那么isChildrenDrawingOrderEnabled()返回结果就为true,自然变量customOrder的值就为true。下面调用getAndVerifyPreorderedIndex()函数就可以通过重写的getChildDrawingOrder(int childCount, int drawingPosition)得到对应的子View的查找序号。这种情况就对应我们没有设置子View的Z坐标值,然后想要改变子View的查找顺序。
  如果返回的结果preorderedList为null,并且没有setChildrenDrawingOrderEnabled(),这种情况就按照子View的添加顺序的倒序来查找触摸对象。
  35行开始的循环就是按照我上面描述的额三种情况来寻找触摸对象的。注意,看到顺序是从childrenCount - 1往前找的。怎么判断子View能不能处理该事件呢?通过40行到43行代码可以知道,view是canReceivePointerEvents()并且isTransformedTouchPointInView(x, y, child, null)。

    /**
     * Returns whether this view can receive pointer events.
     *
     * @return {@code true} if this view can receive pointer events.
     * @hide
     */
    protected boolean canReceivePointerEvents() {
        return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
    }

  1、view是canReceivePointerEvents(),通过代码可知view必须可见或者计划播放或正播放相关动画不为null。

    /**
     * Returns true if a child view contains the specified point when transformed
     * into its coordinate space.
     * Child must not be null.
     * @hide
     */
    @UnsupportedAppUsage
    protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempLocationF();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }
………………………………
   
    public void transformPointToViewLocal(float[] point, View child) {
        point[0] += mScrollX - child.mLeft;
        point[1] += mScrollY - child.mTop;

        if (!child.hasIdentityMatrix()) {
            child.getInverseMatrix().mapPoints(point);
        }
    }

  2、isTransformedTouchPointInView用来判断触摸事件的坐标点是否落在View的范围之内。调用transformPointToViewLocal()方法将坐标值转化为子View里面的坐标值,然后通过pointInView()方法判断触摸点是否在View的范围之内。
  如果这两个条件都满足了,那么触摸对象就找到了。找到触摸对象之后,返回到代码45行,调用getTouchTarget(child)来在触摸对象链表中是否能找到。

    /**
     * Gets the touch target for specified child view.
     * Returns null if not found.
     */
    private TouchTarget getTouchTarget(@NonNull View child) {
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            if (target.child == child) {
                return target;
            }
        }
        return null;
    }

  触摸派发对象使用TouchTarget类来描述的,通过TouchTarget类的内部成员next来维持一个链表。如果得到的触摸派发对象不为null,说明该对象已经添加到链表中了。这种情况应该对应的是MotionEvent.ACTION_POINTER_DOWN的情况(不是第一个手指按下的时候),并且按到了同一个控件上面。
  代码46行到51行,如果发现控件已经在触摸对象链表中,将待处理的控件的id bit位设置到触摸对象里面就跳出循环了,这个事件的处理需要走到下面的代码了。
  代码53行调用resetCancelNextUpFlag(child),去除控件的PFLAG_CANCEL_NEXT_UP_EVENT标志。
  接着会调用dispatchTransformedTouchEvent()方法,去处理控件的事件。dispatchTransformedTouchEvent()这个方法后面也会用到,等到后面再讲。在这里如果这个方法返回true,说明这个控件消费了当前事件,设置成员变量mLastTouchDownTime的值。57行到67行是为了设定mLastTouchDownIndex,preorderedList是前面说过的子View按照Z轴坐标大小从小到大排列的一个集合,如果preorderedList不为null,看代码注释是为了寻找当前消费事件的子VIew在mChildren中的顺序。因为变量childIndex是在preorderedList集合中的顺序,不过看他的代码可能有点问题,因为变量children本身就是mChildren,感觉这块children应该写成preorderedList。后面接着设置mLastTouchDownX和mLastTouchDownY,然后会调用addTouchTarget()方法,将控件加入到触摸事件对象链表中,后续的相关事件会接着让该控件处理,还会将变量alreadyDispatchedToNewTouchTarget设置为true,并且会跳出循环。设置变量alreadyDispatchedToNewTouchTarget的值表明这个事件已经处理完毕,下面的处理代码就会跳过。如果dispatchTransformedTouchEvent()方法返回的是false,会接着循环下一个控件,接着寻找触摸事件对象。
  寻找触摸事件对象的for循环完毕之后,如果preorderedList不为null,会清空它,因为不再需要它了。
  寻找触摸对象的最后一段代码,即82行到90行的if。newTouchTarget == null && mFirstTouchTarget != null这种情况,是什么情况?这种情况应该是多点触控,已经发现了触控事件对象的情况下,另一只手指也按下了,但是这个手指的触摸事件没找到对应的子View进行处理,也就是上面说的每个子View的dispatchTransformedTouchEvent()方法返回了false,这个时候,newTouchTarget==null,mFirstTouchTarget != null。看下这个处理,会将newTouchTarget设置成mFirstTouchTarget链表中最后一个。mFirstTouchTarget链表中最后一个对应最先加入mFirstTouchTarget的触摸对象,后加入的会排在链表的前面。设置完newTouchTarget之后,将newTouchTarget的pointerIdBits对应的事件的id bit位置位,表示该触摸对象接收该事件。

派发触摸对象处理事件

  这一段代码如下:

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

  前面寻找了触摸对象之后,会派发对应事件。看这代码主要就是通过dispatchTransformedTouchEvent()事件进行处理,前面在寻找触摸事件对象,也是通过该方法执行ACTION_DOWN和ACTION_POINTER_DOWN事件的。
  前面寻找触摸对象的代码是在事件类型为ACTION_DOWN和ACTION_POINTER_DOWN的时候执行的,这个派发事件的代码是处理所有的事件类型的。只不过如果前面处理过ACTION_DOWN和ACTION_POINTER_DOWN之后,在这块代码是通过变量alreadyDispatchedToNewTouchTarget 来避免重复处理的。
  如果mFirstTouchTarget 对象为null,则说明前面在该ViewGroup的子View中没找到对应的触摸对象。没找到触摸对象,就会将参数child设置为null,这个就是交给ViewGroup本身来处理。
  如果mFirstTouchTarget 对象不为null,则需要将触摸事件派发给它,由它进行处理。循环触摸对象链表,让每个触摸对象处理事件。可以看到如果alreadyDispatchedToNewTouchTarget变量为true,并且触摸对象和newTouchTarget是同一个,则直接设置变量handled的值为true,表示处理过了。对于其他的触摸对象则会调用dispatchTransformedTouchEvent()方法进行处理。只要某一个对象的dispatchTransformedTouchEvent()返回true,就会将变量handled的值设为true。
  还可以看到控件如果设置了PFLAG_CANCEL_NEXT_UP_EVENT标识或者变量intercepted为true,这个时候会将cancelChild赋值true,这个值会传递到dispatchTransformedTouchEvent()方法的参数cancel,进行处理。intercepted是在前面的段落 获取拦截状态 中描述了该值的获取。dispatchTransformedTouchEvent()方法的参数cancel为true,则会生成ACTION_CANCEL事件进行传递,后续分析。如果cancelChild的值为true,还会将该触摸对象从触摸对象链中去除,代码22到31行就是去除触摸对象,从链表中去除了之后,会调用recycle()方法进行回收。predecessor变量就是为了从链表中去除当前触摸对象的。

dispatchTouchEvent()收尾的清理工作

最后一部分的代码如下:

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

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;

  如果变量canceled等于true或者事件是ACTION_UP或者ACTION_HOVER_MOVE,会执行resetTouchState()方法。canceled的赋值是在前面的 获取拦截状态 那部分,如果当前View设置了PFLAG_CANCEL_NEXT_UP_EVENT或者事件是ACTION_CANCEL的时候,该变量就等于true。事件是ACTION_UP则是事件流的最后一个事件,ACTION_HOVER_MOVE是鼠标的相关事件。在这几种情况下,resetTouchState()方法在前面也分析过相关的代码了。从这里可以知道,事件流结束之后,会将mFirstTouchTarget触摸事件链表置为null,即清除所有触摸对象。
  在事件类型为ACTION_POINTER_UP,即多点触控中,抬起了一个手指,会得到该手指的id bit位idBitsToRemove,然后调用removePointersFromTouchTargets(),代码如下:

    private void removePointersFromTouchTargets(int pointerIdBits) {
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            if ((target.pointerIdBits & pointerIdBits) != 0) {
                target.pointerIdBits &= ~pointerIdBits;
                if (target.pointerIdBits == 0) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            predecessor = target;
            target = next;
        }
    }

该方法是为了清除所有与该抬起手指相关触摸对象的pointerIdBits 中保存的Id位,如果该pointerIdBits == 0,则会将该触摸对象从触摸对象链表中清除掉。
  收尾的最后会将变量handled的结果返回,就是事件的处理结果。

上面是将VIewGroup类的dispatchTouchEvent()方法从前到后讲了一下,不过重要的dispatchTransformedTouchEvent()还没分析,现在开始。

重要的dispatchTransformedTouchEvent()

  dispatchTransformedTouchEvent()方法的代码很重要,包括后面的其他的事件类型,也是通过该方法处理,如下

    /**
     * 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) {
            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;
    }

  该方法参数event是派发的事件;参数cancel是代表取消状态,如果该参数为true,会将事件类型设置为ACTION_CANCEL;参数child是需要处理该事件的控件;参数desiredPointerIdBits是child想要处理的事件的id的bit位。
  该方法主要处理如下:
  一、事件如果是ACTION_CANCEL或参数cancel 是true,设置事件类型为MotionEvent.ACTION_CANCEL,进行处理,处理完毕将原来的事件类型还原,然后返回结果。通过代码可以看到如果child==true,会调用super.dispatchTouchEvent(event),即执行父类(这里指View类)的dispatchTouchEvent()方法,否则调用child.dispatchTouchEvent(event)。
  二、判断事件是否分解,如果需要分解的话,调用MotionEvent 的split()方法进行分解出来新事件接着进行传递;如果事件不需要分解,并且child == null,会执行super.dispatchTouchEvent(event),即执行父类(这里指View类)的dispatchTouchEvent()方法,并将执行结果返回;如果事件不需要分解,并且child不为null,并且child.hasIdentityMatrix(),需要将event进行简单转化,然后派发到child,执行child.dispatchTouchEvent(event),恢复event,返回执行结果handled;如果事件不需要分解,但是也不符合上面说的那两种情况,需要调用MotionEvent.obtain(event)获取一个新的事件transformedEvent,并且复制event的属性信息,后面再将该新事件进行属性转化再派发。
  三、对新生成的事件进行处理,如果child等于null,调用super.dispatchTouchEvent(transformedEvent);如果child不等于null,则先进行转化,然后会调用child.dispatchTouchEvent(transformedEvent)进行处理。在最后返回结果之前,会将新生成的transformedEvent进行回收,最后返回结果。
  其中可以看到,什么情况下会产生ACTION_CANCEL事件?在dispatchTransformedTouchEvent的参数cancel的值为true的时候,会将当前事件类型设置成ACTION_CANCEL,进行派发。什么情况下,设置参数cancel为true呢,前面派发触摸对象处理事件里面已经阐释
  参数child如果为null,就是没找到子View,这个时候,是需要该ViewGroup本身进行处理(调用父类View的方法dispatchTouchEvent()进行处理,代码为super.dispatchTouchEvent(event));如果参数child不为null,那么调用child.dispatchTouchEvent(event)进行处理。这个child如果也是ViewGroup类型,那就会嵌套执行dispatchTouchEvent()。
  dispatchTouchEvent()的复杂也就在这个嵌套执行上面,理这块的逻辑的时候不能着急,需要慢慢理清楚,碰到问题才能找到解决办法。尤其在前面讲的步骤 寻找触摸事件派发对象也涉及到调用dispatchTouchEvent(),一层一层嵌套进去,出来结果再一层一层返回,很容易乱。
  参数desiredPointerIdBits是指子View接收处理的事件的id的bit位。oldPointerIdBits 临时变量是事件中所有的id的bit位。两者进行位操作&,得到变量newPointerIdBits。如果newPointerIdBits和oldPointerIdBits 不相等,进行事件分解。这个怎么理解呢?举个例子,多点触控,A控件需要处理id=0的事件,B控件处理id=1的事件,MotionEvent 事件对象的id中包括0和1,即oldPointerIdBits=0x00000003。当事件分发到A控件的时候,desiredPointerIdBits=0x00000001。这个时候需要进行事件分解,将id=0的事件的相关属性放到一个新MotionEvent 对象中。然后将这个新事件交给A控件处理。同样,事件分发到B控件的时候,也需要事件分解,只不过desiredPointerIdBits=0x00000002。事件分解代码详细可以见https://blog.csdn.net/q1165328963/article/details/120032644
  可以看到事件交由子View处理之前需要经过转化,还要注意,在事件不需要分解的情况,子View child.hasIdentityMatrix()的情况下,转化完的事件经过子View处理之后,还需要将属性还原调用的是event.offsetLocation(-offsetX, -offsetY)。View的hasIdentityMatrix()是什么意思呢,就是有初始化矩阵。这个怎么理解?View相对于父控件有时会做复杂的变化,包括位移,旋转等,这些复杂的变化会保存在一个矩阵中。以后父控件的属性如果要改成相对子View的属性,那就可以通过这个矩阵进行转化。
  dispatchTransformedTouchEvent()方法如果找到子View,嵌套执行dispatchTouchEvent()。如果找到子View类型是View类型,则会执行View类的dispatchTouchEvent(MotionEvent event)方法,包括没有找到触摸对象,将child设置为null的时候,也会执行View类的dispatchTouchEvent(MotionEvent event)方法,进入其中看一下代码:

View类的dispatchTouchEvent(MotionEvent event)方法

    /**
     * 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 (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

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

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

  该方法的逻辑还是很清晰的
  1、事件是ACTION_DOWN的时候,调用stopNestedScroll()
  2、事件在控件ENABLED状态下,如果设置了mOnTouchListener接口,先执行接口的onTouch()方法,该接口方法返回true,则将变量result设置为true。
  3、result如果为false,执行控件的onTouchEvent()方法,该方法返回true,则将变量result设置为true。
  4、事件如果是ACTION_UP或者ACTION_CANCEL或者 ACTION_DOWN并且result如果为false的情况下,调用stopNestedScroll()方法。最后会将变量result的值返回。
  从这里可以看到,如果控件在ENABLED状态,并且设置了mOnTouchListener接口,会先调用该接口之后,才会执行onTouchEvent()方法。如果mOnTouchListener接口返回true,那么控件的onTouchEvent()方法是不会执行的。onTouchEvent()方法等到下一篇文章再讲。
  在这里总结一下ViewGroup的dispatchTouchEvent()主要内容:
  首先在非拦截非取消的转态下,在手指按下的时候去寻找子View触摸对象
  然后如果找到了触摸对象,后续的事件如ACTION_MOVE及ACTION_UP都将派发到触摸对象;如果没有找到触摸对象则会调用父类View的dispatchTouchEvent()方法执行。
  这块说起来很简单,但是实际中确实相当复杂,细节极多,因为涉及到嵌套执行和多点触控。下面是一幅流程简图,里面包含主要流程。
Android触摸事件派发(一) ViewGroup的dispatchTouchEvent()

上一篇:Android setContentView源码阅读


下一篇:Android View 绘制流程,硬核