Android touch 事件的分發,是面試中最常被問到的問題之一。我們來看看 😎、😨 和 🤔️ 三位同學是怎麼回答的吧
😎 自認爲無所不知,水平已達應用開發天花板,目前月薪 10k
面試官:講講 Android 的事件分發機制
😎:當用戶手指觸摸屏幕時,Android 會將對應的事件包裝成一個事件對象 MotionEvent ,從 ViewTree 的頂部至上而下地分發傳遞。用戶從手指接觸屏幕至離開屏幕會產生一系列的事件,事件是以 down 開始,up 或 cancel 結束,中間無數個 move ; 一個事件的分發順序是:Activity 到 ViewGroup 再到 View
面試官:事件分發的過程用到哪些方法
😎:首先是 dispatchTouchEvent 執行事件分發的方法,整個事件分發的過程就是在遞歸這個方法;
然後就是 onTouchEvent 消費方法,View 響應點擊事件、ScrollView 響應滾動事件就是在這裏面實現
面試官:還有一個攔截方法呢?
😎:什麼攔截方法,分發關攔截什麼事?(糟糕背的答案忘了)
面試官:哦,沒事,回去等通知吧。
😨 業餘時間經常打遊戲、追劇、熬夜,目前月薪 15k
面試官:事件分發的過程用到哪些方法
😨:有 dispatchTouchEvent 、onTouchEvent 、 onInterceptTouchEvent ;ViewGroup 在調用 dispatchTouchEvent 進行事件分發時,會適時調用 onInterceptTouchEvent ,來判斷是否能攔截這個事件。相應如果不想 ViewGroup 攔截事件,可以調用 ViewGroup 的 requestDisallowInterceptTouchEvent方法,傳 true 就是禁止攔截,false 你開心就攔吧;常用來解決一些嵌套 View 的事件衝突。
面試官:說一下這些方法的關係
😨:比如 ScrollView 用戶手指點擊下去時,Down 事件會被子 View 消費,這樣如果緊接着用戶手指直接擡起那這個子 View 就消費這個完整的事件序列,一般是點擊事件;而如果接下去用戶的手指進行滑動產生 Move事件,那就必須要由 ScrollView 來響應滾動事件了,爲了能達到這個效果 ScrollView 在 dispatchTouchEvent( Move ) 時,調用 onInterceptTouchEvent 返回了 true 來實現攔截事件,不再向子 View 分發。
看一下僞代碼
// 事件分發到某個具體的 ViewGroup,會直接調用 dispatchTouchEvent() 方法
public boolean dispatchTouchEvent(MotionEvent ev) {
//代表是否消費事件
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
// 如果 onInterceptTouchEvent() 返回 true 則代表當前 View 攔截了事件
// 則該事件則會交給當前View進行處理
// 即調用 onTouchEvent() 方法去處理事件
consume = onTouchEvent (ev) ;
} else {
// 如果 onInterceptTouchEvent() 返回 false 則代表當前 View 不攔截事件
// 則該事件則會繼續傳遞給它的子元素
// 子元素的 dispatchTouchEvent() 就會被調用,重複上述過程
// 直到事件被最終處理爲止
consume = child.dispatchTouchEvent(ev); //遍歷處理
}
return consume;
}
面試官:你這僞代碼雖然通俗易懂,但是省略了太多邏輯了,子 View 在消費掉 Down 事件後,後續的事件都給會傳遞給它,你知道是怎麼實現的嗎
😨:具體怎麼實現沒關注
面試官:好的,回去等通知吧。
🤔️ 堅持每天學習、不斷的提升自己,目前月薪 30k
面試官:講講 Android 的事件分發機制
🤔️:說起來太費勁了,上神圖,放大了橫屏看:
面試官:子 View 在消費掉 Down 事件後,後續的事件都會傳遞給它,你知道是怎麼實現的嗎
🤔️:ViewGroup 裏面用了一個成員變量 mFirstTouchTarget 來保存消費事件的子 View 信息,因爲安卓是支持多指操作的,所以這個 mFirstTouchTarget 是一個 TouchTarget 的鏈表。在View 的 dispatchTouchEvent 可以分爲三個階段:判斷是否需要攔截;分發事件找到消費事件的子 View,更新到 mFirstTouchTarget;根據是否攔截和 mFirstTouchTarget 再次分發事件。
再細節我們就要到源碼裏看實現了,以下爲 API 28 ViewGroup 的 dispatchTouchEvent 部分源碼:
1. 判斷是否需要攔截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// disallowIntercept 就是 requestDisallowInterceptTouchEvent 設置的
// 根據 disallowIntercept 和 onInterceptTouchEvent 決定 intercepted
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// 不是 Down 事件 並且之前的事件沒有被子 View 捕獲,就可以直接攔截
intercepted = true;
}
2. 分發事件找到消費事件的子 View
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN || ...) {
// 只分發 Down 事件(省略的爲多指或鼠標的情況)
for (int i = childrenCount - 1; i >= 0; i--) {
...
//調用 dispatchTransformedTouchEvent 方法將事件分發給子 View
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
// 如果事件被子 View 消費,更新 mFirstTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
break;
}
...
}
}
}
3. 根據攔截結果和 mFirstTouchTarget 再次分發事件。
if (mFirstTouchTarget == null) {
// 沒有子 View 消費事件,則傳入 null 去分發,最終調用的是自身的 onTouchEvent 方法,進行處理 touch 事件
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) {
handled = true; //已經處理了的避免重複分發
} else {
//如果 intercepted 就取消 cancelChild,這便是攔截子 View 事件的原理
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
//內部會比較 pointerIdBits 和當前事件的 pointerIdBits,一致纔會處理
//這便是 Down 事件處理後後續事件都交給該 View 處理的原理
handled = true;
}
}
...
}
}
好了,終於說完了
面試官:太多了,能總結下嗎?
🤔️:好吧,我們來複習一下:
-
判斷是否需要攔截 —> 主要是根據 onInterceptTouchEvent 方法的返回值來決定是否攔截;
-
在 DOWN 事件中將 touch 事件分發給子 View —> 這一過程如果有子 View 捕獲消費了 touch 事件,會對 mFirstTouchTarget 進行賦值;
-
dispatchTouchEvent 的最後一步,DOWN、MOVE、UP 事件都會根據 mFirstTouchTarget 是否爲 null,決定是自己處理 touch 事件,還是分發給子 View。
-
DOWN 事件是事件序列的起點;決定後續事件由誰來消費處理;
-
mFirstTouchTarget 的作用:記錄捕獲消費 touch 事件的 View,是一個鏈表結構;
-
CANCEL 事件的觸發場景:當父視圖先不攔截,然後在 MOVE 事件中重新攔截,此時子 View 會接收到一個 CANCEL 事件。
-
如果一個事件最後所有的 View 都不處理的話,最終回到 Activity 的 onTouchEvent 方法裏面來。
面試官:可以,我們再來聊聊別的。
看完了這三位同學的面試表現,你有什麼感想呢?歡迎關注評論留言討論,另外今天留一些簡單的思考題,如果答不出來的建議收藏文章多看幾遍。
- 如果一個事件序列的 ACTION_DOWN 事件被 ViewGroup 攔截,此時子 View 調用 requestDisallowInterceptTouchEvent 方法有沒有用?
- ACTION_DOWN 事件被子 View 消費了,那 ViewGroup 能攔截剩下的事件嗎?如果攔截了剩下事件,當前這個事件 ViewGroup 能消費嗎?子 View 還會收到事件嗎?
- 當 View Disable 時,會消費事件嗎?
最後這裏是關於我自己的Android 學習,面試文檔,視頻收集大整理,有興趣的夥伴們可以看看~