02 Jan 2016
Android view touch 事件分发机制之我的理解
2015 年 9 月份的时候把 view 的事件分发机制研究得觉得自己掌握得很好了,没有记录到笔记上,结果最近要修改第三方自定义控件时,又觉得有点迷糊了,索性重新再学习了一遍,画了一张图来加深自己的理解:
其它一些要点 (下文中 View 和 ViewGroup 是分开描述的,View 就是 View,并不指代 ViewGroup):
- View/ViewGroup 的 dispatchTouchEvent 在
ACTION_DOWN
时返回 true 表示消费了 touch 事件,返回 false 表示不消费 touch 事件。是否消费 touch 事件只取决于ACTION_DOWN
时返回值。 - Android view 的事件分发机制,是一个由顶层 Activity 开始,中间经历多个 ViewGroup,传递到最底层的 View (或 ViewGroup),再由底层传回顶层的过程。
- 当传递到 View 时,说明已经达到一条事件传递路径的最底层,然后开始回溯。View 只可能处于一条传递路径的最底层,且最多只有一个。它不可能处于中间,因于它没有子 View/ViewGroup。处于中间的必须都是 ViewGroup,可能有多个。
- 当执行到 onTouchEvent 时,说明事件处于回溯的过程中,事件由底层从顶层传递的过程中,不会再往底层分发。
- Activity/ViewGroup/View 的 dispatchTouchEvent 方法功能各不相同。只有 ViewGroup 有 onInterceptTouchEvent 和遍历子 View/ViewGroup 的方法;ViewGroup 在拦截 touch 事件时或者子 View/ViewGroup 都没有消费 touch 事件 (即它们的 dispatchTouchEvent 都返回了 false) 的情况下,就会把自己当成一个普通的 View,调用父类 View 的 dispatchTouchEvent;View 的 dispatchTouchEvent 功能很简单,如果有 touch listener 的话,优先执行 listener.onTouch,如果返回 false,则再执行 onTouchEvent;Activity 没有 touch listener。
- 一次完整的 touch 事件,开始于
ACTION_DOWN
,终止于ACTION_UP
。只有/只要(除非被上层 ViewGroup 拦截) 消费了ACTION_DOWN
事件,才/就 会收到后续事件,否则不会收到除了ACTION_DOWN
外的其它事件。 - ViewGroup 的遍历子 View/ViewGroup 的行为在一次完整的 touch 事件过程中,只会执行一次,就是当事件为
ACTION_DOWN
时。事件ACTION_DWON
是一个完整 touch 事件过程的开始,此时,需要通过遍历来定位一条路径,当路径确认之后,就不再需要遍历,后续的事件就沿着这条确定的路径传递。 - ViewGroup 的 onInterceptTouchEvent,在传递的路径确定以后 (即之后传递的事件为
ACTION_DWON
的后续事件),一般情况下,这个方法在每次事件到来时都会调用,但有一个例外,当这个 ViewGroup 处于传递路径的最底层时,此时它就相当于一个普通的消费了 touch 事件的普通 View,因此会直接跳过 onInterceptTouchEvent,直接执行父类 View.dispatchTouchEvent (listener.onTouch 或者 onTouchEvent)。 - 在传递路径确定以后,当路径中的 ViewGroup 一旦决定拦截目标子 View/ViewGroup 的某个事件后 (如 ScrollView 不会拦截子 View/ViewGroup 的
ACTION_DOWN
事件,但会拦截子 View/ViewGroup 的ACTION_MOVE
事件),就会给目标子 View/ViewGroup 发送ACTION_CANCEL
的事件,并把目标子 View/ViewGroup 重置为 null,等同于此 ViewGroup 成为新的传递路径的最底层,因此,正如 8 所说,后续事件到来时,将不会再进入 onInterceptTouchEvent,而是直接调用父类 View.dispatchTouchEvent。
ViewGroup 在 onInterceptTouchEvent 返回 true 后将目标子 View/ViewGroup 置 null 的代码:
Android ViewGroup 早期源码 (不知哪个版本),摘自郭霖博客:Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
// 发送 ACTION_CANCEL 事件给 mMotionTarget
if (!target.dispatchTouchEvent(ev)) {
}
// 将 mMotionTarget 置 null,从而让自己成为传递路径的最底层,截断事件继续往下传递
mMotionTarget = null;
return true;
}
Android ViewGroup 最新源码 (API 23)
// 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;
// 如果 intercepted 为 true,给 target 发送 ACTION_CANCEL 事件
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
// 如果 intercepted 为 true,将 target 置 null (next 值此时一般为 null)
// 从而截断事件往下传递
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
参考资料:
网上这方面的文章其实不少,但真正说清楚的没几篇,有些洋洋洒洒,其实根本没有说到点子上去,有些则是误人子弟,比如说什么 “当 onTouchEvent 返回 false 时事件就会继续传递到子 View” 之类的 (为什么是错误的见上面第四条)。当然我这篇也不见得都正确,所以事先声明一下,免得误导了你。而且我这篇写得一点都不详细,只能算是写给自己看的一篇粗略笔记。
Trinea 的这篇文章我觉得是讲得最清楚的,其实算是翻译。
- Trinea 博客文章
- 英文原文 PDF
- 网友 Ocean 翻译的视频 (这个视频超赞,推荐)
郭霖也写了两篇博客讲解 View touch 事件传递机制,但我觉得对于初学者来说不是很容易理解,不过源码分析那部分还是不错的。
另外最近买了任玉刚的 《Android 开发艺术探索》,书中这部分内容写得也很详细,推荐。
Baurine