终究没有人在意一家民营企业的生死

去泰国看了一场“成人秀”,画面尴尬到让人窒息.....

网友建议:远离举报者李X夫!

【少儿禁】马建《亮出你的舌苔或空空荡荡》

10部适合女性看的唯美情色电影

生成图片,分享到微信朋友圈

自由微信安卓APP发布,立即下载! | 提交文章网址
查看原文

细说 Android 的事件分发机制 | 开发者说·DTalk

The following article is from BennuCTech Author BennuC

本文原作者: BennuC,原文发布于: BennuCTech


前言



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


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



dispatchTouchEvent



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

我们来看看 ViewGroup 的 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;}


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

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。


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


可以看到它改变了一个开关 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 的返回值设置就无效了,因为根本就不再执行这个函数了。



拦截总结



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





长按右侧二维码

查看更多开发者精彩分享




"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。




 点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk" 



为了讲清楚Android触摸事件,我“拆了部手机”
分享一个困惑已久的问题:手指滑出View范围会怎样?
细说Android的事件分发机制
探索 BottomSheet 的背后原理
CoordinatorLayout驾轻就熟,不怕UI任意需求

文章有问题?点此查看未经处理的缓存