Android面試官必問的事件分發,你答得上來嗎?

 

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 學習,面試文檔,視頻收集大整理,有興趣的夥伴們可以看看~  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章