我们把 Android 的触摸事件流程想象成一个快递配送系统,力求通俗易懂地拆解它!
核心目标: 把用户的手指触摸(点击、滑动、长按等)这个“包裹”,准确无误地送到能“签收”(处理)它的“收件人”(View)手中。
主要角色:
- Activity: 整个应用的“总部”。它最先知道有快递(触摸事件)来了。
- Window: Activity 的“前台窗口”。负责把事件初步转交给合适的“区域”。
- DecorView: 窗口的“根视图容器”(通常是
FrameLayout),包含了标题栏和setContentView()设置的整个布局。它是视图树的“顶端配送中心”。 - ViewGroup: 像
LinearLayout,RelativeLayout,FrameLayout等可以包含其他 View 或 ViewGroup 的“区域配送中心”或“配送站”。负责把事件分发给自己的“下属”(子 View)。 - View: 像
Button,TextView,ImageView等不能再包含其他 View 的“最终收件人”。它们决定是否签收(消费)这个包裹(事件)。
触摸事件包裹:
- 每个包裹都贴有“事件类型”标签:
ACTION_DOWN(手指按下)、ACTION_MOVE(手指移动)、ACTION_UP(手指抬起)、ACTION_CANCEL(事件被取消)等。 - 一个完整的“手势”(比如一次点击)通常包含一个
ACTION_DOWN,零个或多个ACTION_MOVE,最后以一个ACTION_UP或ACTION_CANCEL结束。ACTION_DOWN是关键起点,它决定了后续事件会送给谁处理。
核心配送流程(分发逻辑):
想象一个树状结构(视图树),根是 DecorView,下面是层层嵌套的 ViewGroup,叶子节点是 View。
-
总部接收 (Activity -> Window -> DecorView):
- 当用户触摸屏幕,系统内核生成一个触摸事件。
- 事件首先到达 Activity 的
dispatchTouchEvent()方法。Activity 说:“哦,有包裹来了!” - Activity 把包裹交给它的 Window (
mWindow.dispatchTouchEvent())。 - Window 再把包裹交给 DecorView (
mDecor.superDispatchTouchEvent())。 - 至此,包裹正式进入“视图配送系统”。
-
自上而下分发 (递归进入 ViewGroup):
-
包裹现在在
DecorView(根 ViewGroup)手上。 -
DecorView的dispatchTouchEvent()方法被调用。 -
关键决策点:
ViewGroup在dispatchTouchEvent()中做两件重要事情:-
a) 检查拦截 (onInterceptTouchEvent):
ViewGroup问自己:“这个包裹是给我的,还是要转发给下属的?”(默认返回false表示不拦截)。- 如果
onInterceptTouchEvent()返回true,表示:“这个包裹我截下了!后续事件(MOVE,UP)都直接找我,不再问下属了!”(拦截)。 - 拦截通常发生在用户开始滑动 (
MOVE事件) 时,比如ScrollView发现用户意图是滑动而不是点击内部按钮。
-
b) 向下分发给子 View (如果没拦截):
-
如果不拦截(或还没到拦截的时机),
ViewGroup需要找出“哪个子 View 在触摸点下面并且愿意接收包裹”。 -
ViewGroup按照子 View 绘制的倒序(后绘制的在上面)遍历所有子 View:- 检查触摸点坐标是否在子 View 的区域内 (
frame.contains(x, y))。 - 检查子 View 是否可见 (
VISIBLE) 且没有动画正在执行。 - 如果满足条件,调用该子 View 的
dispatchTouchEvent(),把包裹交给它。
- 检查触摸点坐标是否在子 View 的区域内 (
-
如果这个子 View 也是一个
ViewGroup,那么它重复步骤 2:检查自己是否拦截,再向下分发。 -
如果这个子 View 是一个普通的
View,那么进入步骤 3。 -
如果找到了一个愿意接收
ACTION_DOWN的子 View,ViewGroup会记住它(存储在一个TouchTarget链表中),后续事件 (MOVE,UP) 会直接尝试分发给它,不再遍历所有子 View(除非事件被取消或新的ACTION_DOWN发生),这提高了效率。 -
如果没有任何子 View 愿意消费
ACTION_DOWN事件,那么ViewGroup会自己尝试处理(进入步骤 3)。
-
-
-
-
最终签收处理 (View):
-
包裹终于到达一个叶子节点 View(如
Button)的dispatchTouchEvent()方法。 -
该 View 会按顺序尝试“签收”包裹:
-
a) 优先给 OnTouchListener: 如果给这个 View 设置了
setOnTouchListener(),并且onTouch()方法返回了true(表示“我签收了,不用再处理了”),那么流程到此结束,View自己的onTouchEvent()不会被调用。 -
b) 自己处理 (onTouchEvent): 如果没有
OnTouchListener或onTouch()返回了false(表示“我没签收,或者签收了但还想让内部再处理一下”),那么就会调用 View 自己的onTouchEvent()方法。-
在
onTouchEvent()里,View 根据事件类型做具体响应:- 对于
Button:ACTION_DOWN改变背景色,ACTION_UP触发点击事件 (OnClickListener.onClick())。 - 对于
TextView:可能只是改变按下状态或不做响应。
- 对于
-
如果
onTouchEvent()返回true,表示“这个包裹我签收并处理了!”。 -
如果返回
false,表示“这个包裹我不要,退回去!”。
-
-
-
-
自下而上回溯 (消费与否):
-
不管是在
ViewGroup的dispatchTouchEvent()中(自己尝试处理或拦截后处理),还是在View的dispatchTouchEvent()/onTouchEvent()中,只要有一个环节返回true,就表示“包裹已签收”,事件分发流程就此结束,结果返回true。 -
如果某个
ViewGroup分发给所有子 View 后,没有任何子 View 消费事件(所有子 View 的dispatchTouchEvent()都返回false),并且ViewGroup自己的onTouchEvent()也返回false(表示它自己也不要),那么这个ViewGroup的dispatchTouchEvent()会返回false。 -
这个
false会沿着视图树向上传递(回溯):- 父
ViewGroup收到false,知道下属没人要,就会尝试用自己的onTouchEvent()处理。 - 如果父
ViewGroup的onTouchEvent()也返回false,它也会向上返回false。
- 父
-
最终,如果回溯到
DecorView甚至Activity,它们自己的onTouchEvent()都没有消费事件(返回false),那么Activity的dispatchTouchEvent()最终返回false,系统就知道这次触摸事件没有被任何应用内的 View 消费。
-
核心方法回调总结:
| 方法名 | 所属类 | 调用时机/作用 | 返回值意义 |
|---|---|---|---|
dispatchTouchEvent() | Activity | 最先接收事件,交给 Window。 | 最终表示事件是否被消费。 |
dispatchTouchEvent() | ViewGroup | 核心枢纽! 1. 调用 onInterceptTouchEvent() 决定是否拦截。2. 若不拦截,遍历子 View 调用其 dispatchTouchEvent()。3. 若子 View 都不消费或自己拦截了,调用自身 onTouchEvent()。 | 返回 true:事件被消费(自己或子 View)。返回 false:事件未被消费。 |
onInterceptTouchEvent() | ViewGroup | 仅 ViewGroup 有! 在 dispatchTouchEvent() 内部调用,询问是否要拦截事件,停止向下分发,后续事件自己处理。 | 返回 true:拦截,事件流后续事件直接给此 ViewGroup 的 onTouchEvent()。返回 false:不拦截(默认)。 |
dispatchTouchEvent() | View | 1. 若有 OnTouchListener 且其 onTouch() 返回 true,则消费。2. 否则调用自身 onTouchEvent()。 | 返回 true:事件被消费(Listener 或 onTouchEvent)。返回 false:事件未被消费。 |
onTouchEvent() | View/ViewGroup | 最终处理者! 被 dispatchTouchEvent() 调用(当没有 Listener 消费或 ViewGroup 拦截/自己处理时)。实现具体的触摸响应逻辑(如点击、滑动)。 | 返回 true:事件在此被消费。返回 false:事件未被消费,向上回溯。 |
onTouch() | OnTouchListener | 通过 View.setOnTouchListener() 设置。在 View 的 dispatchTouchEvent() 中优先于 onTouchEvent() 被调用。 | 返回 true:消费事件,阻止 onTouchEvent() 被调用。返回 false:不消费,继续执行 onTouchEvent()。 |
onClick() | OnClickListener | 通过 View.setOnClickListener() 设置。在 onTouchEvent() 处理 ACTION_UP 且满足点击条件时触发。 | N/A |
源码调用流程简述 (简化版,聚焦关键点):
-
起点 (
Activity):// Activity.java public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); // 可选回调,通知用户开始交互 } if (getWindow().superDispatchTouchEvent(ev)) { // 交给 Window return true; // Window 及其下的视图树消费了事件 } return onTouchEvent(ev); // 如果都没消费,Activity 自己尝试处理 } -
Window 到 DecorView (
PhoneWindow是常见实现):// PhoneWindow.java public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); // 转给 DecorView } -
DecorView 分发 (
FrameLayout子类):// DecorView.java public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); // 调用 ViewGroup 的 dispatchTouchEvent } -
ViewGroup 核心分发逻辑 (
ViewGroup.dispatchTouchEvent):// ViewGroup.java public boolean dispatchTouchEvent(MotionEvent ev) { // ... (处理事件有效性、辅助功能等) boolean intercepted = false; // 关键点 1: 检查是否要拦截 (ACTION_DOWN 或 已有 TouchTarget) if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); // 调用拦截方法 ev.setAction(action); // 恢复 action (防止被拦截方法修改) } else { intercepted = false; } } else { // 不是 DOWN 且没有 TouchTarget (说明没人消费 DOWN),直接拦截后续事件 intercepted = true; } // ... (检查取消事件等) TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; // 关键点 2: 如果不拦截 && 是 DOWN 事件 (或预取事件) && 事件在窗口内 && 没有 TouchTarget if (!canceled && !intercepted && (actionMasked == MotionEvent.ACTION_DOWN || ...)) { // ... (处理辅助焦点) // 关键点 3: 倒序遍历子 View 寻找能接收事件的 final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final View child = children[i]; // 检查 child 是否可见、坐标是否在 child 区域内 if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) { continue; } // 检查 child 是否在 TouchDelegate 内 (略) // 关键点 4: 分发给子 View! if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 子 View 消费了! // ... (记录这个 child 为 newTouchTarget, 添加到 mFirstTouchTarget 链表) break; // 找到了一个消费的 child, 跳出循环 } } } // 关键点 5: 处理 TouchTarget (mFirstTouchTarget) if (mFirstTouchTarget == null) { // 没有子 View 消费 || 被拦截了 -> 自己处理 (调用父类 View.dispatchTouchEvent -> onTouchEvent) handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // 有 TouchTarget (即有子 View 消费了之前的 DOWN) TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; // 已经发给新目标了 } else { // 关键点 6: 分发给之前消费 DOWN 的 TouchTarget (子 View) if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } } target = next; } } return handled; // 返回事件是否被消费 } // 辅助方法: 实际分发事件给特定 child 或自己 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { if (child == null) { // 调用自己的 super.dispatchTouchEvent (即 View.dispatchTouchEvent) return super.dispatchTouchEvent(transformedEvent); } else { // 调用 child 的 dispatchTouchEvent return child.dispatchTouchEvent(transformedEvent); } } -
View 的处理 (
View.dispatchTouchEvent):// View.java public boolean dispatchTouchEvent(MotionEvent event) { // ... (处理辅助功能等) // 关键点 1: 优先给 OnTouchListener ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; // Listener 消费了 } // 关键点 2: Listener 没消费 -> 调用自己的 onTouchEvent if (!result && onTouchEvent(event)) { result = true; // onTouchEvent 消费了 } // ... (清理等) return result; } // View.onTouchEvent() - 非常长,处理各种状态 (按下、移动、抬起、长按、点击等) public boolean onTouchEvent(MotionEvent event) { // ... (处理是否可用、点击检测、长按检测、滑动检测等) switch (action) { case MotionEvent.ACTION_DOWN: // 记录按下状态, 可能触发按下效果, 发送长按检测延迟消息 break; case MotionEvent.ACTION_MOVE: // 检查是否滑出边界 (取消点击/长按), 处理滑动 break; case MotionEvent.ACTION_UP: // 移除长按检测消息 // 如果满足点击条件 (在区域内抬起), 触发 performClick() -> onClickListener.onClick() // 移除按下效果 break; case MotionEvent.ACTION_CANCEL: // 移除按下效果, 取消长按检测 break; } return true; // 大多数可点击的 View 在按下时都返回 true }
通俗比喻总结:
-
快递到达 (触摸发生): 快递员(系统)把包裹(
ACTION_DOWN事件)送到公司总部(Activity)。 -
总部转交: 总部前台(
Window)签收,转给大楼前台(DecorView)。 -
大楼派送: 大楼前台(
DecorView,是个大部门经理ViewGroup)拿到包裹。- 他先看自己要不要(
onInterceptTouchEvent?默认不要)。 - 他查大楼地图(子 View 列表),从最顶层的办公室(最后绘制的子 View)开始问:“谁在收件地址(触摸点)附近?这个包裹是你的吗?”(倒序遍历子 View)。
- 他先看自己要不要(
-
部门处理: 如果包裹是给某个子部门(另一个
ViewGroup)的:- 该子部门经理重复步骤 3:自己要不要?再问自己手下。
-
员工签收: 包裹最终送到一个普通员工(
View)桌上。-
员工先问秘书(
OnTouchListener):“你帮我签吗?”如果秘书签了 (onTouch返回true),流程结束。 -
秘书不签,员工自己拆包处理 (
onTouchEvent):- 如果是按下的包裹 (
ACTION_DOWN),他亮起工位灯(按下状态),并设置一个 500ms 的闹钟(长按检测)。 - 如果是移动的包裹 (
ACTION_MOVE),他检查手有没有滑出工位范围(取消事件)。 - 如果是抬起的包裹 (
ACTION_UP),他关掉工位灯,如果手没滑出去且按下时间短,就大喊“我点了!”(触发onClick),关掉闹钟。
- 如果是按下的包裹 (
-
如果员工处理了包裹(
onTouchEvent返回true),他就告诉经理“我签收了”。
-
-
向上汇报:
- 如果员工签收了 (
true),经理就告诉大楼前台“有人签了”,大楼前台告诉总部前台“有人签了”,总部告诉快递员“妥投”。 - 如果员工不签 (
false),包裹退回给经理。经理想:“手下没人要?那我看看要不要?”(调用自己的onTouchEvent)。如果经理也不要,包裹继续退回给大楼前台... 一直退到总部。如果总部 (Activity) 也不要 (onTouchEvent返回false),快递员就知道“无人签收”。
- 如果员工签收了 (
-
后续包裹: 同一个手势的后续包裹 (
ACTION_MOVE,ACTION_UP) 通常会直接送给最开始签收ACTION_DOWN的那个人(或中途拦截的经理),不再重新问遍所有办公室(效率优化)。
关键要点:
ACTION_DOWN是核心: 它决定了谁接收整个事件流。后续事件默认发给消费了ACTION_DOWN的 View(或拦截它的 ViewGroup)。- 责任链模式: 事件分发是典型的“责任链”模式,从顶层到底层,再从底层回溯到顶层。
- 拦截权 (
onInterceptTouchEvent):ViewGroup的杀手锏,可以中途截胡事件流。 - 优先权 (
OnTouchListener): 比onTouchEvent优先级更高。 - 消费标志 (
return true): 任何环节返回true都表示“我处理了,到此为止”。 - 回溯机制: 如果子 View 不消费,父容器有机会自己处理。
理解了这个“快递配送”流程,处理滑动冲突(多个 ViewGroup 都想拦截)、自定义触摸行为就有了坚实的基础。记住:分发是自上而下 (dispatchTouchEvent + onInterceptTouchEvent),消费是自下而上 (onTouchEvent) 。