详细解读Android中的事件分发机制

详细解读Android中的事件分发机制_第1张图片

目录

  • 前言
  • dispatchTouchEvent
  • ACTION_DOWN事件
    • 返回ture的情况
    • 返回false的情况
  • mFirstTouchTarget
    • target为null的情况
    • target不为null的情况
  • 拦截机制
    • 源码分析
    • 扩展思考
  • 总结

前言

Android的事件分发机制也是老生常谈了,本文从细节入手解读一下整个机制中的几个重要部分。

Android中touch事件一定是从ACTION_DOWN开始,所以ACTION_DOWN的处理至关重要,我们先来看看ACTION_DOWN这个事件相关的细节。

dispatchTouchEvent

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

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

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

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

  • intercepted
  • canceled
  • mFirstTouchTarget
  • alreadyDispatchedToNewTouchTarget

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

ACTION_DOWN事件

一个完整的事件应该包含

  1. ACTION_DOWN
  2. ACTION_MOVE
  3. ACTION_UP

其中ACTION_DOWN是开始也是关键。

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

for (int i = childrenCount - 1; i >= 0; i--) {

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

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

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

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

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

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。

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    ...
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;
}

addTouchTarget函数源码如下:

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这两个属性在分发过程中的作用是什么?我们分别来说:

target为null的情况

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

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自己处理

target不为null的情况

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

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。

拦截机制

我们知道在事件分发过程中是存在一个拦截机制的:onInterceptTouchEvent

当它返回true则不向下分发事件,否则向下分发。

但是在这个过程中,还有一个参与者:requestDisallowInterceptTouchEvent,这个函数直接影响事件的拦截。我们今天就来说一说这个这个函数是如何影响事件分发的。

源码分析

我们先看看这个函数的源码

@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too
        return;
    }

    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // Pass it up to our parent
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}

可以看到它改变了一个开关FLAG_DISALLOW_INTERCEPT,同时调用其parent的函数。

那么这个开关有什么用?

在ViewGroup的dispatchTouchEvent函数开头有这样一段代码:

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

先来看判断逻辑,当是down事件或者mFirstTouchTarget不为空,则进入一个代码段;否者拦截设置为true。

我们知道down事件分发过程中,如果有子view消费事件,则赋值给mFirstTouchTarget,后续事件会直接分发给mFirstTouchTarget

这里也可以看出,如果有子View消费了down事件,即mFirstTouchTarget不为空,所以后续事件还会检查拦截。

所以上面就可以理解了,如果down事件中没有子view消费事件,那么后续事件的拦截都为true。所以后续事件不会再遍历子View。

下面再看if代码段

一开始就使用了FLAG_DISALLOW_INTERCEPT开关,即disallowIntercept

  • disallowIntercept为true,则不拦截
  • 否者判断onInterceptTouchEvent

所以简单来说requestDisallowInterceptTouchEvent设置为true可以跳过onInterceptTouchEvent,不拦截事件。

而且因为requestDisallowInterceptTouchEvent又调用了parent的函数,所以所有层次的父view都不再拦截。

所以requestDisallowInterceptTouchEvent 的功能是让这个view及上面的所有父view都放开拦截,即使onInterceptTouchEvent为true。

所以我们一般如下使用

view.getParent().requestDisallowInterceptTouchEvent(true);

这样view的所有层次的父view都不会拦截事件了。

扩展思考

下面让我们再深入想想。上面这种的情况是在touch事件发生前设置onInterceptTouchEvent,也是我们一般的用法。但是如果事件发生过程中调用这个函数呢?

比如在view的onTouch的某个事件中使用

getParent().requestDisallowInterceptTouchEvent(true)

当事件开始分发时,down事件进入父view的dispatchTouchEvent时,这是子view还未得到事件,所以没有设置requestDisallowInterceptTouchEvent

这时如果父view的onInterceptTouchEvent返回true,即拦截的话,事件则不会分发给子view了,所以requestDisallowInterceptTouchEvent永远不会执行,子view则无法得到事件。

但是如果父view的onInterceptTouchEvent返回false,即不拦截的话,事件就可以分发到子view,requestDisallowInterceptTouchEvent执行,之后的事件都会跳过父view的onInterceptTouchEvent的判断

例如父view的onInterceptTouchEvent代码如下

   public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                return false;
            case MotionEvent.ACTION_MOVE:  
                    return true;
            case MotionEvent.ACTION_UP:
                return true;
            default:
                break;
        }
        return false;   
    }

down事件不进行拦截,但是拦截了move和up事件。

如果子view的onTouch的down事件中使用

getParent().requestDisallowInterceptTouchEvent(true)

这样down事件分发到了子view,执行了requestDisallowInterceptTouchEvent,同时返回了true。随后move或up事件分发到父view时,因为被设置了FLAG_DISALLOW_INTERCEPT标签,所以就会跳过onInterceptTouchEvent。

所以onInterceptTouchEvent中move和up的返回值设置就无效了,因为根本就不再执行这个函数了。

总结

通过上面的我们得到几个结论:

  1. ViewGroup分发事件down的时候,会遍历自己的子view,从前面的到后面的
    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。

关于拦截通过上面的分析可以知道requestDisallowInterceptTouchEvent会让父view放开拦截,并且是向上层层生效的。同时我们也可以通过一些逻辑控制,使requestDisallowInterceptTouchEvent只作用在部分情况下。

那么你们有没有考虑一个问题,Android中的事件又是从哪来的,怎么分发到Activity的呢?关于这个可以看我的另外一篇文章《【Android开发】事件是如何分发到Activity的?》

你可能感兴趣的:(android开发,Android进阶之路,android,事件分发)