一點見解: Android事件分發(fā)機制(二)

一點見解: Android事件分發(fā)機制(一) - 基本概念解釋
一點見解: Android事件分發(fā)機制(二) - 分析ViewGroup
一點見解: Android事件分發(fā)機制(三) - 分析View

本文主要分析事件分發(fā)機制的傳遞路徑和傳遞規(guī)則, 著重分析ViewGroup.

對于源碼的分析假設(shè)大家總是能夠找到具體的源碼, 所以只貼出關(guān)鍵的部分進行分析.

分發(fā)的源頭邏輯分析

從頭開始最清晰.

事件最開始會由系統(tǒng)分發(fā)給Activity#dispatchTouchEvent(MotionEvent ev)

注意這時候還沒進入控件間的事件分發(fā)邏輯, 因為Activity不是一個View.那么Activity又是怎樣把事件傳給第一個View的, 又是傳給了誰, 看源碼.

// Activity#dispatchTouchEventpublic 
boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) { 
        onUserInteraction(); 
    } 
    if (getWindow().superDispatchTouchEvent(ev)) {// 分發(fā)了這個事件 
        return true;
    } 
    return onTouchEvent(ev);
}

因為Activity只是一個中轉(zhuǎn)站, 所以代碼不多, 關(guān)鍵代碼就是getWindow().superDispatchTouchEvent(ev).
getWindow()返回的是一個Window抽象類, 在Android中, 唯一繼承了這個抽象類的類是PhoneWindow, 所以這里實際調(diào)用的就是PhoneWindow#superDispatchTouchEvent(MotionEvent ev), 同樣PhoneWindow也不是View, 所以還要再看PhoneWindow源碼

// PhoneWindow.javaprivate DecorView mDecor;
@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) { 
    return mDecor.superDispatchTouchEvent(event);
}
// PhoneWindow.DecorView 內(nèi)部類
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
    public boolean superDispatchTouchEvent(MotionEvent event) { 
        return super.dispatchTouchEvent(event); 
    }
}

從摘錄的源碼可以得到結(jié)論

系統(tǒng)分發(fā)事件給Activity, 然后傳遞給PhoneWindow, 接著傳遞給PhoneWindow實例中的DecorView, 而DecorView是一個View(繼承了FrameLayout), 之后, 事件就進入了控件間傳遞邏輯了.

源碼Bonus

  1. 因為Activity#dispatchTouchEvent(MotionEvent ev)是事件分發(fā)的起點站, 所以只要重寫這個方法不調(diào)用PhoneWindow#superDispatchTouchEvent(MotionEvent ev)就可以使得整個Activity內(nèi)的控件接收不到任何事件. 實際上還有幾個類似的dispatchXXXEvent()方法, 可以攔截鍵盤點擊事件等.
  2. Activity#dispatchTouchEvent(MotionEvent ev)里面出現(xiàn)了onUserInteraction(), 這個方法可以看作一個回調(diào), 任何用戶操作開始之前, 包括鍵盤操作都會調(diào)用這個方法, 所以可以重寫這個方法來監(jiān)聽用戶操作的開始節(jié)點. 還有一個對應方法onUserLeaveHint()
  3. Activity#dispatchTouchEvent(MotionEvent ev)中如果PhoneWindow#superDispatchTouchEvent(MotionEvent ev)沒有消費掉這個事件, 會調(diào)用Activity#onTouchEvent(MotionEvent event)來嘗試消費事件.

從ViewGroup開始

從上面分析可以知道, 第一個接收到事件的View方法是DecorView#superDispatchTouchEvent(MotionEvent event), 里面直接調(diào)用了super.dispatchTouchEvent(), DecorView直接繼承FrameLayout, 一路跟蹤過去就可以得到結(jié)論

控件間事件傳遞的起始方法是ViewGroup#dispatchTouchEvent()

值得指出的是, View也有dispatchTouchEvent(), 后面再說.

從方法命名就可以看出, 這個方法的作用是分發(fā)事件, 所以事件分發(fā)機制的實現(xiàn)邏輯就在這個方法里面, 這部分的代碼有200多行, 其中很多代碼都是保持控件狀態(tài)一致或者處理多點觸控的問題, 本文不關(guān)心這部分的實現(xiàn), 所以在分析前需要明確分析的關(guān)鍵點

  1. 它是如何把事件傳遞給下一個控件的, 包括之前提到的攔截等
  2. 返回值標識事件是否被消費, 所以它是如何確定返回值的為了讓代碼更清晰, 逐段分析源碼, 以下源碼都是來自ViewGroup#dispatchTouchEvent

傳遞規(guī)則

因為要把事件分發(fā)給子控件, 所以在這個方法內(nèi)必定會遍歷子控件的, 所以我們首先找到這部分遍歷代碼, 如下

for (int i = childrenCount - 1; i >= 0; i--) {
    // 允許修改默認的child獲取規(guī)則, 但是一般情況下會獲取
    children[childIndex] final View child = (preorderedList == null) 
                                ? children[childIndex] : preorderedList.get(childIndex); 
    // 省略通常不會影響流程的代碼 
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {// 關(guān)鍵方法 
        // ... 
        newTouchTarget = addTouchTarget(child, idBitsToAssign);// 關(guān)鍵方法 
        // ... break; 
    }
}

上面在遍歷的過程有個關(guān)鍵的判斷中執(zhí)行了ViewGroup#dispatchTransformedTouchEvent方法, 代碼就不貼出來了, 雖然有一系列的判斷, 但是歸根到底就是判斷child參數(shù)是否為空, 為空就執(zhí)行super.dispatchTouchEvent()不為空就執(zhí)行child.dispatchTouchEvent(), 這里child必定不為空, 所以就是在這里把事件傳遞給了子控件.

傳遞給子控件后, 返回true證明子控件接收了這個事件, 注意, 這里有另一個關(guān)鍵方法ViewGroup#addTouchTarget, 這個方法把當前這個接收事件的子控件轉(zhuǎn)換成了TouchTarget對象并賦值給了mFirstTouchTarget, 為什么要這樣做?

因為這個遍歷代碼是包含在一個3重條件判斷里面的, 也就說有可能不被執(zhí)行, 看看判斷的條件

if (!canceled && !intercepted) { 
    // ...
    if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { 
        // ... 
        if (newTouchTarget == null && childrenCount != 0) { 
            // 遍歷代碼 
        } 
    }
}

第一重判斷根據(jù)命名可以推測是ACTION_CANCEL事件和被當前控件攔截事件, 之后再討論;
第三重判斷只要存在子控件就會為true;
關(guān)鍵是第二重判斷, 限制了事件必須是ACTION_DOWN, ACTION_POINTER_DOWN或者ACTION_HOVER_MOVE才有可能進入遍歷代碼(對分發(fā)機制我們只關(guān)注ACTION_DOWN事件, 所以后面省略另外兩個), 也就是說當事件為ACTION_MOVE等中間事件時, 是不會直接執(zhí)行遍歷代碼的, 也就不會把事件分發(fā)給子控件, 所以還會有地方執(zhí)行分發(fā)工作, 也就是調(diào)用ViewGroup#dispatchTransformedTouchEvent, 查找其余部分代碼找到

if (mFirstTouchTarget == null) { 
    handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} else { 
    // ... 
    TouchTarget target = mFirstTouchTarget; 
    while (target != null) { 
        // ... 
        final TouchTarget next = target.next; 
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { 
            handled = true; 
        } else { 
            final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; 
            if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { 
                handled = true; 
            } 
            // ... 
        }
        // ... 
        target = next;  
    }
}

這段代碼總是會執(zhí)行的, 關(guān)鍵的判斷依據(jù)是mFirstTouchTarget是否為空, 為空時最后就會調(diào)用View#dispatchTouchEvent, 否則就會把事件傳給mFirstTouchTarget對應的子控件, 結(jié)合上面的分析, 只有在子控件接收了ACTION_DOWN等事件的時候, 它才不為空, 也就是說

當子控件沒有接收ACTION_DOWN(即是View#dispatchTouchEventACTION_DOWN沒有返回true)的時候, 后續(xù)的事件就不會分發(fā)給這個子控件.

不為空的時候可以看到mFirstTouchTarget其實是一個鏈表, 會把事件分發(fā)給鏈表中的所有子控件, 這是針對多點觸控的處理, 不是本文關(guān)注的問題, 不作分析, 只需要知道其他ACTION_DOWN事件的傳遞不會重新遍歷所有子控件, ACTION_DOWN是整個操作(一系列事件)的起點, 在這時候就已經(jīng)確定后續(xù)事件需要傳遞的子控件了.

分析到這里我們已經(jīng)知道事件分發(fā)機制是怎樣在控件間傳遞事件的了

父控件遍歷子控件, 詢問所有子控件是否接收ACTION_DOWN事件, 然后保存接收事件的子控件到鏈表, 確定后續(xù)事件的分發(fā)對象, 當其他事件傳遞給父控件時直接傳遞事件給鏈表中的子控件. 當沒有子控件接收ACTION_DOWN時執(zhí)行View#dispatchTouchEvent

攔截事件 onInterceptTouchEvent

ViewGroup的事件分發(fā)還有一個關(guān)鍵點, 就是上面提到的遍歷的第一重判斷中的intercepted變量, 如果這個變量為true那么即使是ACTION_DOWN事件也不會遍歷詢問子控件, 這時mFirstTouchTarget鏈表就必定為空, 后續(xù)的所有事件都會傳遞給View#dispatchTouchEvent而不會傳給子控件., 也就是說此時父控件攔截了傳遞給它的事件

final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 
    if (!disallowIntercept) { 
        intercepted = onInterceptTouchEvent(ev);
        // ...
    } else { 
        intercepted = false; 
    }
} else { 
    intercepted = true;
}

一般情況下intercepted的值由ViewGroup#onInterceptTouchEvent決定, 值得指出, View是沒有這個方法的, 很容易理解這是因為View不會有其他子控件了, 沒有攔截事件的需要.

接著看ViewGroup#onInterceptTouchEvent, 里面直接返回了false, 默認不攔截事件, 因此

可以通過重寫ViewGroup#onInterceptTouchEvent來攔截特定的事件

但是攔截方法同樣有條件判斷

  1. 需要是ACTION_DOWN事件, 或者mFirstTouchTarget不為空, 而mFirstTouchTarget即是第一個消費事件的子控件, 所以如果有子控件消費了事件, 那么后續(xù)總會調(diào)用ViewGroup#onInterceptTouchEvent, 父控件仍有機會攔截事件, 而如果是父控件自身消費了ACTION_DOWN事件, 那么就不會再調(diào)用ViewGroup#onInterceptTouchEvent
  2. 還需要沒有設(shè)置FLAG_DISALLOW_INTERCEPT標志位, 很容易找到相關(guān)的方法ViewGroup#requestDisallowInterceptTouchEvent, 也就是可以通過調(diào)用這個方法來禁用攔截機制.

至此ViewGroup分發(fā)機制涉及的方法大致分析完畢了.

源碼Bonus

  1. 可以通過ViewGroup#requestDisallowInterceptTouchEvent禁用攔截機制.
  2. 可以通過對子控件設(shè)置AccessibilityFocused來在遍歷子控件的時候優(yōu)先詢問該子控件是否接收ACTION_DOWN事件.
  3. 默認的遍歷順序是根據(jù)子控件在布局中的Z軸值來決定的, 但是可以重寫ViewGroup#getChildDrawingOrder來修改默認的子控件遍歷順序.

通過本文可以知道, 無論有沒有子控件接收事件, 事件都會傳遞給View#dispatchTouchEvent, 所以下一篇將分析View

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容