细说 Android 的事件分发机制 | 开发者说·DTalk
The following article is from BennuCTech Author BennuC
前言
Android 的事件分发机制也是老生常谈了,本文从细节入手解读一下整个机制中的几个重要部分。
Android 中 touch 事件一定是从 ACTION_DOWN 开始,所以 ACTION_DOWN 的处理至关重要,我们先来看看 ACTION_DOWN 这个事件相关的细节。
dispatchTouchEvent
@Overridepublic 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;}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); ...}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 为 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 自己处理
当 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; }}ACTION_DOWN 总结
这样我们得到几个结论:
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。
拦截机制
我们知道在事件分发过程中是存在一个拦截机制的
onInterceptTouchEvent当它返回 true 则不向下分发事件,否则向下分发。
但是在这个过程中,还有一个参与者: requestDisallowInterceptTouchEvent,这个函数直接影响事件的拦截。我们今天就来说一说这个这个函数是如何影响事件分发的。
源码分析
我们先看看这个函数的源码
@Overridepublic 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); }}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;}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 的返回值设置就无效了,因为根本就不再执行这个函数了。
拦截总结
通过上面的分析可以知道 requestDisallowInterceptTouchEvent 会让父 view 放开拦截,并且是向上层层生效的。同时我们也可以通过一些逻辑控制,使 requestDisallowInterceptTouchEvent 只作用在部分情况下。
长按右侧二维码
查看更多开发者精彩分享
"开发者说·DTalk" 面向