这是 Android 触摸事件系列文章的第一篇.
大领导安排任务会经历一个 "递" 的过程: 大领导先把任务告诉小领导, 小领导再把任务告诉小明. 也可能会经历一个 "归" 的过程: 小明告诉小领导做不了, 小领导告诉大领导任务完不成. 然后, 就没有然后了....
Android 触摸事件和领导安排任务的过程很相似, 也会经历 "递" 和 "归". 这一篇会试着阅读源码来分析 ACTION_DOWN 事件的这个递归过程.
(ps: 下文中的 粗斜体字 表示引导源码阅读的内心戏)
分发触摸事件起点
写一个包含 ViewGroup,View,Activity 的 demo, 并在所有和 touch 有关的方法中打 log. 当触摸事件发生时, Activity.dispatchTouchEvent() 总是第一个被调用, 就以这个方法为切入点:
- public class Activity{
- private Windows mWindow;
- // 分发触摸事件
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- onUserInteraction();
- }
- // 让 PhoneWindow 帮忙分发触摸事件
- if (getWindow().superDispatchTouchEvent(ev)) {
- return true;
- }
- return onTouchEvent(ev);
- }
- // 获得 PhoneWindow 对象
- public Windows getWindow() {
- return mWindow;
- }
- // 参数太长, 省略了
- final void attach(...) {
- ...
- // 构造 PhoneWindow
- mWindow = new PhoneWindow(this, Windows, activityConfigCallback);
- ...
- }
- }
Activity 将事件传递给 PhoneWindow:
- public class PhoneWindow extends Windows implements MenuBuilder.Callback {
- // This is the top-level view of the Windows, containing the Windows decor.
- // 一个窗口的顶层视图
- private DecorView mDecor;
- @Override
- public boolean superDispatchTouchEvent(MotionEvent event) {
- // 将触摸事件交给 DecorView 分发
- return mDecor.superDispatchTouchEvent(event);
- }
- }
- //DecorView 继承自 ViewGroup
- public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks{
- public boolean superDispatchTouchEvent(MotionEvent event) {
- // 事件最终由 ViewGroup.dispatchTouchEvent() 分发触摸事件
- return super.dispatchTouchEvent(event);
- }
- }
PhoneWindow 继续将事件传递给 DecorView, 最终调用了
ViewGroup.dispatchTouchEvent()
至此可以做一个简单的总结: 触摸事件的传递从 Activity 开始, 经过 PhoneWindow, 到达顶层视图 DecorView.DecorView 调用了
- ViewGroup.dispatchTouchEvent()
- .
触摸事件之 "递"
在分析 View 绘制时, 也遇到过 "dispatchXXX" 函数
ViewGroup.dispatchDraw()
, 它用于遍历孩子并触发它们自己绘制自己. 那
dispatchTouchEvent()
会不会也遍历孩子并将触摸事件传递给它们? 带着这个疑问来看下源码:
- public abstract class ViewGroup extends View implements ViewParent, ViewManager {
- @Override
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if (!canceled && !intercepted) {
- ...
- // 遍历孩子
- for (int i = childrenCount - 1; i>= 0; i--) {
- // 按照索引顺序或者自定义绘制顺序遍历孩子
- final int childIndex = customOrder
- ? getChildDrawingOrder(childrenCount, i) : I;
- final View child = (preorderedList == null)
- ? children[childIndex] : preorderedList.get(childIndex);
- ...
- // 如果孩子不在触摸区域则直接跳过
- if (!canViewReceivePointerEvents(child)
- || !isTransformedTouchPointInView(x, y, child, null)) {
- ev.setTargetAccessibilityFocus(false);
- continue;
- }
- ...
- // 转换触摸坐标并分发给孩子 (child 参数不为 null)
- if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
- // 这里的代码也很关键, 先埋伏笔 1
- }
- ...
- }
- }
- if (mFirstTouchTarget == null) {
- // 这里的代码也很关键, 先埋伏笔 2
- } else {
- // 这里的代码也很关键, 先埋伏笔 3
- }
- }
- private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
- View child, int desiredPointerIdBits) {
- final boolean handled;
- ...
- // Perform any necessary transformations and dispatch.
- // 进行必要的坐标转换然后分发触摸事件
- if (child == null) {
- // 这里的代码也很关键, 先埋伏笔 3
- } else {
- // 将 ViewGroup 坐标系转换为它孩子的坐标系 (坐标原点从 ViewGroup 左上角移动到孩子左上角)
- 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);
- }
- ...
- return handled;
- }
- }
果然没猜错! 父控件在 ViewGroup.dispatchTouchEvent() 中会遍历孩子并将触摸事件分发给被点中的子控件, 如果子控件还有孩子, 触摸事件的 "递" 将不断持续, 直到叶子结点. 最终 View 类型的叶子结点调用的是 View.dispatchTouchEvent():
- public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
- public boolean dispatchTouchEvent(MotionEvent event) {
- ...
- if (onFilterTouchEventForSecurity(event)) {
- if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
- result = true;
- }
- //noinspection SimplifiableIfStatement
- //1. 通知触摸监听器 OnTouchListener
- ListenerInfo li = mListenerInfo;
- if (li != null && li.mOnTouchListener != null
- && (mViewFlags & ENABLED_MASK) == ENABLED
- && li.mOnTouchListener.onTouch(this, event)) {
- result = true;
- }
- //2. 调用 onTouchEvent()
- // 只有当 OnTouchListener.onTouch() 返回 false 时, onTouchEvent() 才有机会被调用
- if (!result && onTouchEvent(event)) {
- result = true;
- }
- }
- ...
- // 返回值就是 onTouch() 或者 onTouchEvent() 的返回值
- return result;
- }
- ListenerInfo mListenerInfo;
- // 监听器容器类
- static class ListenerInfo {
- ...
- private OnTouchListener mOnTouchListener;
- ...
- }
- // 设置触摸监听器
- public void setOnTouchListener(OnTouchListener l) {
- // 将监听器存储在监听器容器中
- getListenerInfo().mOnTouchListener = l;
- }
- // 获得监听器管理实例
- ListenerInfo getListenerInfo() {
- if (mListenerInfo != null) {
- return mListenerInfo;
- }
- mListenerInfo = new ListenerInfo();
- return mListenerInfo;
- }
- }
View.dispatchTouchEvent()
是传递触摸事件的终点, 消费触摸事件的起点.
消费触摸事件的标志是调用
OnTouchListener.onTouch()
或
View.onTouchEvent()
, 前者优先级高于后者. 只有当没有设置 OnTouchListener 或者 onTouch() 返回 false 时,
View.onTouchEvent()
才会被调用.
读到这里, 画一张图总结一下触摸事件之 "递":
图中 ViewGroup 层后面的 N 表示在 Activity 层和 View 层之间可能有多个 ViewGroup 层.
图中自上而下一共有三类层次, 触摸事件会从最高层次开始沿着箭头往下层传递.
为简单起见, 图中省略了另一种触摸事件的处理方式:
OnTouchListener.onTouch
图示触摸事件的传递只是众多传递场景中的一种: 被点击的 View 嵌套在 ViewGroup 中, ViewGroup 在 Activity 中.
触摸事件之 "归"
触摸事件之所以在 "递" 之后还会发生 "归" 是因为: 分发触摸事件的函数还没有执行完. 沿着刚才调用链相反的方向重新看一遍源码:
- public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
- /**
- * Implement this method to handle touch screen motion events.
- *
- * @param event The motion event.
- * @return True if the event was handled, false otherwise.
- * 返回 true 表示触摸事件被消费, 否则表示未被消费
- */
- public boolean onTouchEvent(MotionEvent event) {
- ...
- if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
- // 省略了对不同触摸事件的默认处理
- ...
- // 只要控件是可点击的, 就表示触摸事件已被消费
- return true;
- }
- // 若控件不可点击则不消费触摸事件
- return false;
- }
- }
View.dispatchTouchEvent() 调用了 View.onTouchEvent() 后并没有执行完. View.onTouchEvent() 的返回值会影响 View.dispatchTouchEvent() 的返回值:
- public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
- public boolean dispatchTouchEvent(MotionEvent event) {
- ...
- boolean result = false;
- ...
- if (onFilterTouchEventForSecurity(event)) {
- if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
- result = true;
- }
- //noinspection SimplifiableIfStatement
- ListenerInfo li = mListenerInfo;
- if (li != null && li.mOnTouchListener != null
- && (mViewFlags & ENABLED_MASK) == ENABLED
- && li.mOnTouchListener.onTouch(this, event)) {
- result = true;
- }
- if (!result && onTouchEvent(event)) {
- result = true;
- }
- }
- // 返回当前 View 是否消费触摸事件的布尔值
- return result;
- }
同样的, ViewGroup.dispatchTouchEvent() 调用了 View.dispatchTouchEvent() 后也没有执行完, View.dispatchTouchEvent() 的返回值会影响 ViewGroup.dispatchTouchEvent() 的返回值:
- public abstract class ViewGroup extends View implements ViewParent, ViewManager {
- // 触摸链头结点
- private TouchTarget mFirstTouchTarget;
- ...
- @Override
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if (!canceled && !intercepted) {
- ...
- // 遍历孩子
- for (int i = childrenCount - 1; i>= 0; i--) {
- ...
- // 转换触摸坐标并分发给孩子 (child 参数不为 null)
- if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
- ...
- // 有孩子愿意消费触摸事件, 将其插入 "触摸链"
- newTouchTarget = addTouchTarget(child, idBitsToAssign);
- // 表示已经将触摸事件分发给新的触摸目标
- alreadyDispatchedToNewTouchTarget = true;
- break;
- }
- ...
- }
- }
- if (mFirstTouchTarget == null) {
- // 如果没有孩子愿意消费触摸事件, 则自己消费 (child 参数为 null)
- handled = dispatchTransformedTouchEvent(ev, canceled, null,
- TouchTarget.ALL_POINTER_IDS);
- } else {
- TouchTarget predecessor = null;
- TouchTarget target = mFirstTouchTarget;
- // 遍历触摸链分发触摸事件给所有想接收的孩子
- while (target != null) {
- final TouchTarget next = target.next;
- if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
- // 如果已经将触摸事件分发给新的触摸目标, 则返回 true
- handled = true;
- } else {
- // 这里的代码很重要, 继续埋伏笔, 待下一篇分析.
- }
- predecessor = target;
- target = next;
- }
- }
- ...
- // 返回触摸事件是否被孩子或者自己消费的布尔值
- return handled;
- }
- private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
- View child, int desiredPointerIdBits) {
- final boolean handled;
- ...
- // Perform any necessary transformations and dispatch.
- // 进行必要的坐标转换然后分发触摸事件
- if (child == null) {
- //ViewGroup 孩子都不愿意消费触摸事件 则其将自己当成 View 处理 (调用 View.dispatchTouchEvent())
- handled = super.dispatchTouchEvent(transformedEvent);
- } else {
- // 将触摸事件分发给孩子
- }
- ...
- return handled;
- }
- /**
- * Adds a touch target for specified child to the beginning of the list.
- * Assumes the target child is not already present.
- * 添加 View 到触摸链头部
- * @param child View
- * @param pointerIdBits
- * @return 新触摸目标
- */
- private TouchTarget addTouchTarget(View child, int pointerIdBits) {
- TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
- target.next = mFirstTouchTarget;
- mFirstTouchTarget = target;
- return target;
- }
- }
上面这段代码补全了上一节中买下的伏笔. 原来当孩子愿意消费触摸事件时, ViewGroup 会将其接入 "触摸链", 如果触摸链中没有结点则表示没有孩子愿意消费事件, 此时 ViewGroup 只能自己消费事件. ViewGroup 是 View 的子类, 他们消费触摸事件的方式一摸一样, 都是通过
View.dispatchTouchEvent()
调用
View.onTouchEvent()
或
- OnTouchListener.onTouch()
- .
沿着回溯链, 再向上 "归" 一步:
- public class Activity {
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- onUserInteraction();
- }
- if (getWindow().superDispatchTouchEvent(ev)) {
- // 如果布局中有控件愿意消费触摸事件, 则返回 true,onTouchEvent() 不会被调用
- return true;
- }
- return onTouchEvent(ev);
- }
- }
View,ViewGroup 和 Activity, 虽然它们分发触摸事件的逻辑不太一样, 但基本结构都和上面这段代码神似, 用伪代码可以写成:
- //"递"
- if(分发事件给孩子){
如果孩子消费了事件 直接返回 (将触摸事件被消费这一事实往上传递)
}
//"归"
如果孩子没有消费事件, 则自己消费事件
"分发事件给孩子" 这个函数的调用表示 "递", 即将触摸事件传递给下层."分发事件给孩子" 这个函数的返回表示 "归", 即将触摸事件的消费结果回溯给上层, 以便上层采取进一步的行动.
同样的套路, 用图片总结下触摸事件之 "归":
这张图是对图 1 描述场景的补全. 图中黑色的线表示触摸事件的传递路径, 灰色的线表示触摸事件回溯的路径.
因为
View.onTouchEvent()
返回 true, 表示消费触摸事件, 所以
ViewGroup.onTouchEvent()
以及
Activity.onTouchEvent()
都不会被调用.
这张图是对图 1 描述场景的扩展. 图中黑色的线表示触摸事件的传递路径, 灰色的线表示触摸事件回溯的路径.
图示所对应的场景是: 被点击的 View 不消费触摸事件, 而 ViewGroup 在 onTouchEvent() 中返回 true 自己消费触摸事件.
这张图是对图 1 描述场景的扩展. 图中黑色的线表示触摸事件的传递路径, 灰色的线表示触摸事件回溯的路径.
图示所对应的场景是: 被点击的 View 和 ViewGroup 都不消费触摸事件, 最后只能由 Activity 来消费触摸事件.
总结
Activity 接收到触摸事件后, 会传递给 PhoneWindow, 再传递给 DecorView, 由 DecorView 调用
ViewGroup.dispatchTouchEvent()
自顶向下分发 ACTION_DOWN 触摸事件.
ACTION_DOWN 事件通过
ViewGroup.dispatchTouchEvent()
从 DecorView 经过若干个 ViewGroup 层层传递下去, 最终到达 View.
每个层次都可以通过在 onTouchEvent() 或
OnTouchListener.onTouch()
返回 true, 来告诉自己的父控件触摸事件被消费. 只有当下层控件不消费触摸事件时, 其父控件才有机会自己消费.
触摸事件的传递是从根视图自顶向下 "递" 的过程, 触摸事件的消费是自下而上 "归" 的过程.
读到这里可能对于触摸事件还充满诸多疑问:
ViewGroup 层是否有办法拦截触摸事件?
ACTION_DOWN 只是触摸序列的起点, 后序的 ACTION_MOVE,ACTION_UP,ACTION_CANCEL 是如何传递的?
这些问题会在下一篇继续分析.
来源: https://juejin.im/post/5c78fb9be51d457143523547