Android事件分发机制之ACTION_DOWN

2021-12-10 14:54:06 浏览数 (1)

前言

Android的事件分发机制也是老生常谈了,这篇文章并不是笼统的介绍这个机制,而是针对ACTION_DOWN这个事件探讨相关的细节。

dispatchTouchEvent

说到Android事件分发,一定绕不开dispatchTouchEvent函数,View和ViewGroup的该函数有很大的不同。

我们来看看ViewGroup的dispatchTouchEvent函数,它的部分源码如下:

代码语言:javascript复制
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    if (onFilterTouchEventForSecurity(ev)) {
        ...
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {


            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;

                removePointersFromTouchTargets(idBitsToAssign);

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

                    ...

                    for (int i = childrenCount - 1; i >= 0; i--) {
                        ...
                        if (!child.canReceivePointerEvents()
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        ...

                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

                            ...

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

                ...
            }
        }

        // 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;
                    }
                    ...
                }
                predecessor = target;
                target = next;
            }
        }

        ...
    }

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

可以看到整个分发有几个关键因素:interceptedcanceledmFirstTouchTargetalreadyDispatchedToNewTouchTarget

intercepted、canceled比较好理解,重点来说说后面两个因素的是如何影响整个分发的。

ACTION_DOWN

一个完整的事件应该包含ACTION_DOWN、ACTION_MOVE、ACTION_UP。其中ACTION_DOWN是开始也是关键。

从上面dispatchTouchEvent源码中可以看到首先单独对ACTION_DOWN事件进行了处理,对所有child进行遍历,是从后向前遍历的,所以在处理上面的也就是最后添加的view会先得到事件

代码语言:javascript复制
for (int i = childrenCount - 1; i >= 0; i--) {

对于每个child,会先判断事件是不是发生在它的区域内,不是则不处理:

代码语言:javascript复制
if (!child.canReceivePointerEvents()
        || !isTransformedTouchPointInView(x, y, child, null)) {
    ev.setTargetAccessibilityFocus(false);
    continue;
}

如果在区域内,则继续执行,下面dispatchTransformedTouchEvent这个函数就是下发事件的,我们来看下部分源码:

代码语言:javascript复制
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {

    ...

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

有不少逻辑在里面,但是仔细观察可以发现,不论那个条件,执行的代码都比较类似,如下:

代码语言:javascript复制
if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
     ...
    handled = child.dispatchTouchEvent(event);
     ...
}

当child不为null的时候,执行child的dispatchTouchEvent;为null时执行父类的dispatchTouchEvent,即View的dispatchTouchEvent函数,这个函数里会执行onTouchEvent等。所以在ViewGroup是没有onTouchEvent等函数的代码。

由于这时child不为null,所以执行了child的dispatchTouchEvent函数.

回到之前的ACTION_DOWN流程中,根据dispatchTransformedTouchEvent返回值进行不同的处理:

返回ture

如果返回true,即有一个child消费了ACTION_DOWN事件,可以看到后续执行了addTouchTarget函数,同时将alreadyDispatchedToNewTouchTarget置为true。

代码语言:javascript复制
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    ...
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;
}

addTouchTarget函数源码如下:

代码语言:javascript复制
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;  //初始mFirstTouchTarget为null,所以这里next是null
    mFirstTouchTarget = target;
    return target;
}

关键的一点是对mFirstTouchTarget进行了赋值。所以说true的处理是为mFirstTouchTarget赋值,将alreadyDispatchedToNewTouchTarget置为true 最后的break则跳出循环,不再遍历其他child。

返回false

如果返回false,即没有任何一个child消费ACTION_DOWN事件,直接跳过if代码,这样mFirstTouchTarget为null。

mFirstTouchTarget

那么mFirstTouchTarget、alreadyDispatchedToNewTouchTarget这两个属性在分发过程中的作用是什么?我们分别来说:

1、mFirstTouchTarget为null

mFirstTouchTarget为null,进入if语句执行dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)

代码语言:javascript复制
if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

由于child是null,在dispatchTransformedTouchEvent代码中可以看到不再给任何child分发,而是调用了super.dispatchTouchEvent,即ViewGroup自己处理

这样ACTION_DOWN事件分发完了。其他事件分发时由于不再走ACTION_DOWN的处理过程,所以mFirstTouchTarget会一直为null,所以其他事件也不再向下分发了,直接ViewGroup自己处理

2、mFirstTouchTarget不为null

mFirstTouchTarget不为null,进入else语句中,会执行一个while循环

代码语言:javascript复制
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;
            }
            ...
        }
        predecessor = target;
        target = next;
    }
}

这时由于alreadyDispatchedToNewTouchTarget为true,所以直接给handled赋值true并不做任何处理。因为之前代码中child对ACTION_DOWN事件已经响应,所以这里的alreadyDispatchedToNewTouchTarget是为了防止重复分发ACTION_DOWN事件。

这样ACTION_DOWN事件分发完成后,分发其他事件时,alreadyDispatchedToNewTouchTarget被重新赋值false,由于不再走ACTION_DOWN的处理过程,所以alreadyDispatchedToNewTouchTarget就一直是false了,而mFirstTouchTarget会一直保持不变。在这个while循环中则会执行else语句,通过执行dispatchTransformedTouchEvent将事件直接分发给mFirstTouchTarget对应的child,即之前消费ACTION_DOWN事件的child。

总结

这样我们得到几个结论:

1、ViewGroup分发事件down的时候,会遍历自己的子view,从前面的到后面的

代码语言:javascript复制
for (int i = childrenCount - 1; i >= 0; i--) {

然后判断子view的区域是否包含事件,如果包含则进行处理。

所以同级分发时,即两个同级的view叠加在一起时,先分发给前面的view。

2、如果所有的child都不消费ACTION_DOWN事件,那么实际上child并不是收不到任何事件,而是ACTION_DOWN会分发给所有有效范围内的child,但是其他事件就不再分发了。

3、如果有一个child消费了ACTION_DOWN事件,那么后续的事件会直接分发给这个child,不再经过其他child。但是注意,在分发ACTION_DOWN事件时,排在这child前面的child还是会分发到ACTION_DOWN事件,但是也仅仅是ACTION_DOWN事件。

所以整个Touch事件分发过程中,ACTION_DOWN是至关重要的,我们通常考虑的返回值或继续分发的问题,实际上都是讨论ACTION_DOWN这个事件的,基本上ACTION_DOWN事件分发确定了,后续事件的分发就基本确定下来了。但是注意在后续的事件中,依然需要判断InterceptTouchEvent。

0 人点赞