以下文章来源于BennuCTech ,作者BennuC
移动、大前端、全栈,技术无止境。
前言
Android 的事件分发机制也是老生常谈了,本文从细节入手解读一下整个机制中的几个重要部分。
Android 中 touch 事件一定是从 ACTION_DOWN 开始,所以 ACTION_DOWN 的处理至关重要,我们先来看看 ACTION_DOWN 这个事件相关的细节。
dispatchTouchEvent
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 downfinal 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是nullmFirstTouchTarget = 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,这个函数直接影响事件的拦截。我们今天就来说一说这个这个函数是如何影响事件分发的。
源码分析
我们先看看这个函数的源码
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {// We're already in this state, assume our ancestors are tooreturn;}if (disallowIntercept) {mGroupFlags |= FLAG_DISALLOW_INTERCEPT;} else {mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;}// Pass it up to our parentif (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" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。