android事件分發

事件分發在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事件的傳遞機制。


MacDown logo

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條規則,畫了三幅流程圖,寫了一段偽代碼,希望以后我遇到事件分發的問題,都能從這里找到答案。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容