前言
Android的事件分发机制也是老生常谈了,这篇文章并不是笼统的介绍这个机制,而是针对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
一个完整的事件应该包含ACTION_DOWN、ACTION_MOVE、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这两个属性在分发过程中的作用是什么?我们分别来说:
1、mFirstTouchTarget为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自己处理
2、mFirstTouchTarget不为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。
总结
这样我们得到几个结论:
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。