事件傳遞層級(jí)(責(zé)任鏈模式),事件分發(fā)機(jī)制的核心流程是:Activity → Window → DecorView → ViewGroup → View。整個(gè)過程從用戶觸摸屏幕開始,系統(tǒng)生成MotionEvent事件對(duì)象,首先傳遞給Activity的dispatchTouchEvent方法。通過 getWindow().superDispatchTouchEvent() 將事件移交 PhoneWindow,PhoneWindow再將事件傳遞給DecorView。DecorView是Activity的根View,繼承自FrameLayout(屬于ViewGroup),所以事件會(huì)繼續(xù)向下分發(fā)。對(duì)于ViewGroup的事件分發(fā),有三個(gè)關(guān)鍵方法:dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()。ViewGroup通過mFirstTouchTarget來記錄處理事件的子View。當(dāng)事件為ACTION_DOWN時(shí),ViewGroup會(huì)檢查是否攔截(onInterceptTouchEvent),如果不攔截,則遍歷子View尋找能夠處理事件的View。
一、核心流程與關(guān)鍵角色
1. 事件傳遞層級(jí)(責(zé)任鏈模式)
-
Activity首接收:
dispatchTouchEvent()最先處理事件,通過getWindow().superDispatchTouchEvent()將事件移交PhoneWindow -
DecorView中轉(zhuǎn):
PhoneWindow委托DecorView(繼承FrameLayout)處理事件 -
ViewGroup決策:決定攔截(
onInterceptTouchEvent)或向下分發(fā)(dispatchTransformedTouchEvent) -
View終處理:若無子View可處理,調(diào)用自身
onTouchEvent
deepseek_mermaid_20250805_532a47.png
2. 事件序列與關(guān)鍵動(dòng)作
-
序列組成:
ACTION_DOWN→ACTION_MOVE(多次)→ACTION_UP/ACTION_CANCEL -
攔截鎖定:若
ViewGroup在ACTION_DOWN時(shí)攔截,后續(xù)事件直接調(diào)用其onTouchEvent(跳過攔截判斷) -
消費(fèi)綁定:View 處理
ACTION_DOWN后,才能接收同一序列后續(xù)事件
二、核心方法與機(jī)制源碼解析
1. 關(guān)鍵方法職責(zé)對(duì)比
| 方法 | 調(diào)用者 | 作用 | 返回值意義 |
|---|---|---|---|
dispatchTouchEvent() |
所有組件 | 事件分發(fā)入口,決定向下傳遞或自行處理 | true表示消費(fèi),終止傳遞 |
onInterceptTouchEvent |
僅ViewGroup | 判斷是否攔截事件(不傳遞給子View) | true攔截,false不攔截 |
onTouchEvent() |
所有組件 | 事件處理終點(diǎn),實(shí)現(xiàn)點(diǎn)擊/滑動(dòng)邏輯 | true表示消費(fèi)事件 |
2. 核心機(jī)制源碼解析(ViewGroup)
ViewGroup.dispatchTouchEvent 核心流程
public boolean dispatchTouchEvent(MotionEvent ev) {
// 步驟1:預(yù)處理(重置狀態(tài)等)
if (actionMasked == MotionEvent.ACTION_DOWN) {
resetTouchState(); // 重置攔截狀態(tài)
}
// 步驟2:檢查攔截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 檢查是否禁止攔截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev); // 關(guān)鍵攔截點(diǎn)
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true; // 無目標(biāo)View時(shí)默認(rèn)攔截
}
// 步驟3:尋找目標(biāo)View
if (!intercepted) {
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = getAndVerifyPreorderedView();
if (!canViewReceivePointerEvents(child)) continue;
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 找到目標(biāo)View并建立聯(lián)系
newTouchTarget = addTouchTarget(child, idBitsToAssign);
break;
}
}
}
// 步驟4:事件分發(fā)
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget target = mFirstTouchTarget;
while (target != null) {
if (alreadyDispatchedToNewTouchTarget) {
handled = true;
} else {
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
target = target.next;
}
}
// 步驟5:后續(xù)處理
if (canceled || actionMasked == MotionEvent.ACTION_UP) {
resetTouchState(); // 事件序列結(jié)束重置
}
return handled;
}
(1) 攔截判斷邏輯
// 判斷條件:ACTION_DOWN事件 或 已有子View處理事件(mFirstTouchTarget != null)
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev); // 調(diào)用攔截方法
} else {
intercepted = false; // 子View調(diào)用requestDisallowInterceptTouchEvent強(qiáng)制不攔截
}
} else {
intercepted = true; // 非DOWN事件且無子View處理,默認(rèn)攔截
}
-
mFirstTouchTarget:記錄處理
ACTION_DOWN的子View,決定后續(xù)事件流向 -
FLAG_DISALLOW_INTERCEPT:子View通過
requestDisallowInterceptTouchEvent()禁止父容器攔截(對(duì)ACTION_DOWN無效)
(2) 尋找事件處理子View
if (!canceled && !intercepted) {
for (int i = childrenCount - 1; i >= 0; i--) { // 逆序遍歷子View(后添加的優(yōu)先)
if (child.getFrame().contains(x, y)) { // 檢查觸摸點(diǎn)是否在子View區(qū)域內(nèi)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child); // 成功消費(fèi)則添加到TouchTarget鏈表
break;
}
}
}
}
-
坐標(biāo)轉(zhuǎn)換:分發(fā)時(shí)自動(dòng)調(diào)整
MotionEvent的坐標(biāo)到子View坐標(biāo)系
// ViewGroup.dispatchTransformedTouchEvent
if (child == null) {
handled = super.dispatchTouchEvent(event); // 調(diào)用View的方法
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY); // 坐標(biāo)轉(zhuǎn)換
handled = child.dispatchTouchEvent(event); // 子View分發(fā)
event.offsetLocation(-offsetX, -offsetY); // 坐標(biāo)還原
}
(3) 事件二次分發(fā)
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null); // 無子View處理,調(diào)用自身onTouchEvent
} else {
TouchTarget target = mFirstTouchTarget;
while (target != null) {
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child)) {
handled = true; // 向已記錄的子View分發(fā)事件
}
target = target.next;
}
}
三、特殊場(chǎng)景處理與性能優(yōu)化
1. 滑動(dòng)沖突解決方案
| 沖突類型 | 解決方案 | 適用場(chǎng)景 |
|---|---|---|
| 同方向滑動(dòng)(如ScrollView嵌套ListView) | 根據(jù)滑動(dòng)方向判斷: - 縱向距離大:父容器攔截 - 橫向距離大:子View處理4 | 類似淘寶商品詳情頁 |
| 異方向滑動(dòng)(如ViewPager內(nèi)嵌地圖) | 子View在滾動(dòng)到邊界時(shí)通知父容器接管: parent.requestDisallowIntercept(false)6 |
地圖與頁簽聯(lián)動(dòng) |
| 嵌套滑動(dòng)組件 | 使用 NestedScrolling 機(jī)制: 實(shí)現(xiàn) NestedScrollingChild3/Parent3 接口2 |
RecyclerView嵌套ExpandableListView |
2. ACTION_CANCEL 處理要點(diǎn)
ACTION_CANCEL核心作用:事件序列中斷通知
當(dāng)某個(gè) View 已經(jīng)開始處理事件序列(即已消費(fèi)了
ACTION_DOWN),但后續(xù)事件被外部因素強(qiáng)制中斷時(shí),系統(tǒng)會(huì)發(fā)送ACTION_CANCEL通知該 View:
- 觸發(fā)場(chǎng)景:父容器中途攔截事件、窗口失去焦點(diǎn)、View被移除
-
必須重置狀態(tài):在
onTouchEvent中需處理ACTION_CANCEL:
case MotionEvent.ACTION_CANCEL:
setPressed(false); // 取消按壓狀態(tài)
cancelLongPress(); // 終止長按檢測(cè)
resetTouchState(); // 重置標(biāo)志位
所有自定義 View 都應(yīng)包含以下邏輯:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// 1. 設(shè)置按壓狀態(tài)
setPressed(true);
// 2. 啟動(dòng)長按檢測(cè)
startLongPressCheck();
return true;
case MotionEvent.ACTION_MOVE:
// 3. 檢查是否移出邊界
if (isOutsideView(event)) {
setPressed(false);
}
return true;
case MotionEvent.ACTION_UP:
// 4. 觸發(fā)點(diǎn)擊事件
performClick();
// 5. 重置狀態(tài)
resetTouchState();
return true;
case MotionEvent.ACTION_CANCEL: // 關(guān)鍵處理
// 6. 立即終止所有交互狀態(tài)
setPressed(false);
// 7. 取消長按檢測(cè)
cancelLongPressCheck();
// 8. 重置觸摸標(biāo)志位
resetTouchState();
return true;
}
return super.onTouchEvent(event);
}
與 ACTION_UP 的本質(zhì)區(qū)別
| 特性 | ACTION_UP |
ACTION_CANCEL |
|---|---|---|
| 觸發(fā)源 | 用戶手指抬起 | 系統(tǒng)強(qiáng)制生成 |
| 交互完整性 | 完整的事件序列 | 被中斷的事件序列 |
| 后續(xù)事件 | 無 | 可能有后續(xù)事件(父容器處理) |
| 業(yè)務(wù)邏輯觸發(fā) | 應(yīng)執(zhí)行點(diǎn)擊/滑動(dòng)完成邏輯 | 必須終止當(dāng)前操作且不觸發(fā)邏輯 |
| 狀態(tài)恢復(fù) | 正常狀態(tài)恢復(fù) | 緊急狀態(tài)恢復(fù) |
3. 性能優(yōu)化技巧
-
避免對(duì)象創(chuàng)建:不在
onTouchEvent中new Rect()等對(duì)象(高頻MOVE事件易觸發(fā)GC) -
事件過濾:使用
ViewConfiguration獲取系統(tǒng)閾值:
int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); // 最小滑動(dòng)距離
int minFlingVelocity = getScaledMinimumFlingVelocity(); // 最小拋擲速度
-
高頻事件節(jié)流:對(duì)
ACTION_MOVE使用時(shí)間戳或距離差過濾
四、高級(jí)特性與面試深度考點(diǎn)
1. 事件處理優(yōu)先級(jí)
圖表

-
OnTouchListener > onTouchEvent > onClick:若設(shè)置
OnTouchListener且返回true,則onTouchEvent和onClick不會(huì)觸發(fā)
2. 多點(diǎn)觸控實(shí)現(xiàn)
@Override
public boolean onTouchEvent(MotionEvent event) {
int actionIndex = event.getActionIndex(); // 獲取當(dāng)前手指索引
int pointerId = event.getPointerId(actionIndex);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_POINTER_DOWN: // 次要手指按下
handleAdditionalFinger(pointerId, event.getX(actionIndex), event.getY(actionIndex));
break;
case MotionEvent.ACTION_POINTER_UP: // 次要手指抬起
removeFinger(pointerId);
break;
}
}
3. 安卓新版本特性
-
預(yù)測(cè)性滾動(dòng)(Android 12+):通過
MotionEvent.getPredictions()獲取未來軌跡 - 事件分類(Android 13+):
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
int classification = event.getClassification();
if (classification == MotionEvent.CLASSIFICATION_DEEP_PRESS) {
// 處理重壓操作
}
}
五、面試回答技巧與示例
1. 基礎(chǔ)問題應(yīng)答框架
面試官:事件分發(fā)流程是怎樣的?
答:
“事件分發(fā)從Activity開始,依次經(jīng)過PhoneWindow、DecorView、ViewGroup,最終到達(dá)View。核心方法是:
dispatchTouchEvent()負(fù)責(zé)事件分發(fā)onInterceptTouchEvent()(ViewGroup特有)決定是否攔截onTouchEvent()執(zhí)行最終處理
整個(gè)過程類似快遞派送:Activity是總部,ViewGroup是分揀中心,View是收貨人?!?/li>
2. 源碼級(jí)問題應(yīng)答
面試官:ViewGroup如何保證同一事件序列的子事件傳遞一致性?
答:
“關(guān)鍵在于mFirstTouchTarget機(jī)制:
- ACTION_DOWN階段:若子View消費(fèi)事件,
addTouchTarget()會(huì)將其記錄到mFirstTouchTarget鏈表- 后續(xù)事件處理:直接通過鏈表中的
TouchTarget分發(fā)給對(duì)應(yīng)子View(跳過遍歷查找)- 攔截時(shí)重置:當(dāng)
onInterceptTouchEvent返回true時(shí),會(huì)向子View發(fā)送ACTION_CANCEL并清空mFirstTouchTarget
3. 沖突解決案例
問題場(chǎng)景:ScrollView內(nèi)嵌橫向RecyclerView時(shí)滑動(dòng)沖突
解決方案:
public class CustomScrollView extends ScrollView { private float startX, startY; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: startX = ev.getX(); startY = ev.getY(); break; case MotionEvent.ACTION_MOVE: float dx = Math.abs(ev.getX() - startX); float dy = Math.abs(ev.getY() - startY); // 橫向滑動(dòng)距離更大時(shí)攔截事件 if (dx > dy && dx > ViewConfiguration.get(getContext()).getScaledTouchSlop()) { return true; } } return super.onInterceptTouchEvent(ev); } }
關(guān)于ACTION_CANCEL 的一些使用場(chǎng)景
1.嵌套滑動(dòng)組件協(xié)調(diào)
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
// 將未完成的滑動(dòng)進(jìn)度交還給父容器
parent.requestNestedScroll(remainingScroll);
}
}
2.動(dòng)畫中斷處理
// 按壓動(dòng)畫的取消
ValueAnimator pressAnimator;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_CANCEL:
if (pressAnimator != null) {
// 平滑取消動(dòng)畫而非立即停止
pressAnimator.cancel(); // 觸發(fā)onAnimationCancel()
}
return true;
}
}
3. 游戲角色控制
// 游戲角色移動(dòng)中斷
public boolean onTouchEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
// 立即停止角色移動(dòng)
playerCharacter.setVelocity(0, 0);
// 顯示中斷特效
spawnCancellationEffect();
}
}
