一點見解: 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
- 因為
Activity#dispatchTouchEvent(MotionEvent ev)
是事件分發(fā)的起點站, 所以只要重寫這個方法不調(diào)用PhoneWindow#superDispatchTouchEvent(MotionEvent ev)
就可以使得整個Activity
內(nèi)的控件接收不到任何事件. 實際上還有幾個類似的dispatchXXXEvent()
方法, 可以攔截鍵盤點擊事件等. - 在
Activity#dispatchTouchEvent(MotionEvent ev)
里面出現(xiàn)了onUserInteraction()
, 這個方法可以看作一個回調(diào), 任何用戶操作開始之前, 包括鍵盤操作都會調(diào)用這個方法, 所以可以重寫這個方法來監(jiān)聽用戶操作的開始節(jié)點. 還有一個對應方法onUserLeaveHint()
- 在
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)鍵點
- 它是如何把事件傳遞給下一個控件的, 包括之前提到的攔截等
- 返回值標識事件是否被消費, 所以它是如何確定返回值的為了讓代碼更清晰, 逐段分析源碼, 以下源碼都是來自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#dispatchTouchEvent
對ACTION_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
來攔截特定的事件
但是攔截方法同樣有條件判斷
- 需要是
ACTION_DOWN
事件, 或者mFirstTouchTarget
不為空, 而mFirstTouchTarget
即是第一個消費事件的子控件, 所以如果有子控件消費了事件, 那么后續(xù)總會調(diào)用ViewGroup#onInterceptTouchEvent
, 父控件仍有機會攔截事件, 而如果是父控件自身消費了ACTION_DOWN
事件, 那么就不會再調(diào)用ViewGroup#onInterceptTouchEvent
了 - 還需要沒有設(shè)置
FLAG_DISALLOW_INTERCEPT
標志位, 很容易找到相關(guān)的方法ViewGroup#requestDisallowInterceptTouchEvent
, 也就是可以通過調(diào)用這個方法來禁用攔截機制.
至此ViewGroup
分發(fā)機制涉及的方法大致分析完畢了.
源碼Bonus
- 可以通過
ViewGroup#requestDisallowInterceptTouchEvent
禁用攔截機制. - 可以通過對子控件設(shè)置
AccessibilityFocused
來在遍歷子控件的時候優(yōu)先詢問該子控件是否接收ACTION_DOWN
事件. - 默認的遍歷順序是根據(jù)子控件在布局中的Z軸值來決定的, 但是可以重寫
ViewGroup#getChildDrawingOrder
來修改默認的子控件遍歷順序.
通過本文可以知道, 無論有沒有子控件接收事件, 事件都會傳遞給View#dispatchTouchEvent
, 所以下一篇將分析View
類