前言
聊到事件分發,很多朋友就會想到view的dispatchTouchEvent
,其實在此之前,Android還做了很多工作。
比如跨程式獲取輸入事件的方式?在dispatchTouchEvent
責任鏈之前還有一條InputStage
責任鏈?DecorView,PhoneWindow
之間的傳遞順序?
另外還包括事件分發過程中事件序列的處理方式?ViewGroup和View之間的協調?mFirstTouchTarget
真假連結串列?等等。
這一切,都要從你可愛的小拇指
說起...
當你的拇指觸碰手機的那一剎那,手機就被你深深的影響了,沒錯,手機會收到你給他佈置的任務。
這個任務可以是:
- 滑動介面任務
- 點選按鈕任務
- 長按任務
等等,總之,你向手機傳遞了這個任務資訊,接下來就是手機的處理任務時間。
我們可以假設手機系統就是一個大的公司(Android公司)
,而我們觸控手機的任務就是一個完整的專案需求,今天就和大家一起深入Android公司內部,打探事件分發的那些祕密。
在此之前,我也列出了問題和大綱:
硬體部門和核心部門
首先,我的拇指找到了Android
公司,說出了自己的需求,比如:點選某個View並滑動到另外的位置。
Android
公司會派出硬體部門,和我的小拇指進行會談,接收到我的需求之後,硬體部門生成簡單的終端,並傳遞給核心部門。
核心部門將任務進行加工,生成了內部事件——event,並新增到公司內部的一個管理系統 /dev/input/
目錄下。
這樣做的目的是把外來的需求轉化成內部通用,都能看懂的任務。
任務處理部門(SystemServer程式)
當任務記錄在公司管理系統上,就會有專門的任務處理部門對這些任務進行處理,他們做的事情就是一直監聽/dev/input/
目錄,當發現有新的事件就會進行處理。
那這個任務處理部門到底是何方神聖呢?
不知道大家還記不記得在SystemServer
程式中啟動了一系列系統有關的服務,比如AMS,PMS等等,其中還有一個不是很起眼的角色,叫做InputManagerService
。
這個服務就是用來負責與硬體通訊,接受螢幕輸入事件。
在其內部,會啟動一個讀執行緒,也就是InputReader
,它會從這個管理系統也就是/dev/input/
目錄拿到任務,並且分發給InputDispatcher
執行緒,然後進行統一的事件分發排程。
分配給具體的專案組(InputChannel)
然後任務處理部門需要把任務交給 專業處理任務的專案組了,這就涉及到跨部門溝通了(跨程式通訊)。
大家都知道跨部門溝通是個比較麻煩的事情,誰來完成這個事情呢?InputChannel
。
讓我們回到ViewRootImpl
的setView
方法:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
//建立InputChannel
mInputChannel = new InputChannel();
//通過Binder進入systemserver程式
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
}
}
在該方法中,建立了一個InputChannel
物件,並且通過Binder進入systemserver程式,最終形成socket的客戶端。
這裡涉及到socket通訊的知識,比較重要的就是c層的socketpair
方法。
socketpair()函式用於建立一對無名的、相互連線的套接子。如果函式成功,則返回0,建立好的套接字分別是sv[0]和sv[1];這對套接字可以用於全雙工通訊,每一個套接字既可以讀也可以寫。
通過這個方法,就生成了socket通訊的客戶端和服務端:
socket服務端
儲存到system_server中的WindowState的mInputChannel;socket客戶端
通過binder傳回到遠端程式的UI主執行緒ViewRootImpl的mInputChannel;
感興趣的可以看看gityuan
對於input分析的部落格,文末有連結。
所以小結一下就是,在App程式建立了一個物件InputChannel
,通過Binder機制傳入了SystemServer
程式,也就是WindowManagerService
中。然後在WindowManagerService
中建立了一對套接字用於程式間通訊,而傳過來的InputChannel
就指向了socket
的客戶端。
然後App程式的主執行緒就會監聽這個socket客戶端,當收到訊息(輸出事件)後,回撥NativeInputEventReceiver.handleEvent()
方法,最終會走到InputEventReceiver.dispachInputEvent
方法。
dispachInputEvent
,處理輸入事件,感覺離我們熟知的事件分發比較近了。
沒錯,到此,任務已經分配到了具體的專案組,也就是我們所使用的具體APP中了。
小組中任務第一次分發(InputStage)
當任務到達了專案組,首先組內會對這個任務進行分發,這裡會涉及到第一次責任鏈分發模式
。
為什麼強調是第一次呢?因為還沒有到達我們熟知的view事件分發階段,在此之前,還會有一次事件分類的責任鏈分發工作,也就是InputStage
處理事件分發。
//InputEventReceiver.java
private void dispatchInputEvent(int seq, InputEvent event) {
mSeqMap.put(event.getSequenceNumber(), seq);
onInputEvent(event);
}
//ViewRootImpl.java ::WindowInputEventReceiver
final class WindowInputEventReceiver extends InputEventReceiver {
public void onInputEvent(InputEvent event) {
enqueueInputEvent(event, this, 0, true);
}
}
//ViewRootImpl.java
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
adjustInputEventForCompatibility(event);
QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
QueuedInputEvent last = mPendingInputEventTail;
if (last == null) {
mPendingInputEventHead = q;
mPendingInputEventTail = q;
} else {
last.mNext = q;
mPendingInputEventTail = q;
}
mPendingInputEventCount += 1;
if (processImmediately) {
doProcessInputEvents();
} else {
scheduleProcessInputEvents();
}
}
兜兜轉轉,沒想到還是到了ViewRootImpl這裡,所以ViewRootImpl
不僅負責了介面的繪製,也負責了事件分發的部分處理工作。
這裡的enqueueInputEvent
方法中,有涉及到一個QueuedInputEvent
類,這個類就是一個封裝了InputEvent的事件類,然後經過賦值呼叫到doProcessInputEvents
方法:
void doProcessInputEvents() {
// Deliver all pending input events in the queue.
while (mPendingInputEventHead != null) {
QueuedInputEvent q = mPendingInputEventHead;
mPendingInputEventHead = q.mNext;
deliverInputEvent(q);
}
}
private void deliverInputEvent(QueuedInputEvent q) {
InputStage stage;
if (stage != null) {
stage.deliver(q);
} else {
finishInputEvent(q);
}
}
abstract class InputStage {
private final InputStage mNext;
public InputStage(InputStage next) {
mNext = next;
}
public final void deliver(QueuedInputEvent q) {
apply(q, onProcess(q));
}
到這裡邏輯好像慢慢清晰了,QueuedInputEvent
是一種輸入事件,InputStage
是處理輸入事件的責任鏈,next欄位則表示責任鏈的下一個InputStage
。
那InputStage
到底幹了哪些事情呢?返回到ViewRootImpl
的setView方法再看看:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
// Set up the input pipeline.
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);
mFirstInputStage = nativePreImeStage;
mFirstPostImeInputStage = earlyPostImeStage;
}
}
可以看到在setView方法中,就把這條輸入事件處理的責任鏈拼接完成了,不同的InputStage子類,通過構造方法一個個串聯起來了,那這些InputStage到底幹了啥呢?
SyntheticInputStage
。綜合處理事件階段,比如處理導航皮膚、操作杆等事件。ViewPostImeInputStage
。檢視輸入處理階段,比如按鍵、手指觸控等運動事件,我們熟知的view事件分發就發生在這個階段。NativePostImeInputStage
。本地方法處理階段,主要構建了可延遲的佇列。EarlyPostImeInputStage
。輸入法早期處理階段。ImeInputStage
。輸入法事件處理階段,處理輸入法字元。ViewPreImeInputStage
。檢視預處理輸入法事件階段,呼叫檢視view的dispatchKeyEventPreIme方法。NativePreImeInputStage
。本地方法預處理輸入法事件階段。
小結一下,事件到達應用端的主執行緒,會通過ViewRootImpl進行一系列InputStage來處理事件。這個階段其實是對事件進行一些簡單的分類處理,比如檢視輸入事件,輸入法事件,導航皮膚事件等等。
事件分發完成後,會告知SystemServer
程式的InputDispatcher
執行緒,最終將該事件移除,完成此次事件的分發消費。
我們的view手指觸控事件就是發生在ViewPostImeInputStage
階段了,具體來看看:
final class ViewPostImeInputStage extends InputStage {
@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
return processPointerEvent(q);
}
}
}
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
boolean handled = mView.dispatchPointerEvent(event)
return handled ? FINISH_HANDLED : FORWARD;
}
//View.java
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
經過一系列分發,最終會執行到mView的dispatchTouchEvent
方法,而這個mView就是DecorView,同樣是在setView中進行賦值的,就不細說了。
至此,終於到了我們熟悉的環節,dispatchTouchEvent
方法。
大佬之間的任務整理(DecorView)
確定了任務的分類,接下來就開始組內任務討論整理了,這個階段發生在幾個大佬之間的談話,這幾個大佬分別是DecorView、PhoneWindow、Activity/Dialog
:
//DecorView.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//cb其實就是對應的Activity/Dialog
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
可以看到,從DecorView開始,事件依次經過了Activity、PhoneWindow、DecorView
。
有點奇怪哈,為啥是這樣一個順序呢?而不是直接ViewRootImpl交給Activity,再交給頂層View——DecorView?而是轉來轉去,緣起和從呢?
- 首先,為什麼ViewRootImpl不直接把事件交給Activity?
因為介面上不止Activity
一種形態呀,如果介面上存在Dialog
,而Dialog的Window屬於子Window,是可以覆蓋應用級Window的,所以總不能把事件直接交給Activity吧?都被覆蓋了,所以這時候應該把事件交給Dialog。
為了方便,我們用到了DecorView
這個角色來充當分發的第一元素,由他來找到當前介面window的所持著,所以程式碼中也是找到mWindow.getCallback()
,其實也就是對應的Activity或者Dialog。
- 其次,交給Acitivity後,為什麼不直接交給頂層View——DecorView開始分發事件呢?
因為Activity
和DecorView
之間並沒有直接關係。DecorView怎麼來的?通過setContentView被建立出來的,所以在Activity中是看不到DecorView身影的,DecorView的例項儲存在PhoneWindow中,由Window所管理。
所以Activity
的事件肯定是交給Window來管理,之前也說過PhoneWindow的指責就是幫助Activity管理View,所以事件分發交給它也是它的職責所在。而PhoneWindow
的處理方式,就是交給頂層的DecorView
來處理了。
這樣,一個事件分發的鏈條就形成了:
DecorView——>Activity——>PhoneWindow——>DecorView——>ViewGroup
交給做任務具體的人(ViewGroup)
接下來就開始分派任務了,也就是ViewGroup
的事件分發時間,這部分內容是老生常談了,最重要的就是這個dispatchTouchEvent
方法。
假設我們沒有看過原始碼,那麼事件來了,會產生多種傳遞攔截的可能,我畫了個腦圖:
其中產生的疑問就包括:
ViewGroup
是否攔截事件,攔截後怎麼處理?- 不攔截後交給
子View
或者子ViewGroup
怎麼處理? 子View
怎麼決定是否攔截?子View
攔截後怎麼處理事件?子View
不攔截事件後父元素ViewGroup
怎麼處理事件?ViewGroup
不攔截,子View
也不攔截,最終事件怎麼處理?
接下來就具體分析分析。
ViewGroup是否攔截事件,攔截後怎麼處理?
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//1
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
}
}
//2
if (!canceled && !intercepted) {
//事件傳遞給子view
}
//3
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
}
private boolean dispatchTransformedTouchEvent(View child) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
}
上述程式碼分成了三部分,分為ViewGroup
是否攔截、攔截後則不再傳遞下去,ViewGroup
攔截後的處理。
1、ViewGroup是否攔截
可以看到,初始化了一個變數intercepted
,代表viewGroup
是否攔截。
如果滿足兩個條件任意一個,才去討論ViewGroup是否攔截:
- 事件為
ACTION_DOWN
,也就是按下事件。 mFirstTouchTarget
不為null
其中mFirstTouchTarget
是個連結串列結構,代表某個子元素成功消費了該事件,所以mFirstTouchTarget為null就代表沒有子view消費事件,這個待會再細談。
當第一次進入這個方法,事件肯定就是ACTION_DOWN
,所以就進入了if語句,這時候獲取了一個叫做disallowIntercept(不允許攔截)的變數,暫且按下不表,接著看。
然後給這個intercepted賦值為onInterceptTouchEvent
方法的結果,我們可以理解為 viewGroup是否攔擷取決於onInterceptTouchEvent方法。
2、攔截後則不再傳遞
如果viewGroup攔截了,也就是intercepted
為true,自然也就不需要再往子view或者子ViewGroup進行傳遞了。
3、ViewGroup攔截後的處理
如果mFirstTouchTarget
為null,則表示沒有子View進行攔截,然後就轉向執行dispatchTransformedTouchEvent
方法,代表ViewGroup要自己再進行一次分發處理。
這裡有個問題就是為什麼不直接判斷intercepted
呢?非要去判斷這個mFirstTouchTarget
?
- 因為
mFirstTouchTarget==null
不僅代表ViewGroup要自己消費事件,也代表了ViewGroup
沒消費並且子View
也沒有去消費事件,兩種情況都會執行到這裡。
也就是ViewGroup
攔截或子View沒有攔截,都會呼叫到dispatchTransformedTouchEvent
方法,在該方法中,最後會呼叫super.dispatchTouchEvent
。
super代表ViewGroup的父類View,也就是ViewGroup會作為一個普通View執行View.dispatchTouchEvent
方法,至於這個方法具體做了什麼,待會和View的事件處理再一起看。
通過上面的分析,我們可以得出ViewGroup
攔截的虛擬碼:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean isConsume = false;
if (isViewGroup) {
if (onInterceptTouchEvent(event)) {
isConsume = super.dispatchTouchEvent(event);
}
}
return isConsume;
}
如果是ViewGroup,會先執行到onInterceptTouchEvent
方法判斷是否攔截,如果攔截,則執行父類View的dispatchTouchEvent
方法。
ViewGroup不攔截後交給子View或者子ViewGroup處理?
接著說ViewGroup
不攔截的情況,也就會傳到子View的情況:
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int childrenCount = mChildrenCount;
//1
if (newTouchTarget == null && childrenCount != 0) {
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//2
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//3
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
ViewGroup
不攔截,則intercepted
為false,那麼就會進入上述的if語句中。
同樣分為三部分來說,分別是遍歷子View,判斷事件座標,傳遞事件
1、遍歷子View
第一部分就是遍歷當前ViewGroup所有的子View。
2、判斷事件座標
然後會判斷這個事件是否在當前子View的座標內,如果使用者觸控的地方都不是當前的View自然不需要對這個view在進行分發處理,還有個條件就是當前View沒有在動畫狀態。
3、傳遞事件
如果事件座標在這個View內,就開始傳遞事件,呼叫dispatchTransformedTouchEvent方法,如果為true,就呼叫addTouchTarget方法記錄事件消費鏈。
dispatchTransformedTouchEvent方法是不是有點熟悉?沒錯,剛才也出現過,再看一遍:
private boolean dispatchTransformedTouchEvent(View child) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
}
這裡對傳進來的 child
進行了判斷,這個child
就是子View
,如果子View不為null,就呼叫這個子View的dispatchTouchEvent
方法,繼續分發事件。如果為null,就是剛才的情況,呼叫父類的dispatchTouchEvent
方法,預設為自己來消費事件。
當然,這個child有可能為viewGroup有可能為View,總之就是繼續分發呼叫子View
或者子ViewGroup
的方法。
到此,一個關於dispatchTouchEvent
的遞迴就顯現出來了:
如果某個ViewGroup無法消費事件,那麼就會傳遞給子view/子ViewGroup的dispatchTouchEvent方法,如果是ViewGroup,那麼又會重複這個操作,直到某個View/ViewGroup消費事件。
最後,如果dispatchTransformedTouchEvent
方法返回true,就代表有子view消費了事件,然後會呼叫到addTouchTarget
方法:
在該方法中,會對mFirstTouchTarget
這個單連結串列進行了賦值,記錄消費鏈(但是在單點觸控的情況下,其單連結串列的結構並沒有用上,只是作為一個普通的TouchTarget物件,待會會說到),然後就break退出了迴圈。
接下來就看看關於View內部具體處理事件的邏輯。
子View怎麼處理事件,是否攔截?
public boolean dispatchTouchEvent(MotionEvent event) {
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
其實就是兩個邏輯:
- 1、如果View設定了
setOnTouchListener
並且onTouch
方法返回true,那麼onTouchEvent
就不會被執行。 - 2、否則,執行
onTouchEvent
方法。
所以預設情況下是直接會執行onTouchEvent
方法。
關於View的事件分發我們也可以寫一段虛擬碼,並且增加了setOnClickListener
方法的呼叫:
public void consumeEvent(MotionEvent event) {
if (!setOnTouchListener || !onTouch) {
onTouchEvent(event);
}
if (setOnClickListener) {
onClick();
}
}
子View攔截後怎麼處理事件?
子View攔截後,就會給單連結串列mFirstTouchTarget
賦值。
這個剛才已經說過了。邏輯就在addTouchTarget方法中,我們來具體看看:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
final TouchTarget target;
target.child = child;
return target;
}
這個單連結串列到底怎麼連的呢?之前我們說過dispatchTouchEvent
是一個遞迴的過程,當某個子View消費了事件,那麼通過addTouchTarget
方法,就會讓mFirstTouchTarget
的child值指向那個子View,依此向上,最後就會拼接成一個類似單連結串列結構,尾節點就是消費的那個View。
為什麼說類似呢?因為mFirstTouchTarget
並沒有真正連起來,而是通過每個ViewGroup的mFirstTouchTarget
間接連起來。
打個比方,我們假設一個View樹關係:
A
/ \
B C
/ \
D E
A、B、C為ViewGroup,D、E為View。
當我們觸控的點在ViewD中,事件分發的順序就是A-C—D
。
在C遍歷D的時候,ViewD消費了事件,所以走到了addTouchTarget方法中,包裝了一個包含ViewD的TouchTarget
,我們叫它TargetD。
然後設定C的mFirstTouchTarget
為TargetD,也就是其child值為ViewD。
再返回上一層,也就是A層,因為D消費了事件,所以C的dispatchTouchEvent
方法也返回了true,同樣呼叫了addTouchTarget
方法,包裝了一個TargetC。
然後會設定A的mFirstTouchTarget
為TargetC,也就是其child值為ViewC。
最終的分發結構就是:
A.mFirstTouchTarget.child -> C
C.mFirstTouchTarget.child -> D
所以說mFirstTouchTarget
通過child找到了消費鏈的下一層View,然後下一層又繼續通過child找到下下層View,依次往下,就記錄了消費的完整路徑。
那mFirstTouchTarget
的連結串列結構用到哪了呢?多點觸控。
對於多點觸控且點選目標不同的情況,mFirstTouchTarget
才會作為連結串列結構存在,next指向上一個手指按下時建立的TouchTarget物件。
而在單點觸控情況下,mFirstTouchTarget
連結串列會蛻變成單個TouchTarget
物件:
mFirstTouchTarget.next
始終為null。mFirstTouchTarget.child
賦值為這條消費鏈的下一層View,一層層遞迴呼叫每一層的mFirstTouchTarget.child,直到消費的那個view。
最後再補充一點,每次ACTION_DOWN事件來到的時候,mFirstTouchTarget就會被重置,迎接新的一輪事件序列。
子View不攔截事件後ViewGroup怎麼處理事件?
子View不攔截事件,那麼mFirstTouchTarget
就為null,退出迴圈後,呼叫了dispatchTransformedTouchEvent
方法。
//3
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
最終呼叫了super.dispatchTouchEvent
,也就是View.dispatchTouchEvent
方法。
可以看到子View
不攔截事件和ViewGroup
攔截事件的處理是一樣的都會走到這個方法中。
那麼這個方法到底幹了什麼呢?上面說到View的處理方法dispatchTouchEvent
已經說過了,還是那段虛擬碼,只不過在這裡View是作為ViewGroup的父類。
所以,小結一下,如果所有子View
都不處理事件,那麼:
- 預設執行
ViewGroup
的onTouchEvent
方法。 - 如果設定
ViewGroup
的setOnTouchListener
,就會執行onTouch
方法。
ViewGroup不攔截,子View也不攔截,最終事件怎麼處理?
最後一點,如果ViewGroup
不攔截,子View
也不攔截,這個意思就是mFirstTouchTarget == null
的同時,dispatchTransformedTouchEvent
方法也返回false。
總之,就是所有ViewGroup的dispatchTouchEvent
方法都返回false,這時候該怎麼處理呢?返回到一開始大佬會談的時候:
//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
沒錯,如果superDispatchTouchEvent
方法返回false,那麼就會執行Activity的onTouchEvent
方法。
小結
小結一下:
-
事件分發的本質就是一個遞迴方法,通過往下傳遞,呼叫
dispatchTouchEvent
方法,找到事件的處理者,這也就是專案中常見的責任鏈模式
。 -
在消費過程中,ViewGroup的處理方法就是
onInterceptTouchEvent
-
在消費過程中,View的處理方法就是
onTouchEvent
方法。 -
如果底層View不消費,則一步步往上執行父元素的
onTouchEvent
方法。 -
如果所有View的
onTouchEvent
方法都返回false,則最後會執行到Activity的onTouchEvent
方法,事件分發也就結束了。
完整事件消費虛擬碼:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean isConsume = false;
if (isViewGroup) {
//ViewGroup
if (onInterceptTouchEvent(event)) {
isConsume = consumeEvent(event);
} else {
isConsume = child.dispatchTouchEvent(event);
}
} else {
//View
isConsume = consumeEvent(event);
}
if (!isConsume) {
//如果自己沒攔截,子View沒有消費,自己也要呼叫消費方法
isConsume = consumeEvent(event);
}
return isConsume;
}
public void consumeEvent(MotionEvent event) {
//自己消費事件的邏輯,預設會呼叫到onTouchEvent
if (!setOnTouchListener || !onTouch) {
onTouchEvent(event);
}
}
dispatchTouchEvent() + onInterceptTouchEvent() + onTouchEvent()
,大家也可以把這三個方法作為理解記憶事件分發的重點。
後續任務處理(事件序列)
終於,任務找到了它的主人,看似流程也結束了,但是還存在一個問題就是,這個任務之後的後續任務該怎麼處理呢?比如要增加某某模組功能。
不可能再走一遍公司流程吧?如果按照正常邏輯,是應該找到當初負責我們任務的那個人來繼續處理,看看Android公司
是不是這麼做的。
一個MotionEvent
事件序列一般包括:
ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL
剛才我們都說的是ACTION_DOWN
,也就是手機按下的事件處理,那麼後續的移動手機,離開螢幕事件該怎麼處理呢?
假設之前已經有一個ACTION_DOWN
並且被某個子View消費了,所以mFirstTouchTarget
會有一條完整的指向,這時候來了第二個事件——ACTION_MOVE
。
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
}
然後就會發現,ACTION_MOVE
事件根本進不去對子View
的迴圈方法,而是直接到了最後面的邏輯:
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 {
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
predecessor = target;
target = next;
}
}
如果mFirstTouchTarget
為null,就是之前說過的轉到ViewGroup自身的onTouchEvent
方法。
這裡很明顯不為null,所以走到else中,又開始遍歷mFirstTouchTarget
,之前說過單點觸控的時候,target.next
為null,target.child
為消費鏈的下一層View,所以其實就是將事件交給了下一層View。
這裡有個點很多朋友可能之前沒注意到,就是當ACTION_DOWN
的時候,走到這裡,會通過mFirstTouchTarget找到那個消費的View執行dispatchTransformedTouchEvent
。
但是這之前,遍歷View
的時候已經執行了一次dispatchTransformedTouchEvent
方法,難道這裡還要執行一次dispatchTransformedTouchEvent
方法嗎?
這不就重複了?
- 這就涉及到另一個變數
alreadyDispatchedToNewTouchTarget
。這個變數代表之前是否已經執行過一次View消費事件,當事件為ACTION_DOWN
,就會遍歷View,如果view消費了事件,那麼alreadyDispatchedToNewTouchTarget
就被賦值為true,所以到這裡也就不會再次執行了,直接handled = true
。
所以後續任務
的處理邏輯也基本明白了:
只要某個View開始處理攔截事件,那麼這一整個事件序列都只能交給它來處理。
優化任務派發流程(解決滑動衝突)
到此,任務終於是分發完成了,任務完成後,小組開了一個總結會議
:
其實任務分發過程還是有可以優化的過程,比如有些任務是不一定就只交給一個人做,比如交給兩個人做,把A擅長的任務給A做,B擅長的任務給B做,最大化利用好每個人。
但是我們之前的邏輯預設
是按下任務交給了A,後續都會交給A。所以這時候就需要設計一種機制對某些任務進行攔截。
其實這就涉及到滑動衝突
的問題了,舉例一個場景:
外面的ViewGroup
是橫向移動,而內部的ViewGroup
是需要縱向移動的,所以需要在ACTION_MOVE
的時候對事件進行判斷和攔截。(類似ViewGroup+Fragment+Recyclerview)
直接說Android公司的解決方案,兩種方案:
- 外部攔截法。
- 內部攔截法。
外部攔截法
外部攔截法比較簡單,因為不管子View是否攔截,每次都會執行onInterceptTouchEvnet
方法,所以我們就可以在這個方法中,根據自己的業務條件選擇是否攔截事件。
//外部攔截法:父view.java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
//父view攔截條件
boolean parentCanIntercept;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (parentCanIntercept) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
return intercepted;
}
邏輯很簡單,就是根據業務條件,在onInterceptTouchEvent
中決定是否攔截,因為這種方法是在父View中控制是否攔截,所以這種方法叫做外部攔截法。
但是這和我們之前的認知又衝突了,如果ACTION_DOWN
交給了子View處理,那麼後續事件應該會直接被分發給這個view呀,為什麼還能被父View攔截的?
我們再來看看dispatchTouchEvent
方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
intercepted = onInterceptTouchEvent(ev);
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
while (target != null) {
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
}
}
}
當事件為ACTION_MOVE
的時候,並且在onInterceptTouchEvent
方法返回了true,所以這裡的intercepted=true
,再到下面的邏輯,cancelChild
的值也為true,然後被傳到了dispatchTransformedTouchEvent
方法,沒錯,又是這個方法,不同的是cancelChild
子段為true。
看這個欄位的名字肯定是和取消子view事件有關的,繼續看看:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
}
看出來了麼,當第二個欄位cancel為true的時候,事件會被修改成ACTION_CANCEL
!!,然後才會被繼續傳下去。
所以就算某個View消費了ACTION_DOWN
,但是當後續事件來的同時,在父元素的onInterceptTouchEvent()
中返回true,那麼這個事件就會被修改為ACTION_CACLE
事件再傳給子View。
所以子View
再次交出了對該事件序列的控制權,這也就是外部攔截法能實現的原因。
內部攔截法
繼續看看內部攔截法:
//父view.java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
//子view.java
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
//父view攔截條件
boolean parentCanIntercept;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (parentCanIntercept) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(event);
}
內部攔截法是將主動權交給子View,如果子View需要事件就直接消耗,否則交給父容器處理。我們列舉下DOWN和MOVE兩種情況:
ACTION_DOWN
的時候,子View必須能消費,所以父View的onInterceptTouchEvent
要返回false,否則就被父View攔截了,而且後續事件也不會傳到子View這裡了。ACTION_MOVE
的時候,父View的onInterceptTouchEvent
方法要返回true,表示當子View不想消費的時候,父View能及時消費,那麼子View怎麼控制呢?可以看到程式碼設定了一個requestDisallowInterceptTouchEvent
方法,這個是幹嘛呢?
protected static final int FLAG_DISALLOW_INTERCEPT = 0x80000;
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
}
這種通過|=
和 &= ~
運算子修改引數是原始碼中常見的設定標識的方法:
|=
將標誌位設定為1&= ~
將標識位設定為0
所以在需要父元素攔截的時候就設定了requestDisallowInterceptTouchEvent(false)
方法,讓標誌位設定為0,這樣父元素就能執行到onInterceptTouchEvent方法。
具體生效程式碼就在dispatchTouchEvent
方法中:
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
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;
}
}
可以看到,如果disallowIntercept
為false,就代表父View要攔截,然後就會執行到onInterceptTouchEvent
方法,在onInterceptTouchEvent
方法中返回ture,父View成功攔截。
總結
經過拇指記者的探訪,終於把Android公司對於事件任務處理摸清楚了,希望對於螢幕前的你能有些幫助,下期再見啦。
參考
《Android開發藝術探索》
每日一問 | 事件到底是先到DecorView還是先到Window的?
Input系統—事件處理全過程
反思|Android 事件分發機制的設計與實現
View·InputEvent事件投遞原始碼分析
徹底掌握 Android touch 事件分發時序
拜拜
感謝大家的閱讀,有一起學習的小夥伴可以關注下我的公眾號——碼上積木❤️❤️
每日一個知識點,積少成多,建立知識體系架構。
這裡有一群很好的Android小夥伴,歡迎大家加入~