Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android事件分發

android事件分發

編輯:關於Android編程

事件分發在Android中非常重要,在滑動沖突,下拉刷新,嵌套滑動的時候都需要非常清楚事件分發的機制,才能寫好對應的處理代碼。曾經以為我對事件分發已經很清楚了,也寫過幾篇文章,但是總感覺沒有完全說清楚,今天再從代碼的角度分析一遍事件分發機制,希望以後遇到所有事件分發的問題,都能在這裡找到答案。
先看幾個問題,如果這些問題你都知道答案,那本篇文章就不用看了。

問題

1、如果攔截了某個事件,是否就會交由本view的View:dispatchTouchEvent處理?
2、一個事件,如果子view處理失敗,是否就交還給父view處理?
3、如果一個down事件,大家都不處理,會怎麼樣?
4、parent把事件傳遞給哪個子view呢?是根據位置查一遍的嗎?

神聖的規則

規則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。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPrjVssW9u7j4cLXEVmlldzo6ZGlzcGF0Y2hUb3VjaEV2ZW50tKbA7aOsuvPD5rXEwfezzLu5w7vLtaGjtNPHsMPmtcTB97PMv8nS1L+0tb2jrNfftb3V4sDv09Az1tbUrdLyoaMxoaJvbkludGVyY2VwdFRvdWNoRXZlbnS3tbvYwct0cnVlo6zAub3YwcujuzKhorXju/e1xNXiuPa148O709C21NOmtcRjaGlsZKO7M6GiY8O709C0psDtusOjrLe1u9jBy2ZhbHNloaNwtcRWaWV3OjpkaXNwYXRjaFRvdWNoRXZlbnTSsta709Ay1ta94bn7o6x0cnVlu/LV32ZhbHNlo6zI57n7t7W72MHLdHJ1ZaOsxMfV+7j2cLXEZGlzcGF0Y2hUb3VjaEV2ZW50vs23tbvYwct0cnVlo6y1q8rHtMvKsW1GaXJzdFRvdWNoVGFyZ2V0zqpudWxso6zS8s6qysfX1Ly6tKbA7bXEo6yyu8rHY2hpbGS0psDttcSho8jnuftwtcRWaWV3OjpkaXNwYXRjaFRvdWNoRXZlbnS3tbvYwctmYWxzZaOsxMfV+7j2cLXEZGlzcGF0Y2hUb3VjaEV2ZW50vs23tbvYZmFsc2WhozwvcD4NCjxwPrTLyrFwtcRkaXNwYXRjaFRvdWNoRXZlbnS94cr4wcujrL3hyvi1xMqxuvK74be1u9h0cnVlu/LV32ZhbHNlo6zEx7rzw+a74beiyfrKssO0xNijv87Sw8e/tNXiuPbB97PMzbyjrNKq09C13bnptcTLvM/roaO0y8qxcDogZGlzcGF0Y2hUb3VjaEV2ZW50zeqzySzG5Mq1us1jOmRpc3BhdGNoVG91Y2hFdmVudCgpysfSu9H5tcSjrNKqsNG94bn7uObL33Bwo6hwtcRwYXJlbnSjqaGjPC9wPg0KPHA+tMvKsbXE17TMrNPQM9bWOjxiciAvPg0K17TMrDGjunA6IGRpc3BhdGNoVG91Y2hFdmVudCgpt7W72HRydWWjrLKix9JwtcRtRmlyc3RUb3VjaFRhcmdldL/Vo6y0+rHtysdwtKbA7cHLysK8/mRvd248YnIgLz4NCte0zKwgMqO6cDogZGlzcGF0Y2hUb3VjaEV2ZW50KCm3tbvYZmFsc2U8YnIgLz4NCte0zKwzOnA6IGRpc3BhdGNoVG91Y2hFdmVudCgpt7W72HRydWWjrLKix9JwtcRtRmlyc3RUb3VjaFRhcmdldLfHv9UstPqx7XC1xGNoaWxktKbA7cHLysK8/qGjPC9wPg0KPHA+08NtRmlyc3RUb3VjaFRhcmdldLzHwrzT0Mqyw7S6w7SmxNijv8/rz+ujrMjnufu63MnPsuO1xHZpZXfP69aqtcC1vbXXy63Bos/CwcvI57TLtPO5pqOstKbA7cHLysK8/qOsy7PXxW1GaXJzdFRvdWNoVGFyZ2V01dK5/cC0vs3Q0MHLo6zG5Mq11rvT0NTaZG93bsrCvP61xMqxuvK74bj5vt2wtM/CtcTOu9bDwLSy6dXSttTTprXE19N2aWV3o6y688PmtcTKwrz+trzKx7j5vt1tRmlyc3RUb3VjaFRhcmdldMC0sunV0rXEo6zV4tH5w/fP1MzhuN/Qp8LKoaM8L3A+DQo8aDEgaWQ9"move的事件分發">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來處理的

總結

本文提了3條規則,畫了三幅流程圖,寫了一段偽代碼,希望以後我遇到事件分發的問題,都能從這裡找到答案。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved