事件分發在Android中非常重要,在滑動沖突,下拉刷新,嵌套滑動的時候都需要非常清楚事件分發的機制,才能寫好對應的處理代碼。曾經以為我對事件分發已經很清楚了,也寫過幾篇文章,但是總感覺沒有完全說清楚,今天再從代碼的角度分析一遍事件分發機制,希望以后遇到所有事件分發的問題,都能在這里找到答案。
先看幾個問題,如果這些問題你都知道答案,那本篇文章就不用看了。
問題
1、如果攔截了某個事件,是否就會交由本view的View:dispatchTouchEvent處理?
2、一個事件,如果子view處理失敗,是否就交還給父view處理?
3、如果一個down事件,大家都不處理,會怎么樣?
4、parent把事件傳遞給哪個子view呢?是根據位置查一遍的嗎?
5、某個view成功處理down事件,但是處理move事件失敗,會如何?
神圣的規則
規則1:事件傳遞由父控件傳遞到子控件,事件消費是子控件優先。
規則2:down事件,子控件如果不消費,就還給父控件。
規則3:我是一個壞父親,父親吃到肉了,絕不會再給兒子,兒子吃到肉了,父親還可能搶。
手指從按下到抬起,我們稱為一個cycle,以DOWN事件開始,UP事件結束,里面有若干個MOVE事件。一個cycle內,v1處理了某事件,后邊的事件絕不會被v1的child處理,v1肯定會攔下來。
很多文章在介紹事件分發的時候,都會提到onTouch或者onTouchEvent,本文不會說這2個,因為這2個都是View的dispatchTouchEvent方法內,本文只會提到dispatchTouchEvent方法,這樣更準確一點。當然,其實大部分情況下,View的dispatchTouchEvent就是調用onTouchEvent,一般onTouch是沒有的,這塊的邏輯如果不清楚的話,可以看android點擊事件(View)。
down事件分發
在講述事件分發的流程前,先定義三個角色,p,pp,c其中p為主角ViewGroup,pp是p的parent,c為p的child。
手指按下就會觸發down事件。例如我們點擊了一個TextView,down事件會從activity開始傳遞,然后傳遞給DecorView,接著往下傳遞給對應的ViewGroup,一層層傳下來直到TextView。
觸摸了任何一個ViewGroup都會調用ViewGroup的dispatchTouchEvent。首先會先進入onInterceptTouchEvent,如果返回true的話,就攔截了,交由本viewgroup的View::dispatchTouchEvent方法,注意這里和前面的dispatchTouchEvent方法不一樣,一個是View的dispatchTouchEvent,一個是Viewgroup的dispatchTouchEvent。View的dispatchTouchEvent我們在android點擊事件(View)詳細說過了,而Viewgroup的dispatchTouchEvent就是負責事件分發的核心代碼,也就是我們這篇文章的主要內容,看明白了這個函數的200多行代碼,事件分發的所有問題都能明白。
結合下邊的圖,我們可以明白down事件的傳遞機制。
1、如果p的onInterceptTouchEvent返回true,那就直接攔截,交給p的View::dispatchTouchEvent處理,流程圖中的super.dispatchTouchEvent就是指View::dispatchTouchEvent,后面的流程暫時不說;
2、如果p的onInterceptTouchEvent返回false,那就不攔截,繼續查點擊到了哪個child,如果查不到,那就還是交給p的View::dispatchTouchEvent處理。如果查到了,那就交給這個child(簡稱c)的dispatchTouchEvent處理。c的dispatchTouchEvent有2種結果,true和false,如果返回false,那還是交給p的View::dispatchTouchEvent處理;如果返回了true表示c已經處理好了這個事件,那p就很開心了,小弟幫我完成了一件事,記下他的功勞,把mFirstTouchTarget進行賦值,指向c,然后p的dispatchTouchEvent返回true。
剛才交給p的View::dispatchTouchEvent處理,后面的流程還沒說。從前面的流程可以看到,走到這里有3種原因。1、onInterceptTouchEvent返回了true,攔截了;2、點擊的這個點沒有對應的child;3、c沒有處理好,返回了false。p的View::dispatchTouchEvent也只有2種結果,true或者false,如果返回了true,那整個p的dispatchTouchEvent就返回了true,但是此時mFirstTouchTarget為null,因為是自己處理的,不是child處理的。如果p的View::dispatchTouchEvent返回了false,那整個p的dispatchTouchEvent就返回false。
此時p的dispatchTouchEvent結束了,結束的時候會返回true或者false,那后面會發生什么呢?我們看這個流程圖,要有遞歸的思想。此時p: dispatchTouchEvent完成,其實和c:dispatchTouchEvent()是一樣的,要把結果告訴pp(p的parent)。
此時的狀態有3種:
狀態1:p: dispatchTouchEvent()返回true,并且p的mFirstTouchTarget空,代表是p處理了事件down
狀態 2:p: dispatchTouchEvent()返回false
狀態3:p: dispatchTouchEvent()返回true,并且p的mFirstTouchTarget非空,代表p的child處理了事件。
用mFirstTouchTarget記錄有什么好處呢?想想,如果很上層的view想知道到底誰立下了如此大功,處理了事件,順著mFirstTouchTarget找過來就行了,其實只有在down事件的時候會根據按下的位置來查找對應的子view,后面的事件都是根據mFirstTouchTarget來查找的,這樣明顯提高效率。
MOVE的事件分發
我們先回頭看下,down事件結束之后的三種狀態,其實可以合并成2種狀態。
先看狀態1,p: dispatchTouchEvent()返回true,那么pp的dispatchTouchEvent()肯定也返回true,并且pp的mFirstTouchTarget指向p,看看這個是不是和狀態3類似的,只是p換成了pp。 這種情況我們稱為case1,case1的本質是什么?有人成功處理了down事件。
再看狀態2,p: dispatchTouchEvent()返回false,會來到pp的dispatchTouchEvent()代碼內,pp的dispatchTouchEvent()可能返回true或者false,如果返回了true,那其實和case1類似了。如果還是返回false,那就繼續往上傳,只要祖宗有一個返回了true,那就掉入了case1.如果大家堅持返回false,那就會一直傳到DecorView。這種情況我們稱為case2,本質就是無人成功處理down事件。
無人成功處理down事件
無人處理down事件,比較簡單,我們先說,發生的概率也很小。沒有人處理down事件,這個事件就會一直往上拋,直到PhoneWindow$DecorView。而DecorView的onTouchEvent一般返回false,DecorView的mFirstTouchTarget為null。下一次move事件來了,直接攔截并且自己處理。所以結果就是后面的所有事件都停在了DecorView,不會下傳,而DecorView的處理結果就是false。所以這種情況下,后面的事件都不會被處理,可以認為被丟棄了。
有人成功處理down事件
假設有view族譜p1,p2,...pn,后面一個是前面一個的parent。假設p2處理了down事件,那么我們根據規則3,move事件不可能給p1,所以我們不用考慮p1。此時p2的mFirstTouchTarget為null,p3,p4等的mFirstTouchTarget非空。所以此時有2種類型的view要考慮,第一種是p2類型的,mFirstTouchTarget為null;第二種是p3,p4類型的,mFirstTouchTarget非空。
move事件的傳遞,可以分為2個階段,第一階段就是決定intecepted的值,第二階段就是根據intecepted的值進行事件分發.
MOVE事件第一階段
第一階段流程圖如下所示。
第一種情況,mFirstTouchTarget為null,intecepted直接會變為true,攔截所有事件,這就是規則3的來源。
第二種情況,mFirstTouchTarget非空,會根據disallowIntercept標志和onInterceptTouchEvent()來決定intecepted的值。
move事件第二階段
第二階段流程圖如下所示
此時可以分3個case來看
case1
先看mFirstTouchTarget為空的情況,那么他的intecepted必定是true,會調用View:dispatchTouchEvent()作為返回值
case2
若mFirstTouchTarget非空,intecepted為false,此時按理說會去找對應位置的child,NONONO。這里的邏輯和down事件不一樣,這里不會根據位置去找,而是根據mFirstTouchTarget去找,因為我們down事件的child已經記錄在mFirstTouchTarget內了,所以直接找mFirstTouchTarget就行。(其實mFirstTouchTarget其實是個鏈表,跟著鏈表爬一遍)。mFirstTouchTarget的處理結果就作為整個dispatchTouchEvent的返回結果。
case3
若mFirstTouchTarget非空,intecepted為true,他會給mFirstTouchTarget指向的view發一個cancel事件,然后mFirstTouchTarget置null,然后返回true。啊??居然不調用自己的View:dispatchTouchEvent嗎?確實是的。本次move事件,實際上不會調用自己的View:dispatchTouchEvent。但是此時view mFirstTouchTarget已經為null了,所以下一次move來的時候,走的是case2,View:dispatchTouchEvent.這一點我之前是理解錯誤的。其實這里損失了一個MOVE事件,這個MOVE事件雖然返回了true,但是其實沒有任何人處理他。
case2源碼分析
由于case2和case3的代碼我不太熟悉,所以抓出來分析一下。
先看case2,此時mFirstTouchTarget非空,在L13把事件發給child
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
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;
//在這里把事件發給child
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
case3源碼分析
再來看case3,mFirstTouchTarget非空,intecepted為true
來看這段代碼,L12因為intercepted為true,所以cancelChild為true,會走到dispatchTransformedTouchEvent,dispatchTransformedTouchEvent內部會發一個cancel事件出去,然后返回true(后邊會詳細說)。然后L18,因為cancelChild為null,所以會執行L21,把mFirstTouchTarget置null。
{
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//注意這里,因為intercepted為true,所以cancelChild也會為true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
//mFirstTouchTarget置null
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
我們再看看dispatchTransformedTouchEvent的流程,此時傳進來的cancel為true,會再9設置CANCEL事件,在L14由child發出去。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
//走這里,發一個cancel消息出去
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
//返回true
return handled;
}
UP的事件分發
UP事件其實和MOVE事件基本一致,UP事件一般不攔截。即使攔截了UP事件,也不會調用自己的View:dispatchTouchEvent.為什么?可以參考 move事件第二階段的case3,簡單來說如果攔截UP事件,此時mFirstTouchTarget非空的話,此次dispatchTouchEvent會讓child發一個cancel出去,把自己的mFirstTouchTarget置空,然后返回true,不會調用View:dispatchTouchEvent。因為只有下一個事件來臨的時候才調用View:dispatchTouchEvent,可是UP已經是最后一個事件了,所以不會發生后面的事。
偽代碼
講了這么多,我嘗試著用偽代碼寫ViewGroup的dispatchTouchEvent,其實也不麻煩,40行代碼說明了一切。
// Check for interception.
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;
}
//查找合適的子view
if(down事件&&!intercepted){
handled=某個child.dispatchTouchEvent();
if(handled){
mFirstTouchTarget賦值
}
}
if(mFirstTouchTarget==null){
//只有這種情況,parent親自處理
handled=super.dispatchTouchEvent()
}else{
if(intercepted){
mFirstTouchTarget發一個cancel事件
mFirstTouchTarget=null;
}else{
return mFirstTouchTarget.dispatchTouchEvent();
}
}
return handled;
問題答案
1、如果攔截了某個事件,是否就會交由本view的View:dispatchTouchEvent處理?
這里的攔截的意思是指在onInterceptTouchEvent里返回了true,如果攔截的只是down事件,那么必然會交給View:dispatchTouchEvent處理。如果攔截的只是MOVE事件,那么是不會交給View:dispatchTouchEvent處理的,此時只是把mFirstTouchTarget置null,下一個MOVE才會交由View:dispatchTouchEvent處理。如果攔截的只是UP事件,那就更加不可能交給View:dispatchTouchEvent處理了。
2、一個事件,如果子view處理失敗,是否就交還給父view處理?
只有mFirstTouchTarget為null,才交由parent處理。down事件肯定會給parent處理,其他就不一定了,還是看mFirstTouchTarget的值。
3、如果一個down事件,大家都不處理,會怎么樣?
這個文中說的很詳細了,不停往上拋直到DecorView,返回false,然后MOVE和UP給了DecorView處理,DecorView攔下來返回false。相當于所有事件都丟棄了。
4、parent把事件傳遞給哪個子view呢?是根據位置查一遍的嗎?
down是根據位置查的,move和up是根據mFirstTouchTarget來處理的
5、假設子view為c,c的parent為p,p的parent為pp。c成功處理了down事件,所以p的mFirstTouchTarget指向c,在p的dispatchTouchEvent過程里,c處理move失敗,參考MOVE第二階段的流程圖,可以知道p的dispatchTouchEvent返回false,然后接著pp的dispatchTouchEvent也返回false,直到DecorView,這個和第三個問題有點像
總結
本文提了3條規則,畫了三幅流程圖,寫了一段偽代碼,希望以后我遇到事件分發的問題,都能從這里找到答案。