Android 觸控事件分發是面試高頻考點,也是自定義 View 必須掌握的核心知識。本文將從源碼角度,系統梳理 ViewGroup 與 View 的事件分發流程,解析 dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 三大核心方法的協作機制。
事件傳遞遵循「外部攔截、內部消費」原則:
Activity.dispatchTouchEvent 開始DecorView (FrameLayout)ViewGroup 逐層向下分發 (dispatchTouchEvent)View 進行處理 (onTouchEvent)onTouchEventpublic boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
關鍵邏輯:
OnTouchListener.onTouch(),返回 true 表示事件被消費,不再向下傳遞onTouchEvent() 繼續處理public boolean onTouchEvent(MotionEvent event) {
if (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 按下處理
break;
case MotionEvent.ACTION_UP:
performClick(); // 觸發點擊事件
break;
case MotionEvent.ACTION_CANCEL:
// 取消處理
break;
case MotionEvent.ACTION_MOVE:
// 移動處理
break;
}
return true; // 可點擊/長按視圖消費事件
}
return false; // 不可點擊視圖不消費
}
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
點擊事件最終回調 OnClickListener.onClick(),這就是我們平時設置 setOnClickListener 的觸發點。
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
// 1. ACTION_DOWN 時重置狀態
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 2. 判斷是否攔截
boolean intercepted = false;
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
intercepted = !disallowIntercept && onInterceptTouchEvent(ev);
} else {
intercepted = true; // 非序列起始且無目標視圖,直接攔截
}
// 3. 未被攔截,尋找目標子 View
if (!intercepted) {
// 遍歷子 View,找到包含觸摸點的 View
// 呼叫 child.dispatchTouchEvent(transformedEvent)
// 成功則加入 TouchTarget 列表
}
// 4. 無子 View 處理,ViewGroup 自身處理
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, false, null, ...);
}
return handled;
}
ViewGroup 決定是否攔截事件的關鍵:
true:攔截事件,自身處理(調用自身 onTouchEvent)false:不攔截,繼續分發給子 ViewfalseViewGroup 維護 TouchTarget 鏈表,記錄處理特定 Pointer 的子 View:
TouchTarget 對應一個子 View 及其處理的 Pointer IDdispatchTouchEvent 返回 false 不會加入 TouchTarget,後續事件不再分發給該 Viewprivate boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
if (child == null) {
// 無子 View 處理,ViewGroup 自身處理
handled = super.dispatchTouchEvent(transformedEvent);
} else {
// 座標轉換:從父容器座標系轉換到子 View 座標系
transformedEvent.offsetLocation(offsetX, offsetY);
if (!child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
// 關鍵:遞歸調用子 View 的 dispatchTouchEvent
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
Activity.dispatchTouchEvent
↓
DecorView (ViewGroup).dispatchTouchEvent
↓
ViewGroup.onInterceptTouchEvent → true: 自身處理
↓ false
遍歷子 View → 找到目標 View
↓
子 View.dispatchTouchEvent
↓
子 View.onTouchListener.onTouch → true: 消費結束
↓ false
子 View.onTouchEvent
↓
可點擊 → ACTION_UP → performClick → OnClickListener.onClick
↓
返回 true/false 給父 ViewGroup
↓
父 ViewGroup 根據返回值決定是否加入 TouchTarget
dispatchTouchEvent 向下分發onInterceptTouchEvent 決定是否攔截onTouchEvent / onTouch 處理事件A: dispatchTouchEvent 中 onTouch 返回 true 時,直接返回 true,不再調用 onTouchEvent,而 performClick 在 onTouchEvent 中觸發。
A: 父容器 onInterceptTouchEvent 中根據觸摸狀態動態決定是否攔截,配合 requestDisallowInterceptTouchEvent 使用。
Android 事件分發機制通過 dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 三大方法協作,實現了靈活的觸控事件路由。理解其核心流程與 TouchTarget 機制,是掌握自定義 View、解決滑動衝突的基石。