TouchEvent事件分發機制全解析

boredream發表於2017-03-27

網上介紹TouchEvent分發機制的文章很多,可能有的同學看了還是不明白
這裡我會結合原始碼、畫圖、簡化程式碼結構圖、三個人買手機的類比等多個角度全面解釋
其中用三個人買手機的例子做的類比,可以讓你更具象化的直接理解整個流程

開始介紹事件分發機制之前,先簡單介紹下這個TouchEvent是什麼

安卓手機的互動,主要就是手指在螢幕上的戳戳滑滑點點
而我們的這些操作其實主要是由三種基本動作組成的:

  • 按下down
  • 移動move
  • 抬起up

安卓中把這個基礎動作叫做TouchEvent

比如
一次點選就是按下、抬起組成的
一次長按就是按下、等待、抬起組成
一次滑動操作則是,按下、移動、抬起組成

其實除此之外還有多點觸碰,游標操作等動作,這裡暫時用不到,不討論

安卓裡經常會有多個控制元件重疊,即ViewGroup包含View的情況
這個時候點選到子View時,其實也是同時點到ViewGroup這個父控制元件的,那是把這個點選事件分給Parent呢還是Child呢?
這裡我們就要了解下安卓中的TouchEvent事件分發機制啦

TouchEvent的分發傳遞主要涉及到三個核心方法

  • dispatchTouchEvent 分發Touch事件
  • onInterceptTouchEvent 攔截Touch事件
  • onTouchEvent 處理Touch事件

其中
onInterceptTouch是ViewGroup的方法。View中則沒有該方法
dispatchTouchEvent在View和ViewGroup中有不同的實現,後面會展開介紹


那麼在多層結構中TouchEvent到底怎麼傳遞呢?
這仨方法用處和呼叫順序是什麼呢?

下面我們來擼個Demo實踐下~
【例一】
倆ViewGroup和一個View,方法全部預設不修改~

TouchEvent事件分發機制全解析
巢狀佈局

則當點選到Child上時,Touch事件的相關方法呼叫順序就是

grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
— parent dispatchTouchEvent ACTION_DOWN
— parent onInterceptTouchEvent ACTION_DOWN
— — child dispatchTouchEvent ACTION_DOWN
— — child onTouchEvent ACTION_DOWN
— parent onTouchEvent ACTION_DOWN
grandpa onTouchEvent ACTION_DOWN

為什麼是這樣一個從父級到子級再到父級的U型順序呢?
其實看原始碼就知道啦,核心在於ViewGroup的dispatchTouchEvent方法
為了方便理解,我們縮減下程式碼,如下

boolean dispatchTouchEvent() {
    // 是否攔截
    boolean intercepted = onInterceptTouchEvent();

    if(!intercepted) {
        // 如果不攔截遍歷所有child,判斷是否有分發
        boolean handled;
        if (child == null) {
            // 等同於handled = onTouchEvent()
            handled = super.dispatchTouchEvent();
        } else {
            // 如果有child,再呼叫child的分發方法
            handled = child.dispatchTouchEvent();
        }

        if(handled) {
            touchTarget = child;
            break;
        }   
    }

    if(touchTarget == null) {
        // 如果所有child中都沒有消費掉事件
        // 那麼就把自己作為沒child的普通View
        handled = super.dispatchTouchEvent();
    }

    return handled;
}複製程式碼

方法的作用是將螢幕點選事件向下(子一級)傳遞到目標控制元件上,或者傳遞給自己,如果自己就是目標的話

如果事件被(自己或者下面某一層的子控制元件)處理掉了的話,就返回true,否則返回false

那問題來了,如果我沒有child了,或者我就是一個View,那我的dispatchTouchEvent返回值要如何獲取呢?
這種情況下就會使用父類的dispatchTouchEvent方法,
也就是呼叫View類中的實現,簡化程式碼如下

boolean dispatchTouchEvent() {
    // 實質上就是呼叫onTouchEvent用其返回值
    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;
}複製程式碼

由此可見,只要是enable=false或者沒有設定過touchListener, 那麼他一定會呼叫onTouchEvent,且dispatchTouchEvent的返回值就是onTouchEvent的返回值

這樣看原始碼可能還是不太理解U型順序
那我們把程式碼也按照上面的三層結構巢狀起來,就很好理解了,如下

TouchEvent事件分發機制全解析
例一

其中super.dispatchTouchEvent實際上就是呼叫了onTouchEvent方法,同時使用其返回值~
通過上圖上的原始碼執行順序就知道為什麼日誌會這樣輸出了

  1. grandpa dispatchTouchEvent ACTION_DOWN
  2. grandpa onInterceptTouchEvent ACTION_DOWN
  3. — parent dispatchTouchEvent ACTION_DOWN
  4. — parent onInterceptTouchEvent ACTION_DOWN
  5. — — child dispatchTouchEvent ACTION_DOWN
  6. — — child onTouchEvent ACTION_DOWN
  7. — parent onTouchEvent ACTION_DOWN
  8. grandpa onTouchEvent ACTION_DOWN

dispatchTouchEvent分發的方法我們大概瞭解了,
onInterceptTouchEvent攔截方法是做什麼用的呢?

該方法用於攔截事件向下分發
當返回值為true時,就會攔截TouchEvent不再向下傳遞,直接交給自己的onTouchEvent方法處理。返回false則不攔截。

再做個試驗
【例二】
把例一中的Parent層的onInterceptTouchEvent返回值改為true。
執行一下,點View,看下輸出結果:

grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
— parent dispatchTouchEvent ACTION_DOWN
— parent onInterceptTouchEvent ACTION_DOWN
— parent onTouchEvent ACTION_DOWN
grandpa onTouchEvent ACTION_DOWN

即當事件一層層向下傳遞到parent時,被他就攔截了下來然後自己消費使用。
再看一下原始碼中的執行順序原理,如下圖

TouchEvent事件分發機制全解析
例二

intercepted為true~ 沒有進入條件,也就是圖片裡X的地方~
就跳過了child.dispatchTouchEvent的向下事件分發了


最後還剩個onTouchEvent方法
方法的主體內容其實是處理具體操作邏輯的,是產生一次點選還是一次橫縱向的滑動等

而他的返回值才會影響整個事件分發機制,
其意義在於通知父級的ViewGroup們是否已經消費找到目標Target了

同樣,再試驗一下
【例三】
只把例一中的Parent的TouchEvent返回值改為true。攔截方法不變
點一下View,則輸出日誌為

grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
— parent dispatchTouchEvent ACTION_DOWN
— parent onInterceptTouchEvent ACTION_DOWN
— — child dispatchTouchEvent ACTION_DOWN
— — child onTouchEvent ACTION_DOWN
— parent onTouchEvent ACTION_DOWN

grandpa dispatchTouchEvent ACTION_UP
grandpa onInterceptTouchEvent ACTION_UP
— parent dispatchTouchEvent ACTION_UP
— parent onTouchEvent ACTION_UP

暫時先看Down的邏輯,對應的原始碼執行順序如下

TouchEvent事件分發機制全解析
例三

Down部分和例一的前7步流程都是一樣的
但是例三原始碼圖片中7的地方,
parent呼叫super.dispatchTouchEvent實際上是呼叫了onTouchEvent方法,
這裡因為我們修改成了true,所以dispatchTouchEvent最終也返回true。

所以返回到grandpa中,touchTarget 就非空了,
因此grandpa的onTouchEvent也沒有執行~

Up部分我們後面再解釋~

到這裡我們就可以看出來
事件一旦被某一層消費掉,其它層就不會再消費了


好了,到這裡其實對事件分發的機制就有個大概瞭解了
看了原始碼也知道里面的原理是怎麼回事

但是
為什麼例一二中沒有Up,而例三中有呢?
為什麼Up和Down的順序不同呢?
為什麼順序是這樣一個U型的呢?
看的我雲裡霧裡的,光看原始碼和簡單的demo還是太抽象了啊

為了方便理解,我們先來個具體事件的類比
事件的消費,就類似我們用了一個機會券,然後用它去買了一個手機
而事件的傳遞,就類似於這個機會券在不同朋友直接的流通傳遞

下面開始描述下這個傳遞的具體過程
有三個人ABC,之間的關係是A和B認識,B和C認識,但A和C不認識
某天A接到別人給它的一張購買iphone8的機會券,用它才有資格買手機

拿例一做比較物件,下面開始整個類比流程~

  1. A首先接到了這個資訊,然後準備開始思考下這個劵的歸屬
    (grandpa呼叫dispatchTouchEvent開始分發)

  2. A先想了一下是交給其他人呢?還是自己先用掉這個劵呢
    (grandpa呼叫onInterceptTouchEvent判斷是否攔截)

  3. A尋思暫時不攔截了吧,然後把劵給了B,讓他去處理下這張劵
    (grandpa不攔截,呼叫child.dispatchTouchEvent)

  4. B拿到劵後第一反應也是,我要自己用還是問有沒有朋友要呢?
    (parent呼叫onInterceptTouchEvent判斷是否攔截)

  5. B也有點糾結,算了先問問有沒有其他朋友要用吧,就給了C
    (parent不攔截,呼叫child.dispatchTouchEvent給C分發)

  6. C拿到劵,額我沒朋友,那就不問誰了,那我自己要不要用呢?
    不用了最新窮~消費不起,那還給B吧。
    (child的分發就是看自己消費與否,返回false給B)

  7. B一看,不要啊~ 那我自己要不要消費呢?還是不了,還給A吧
    (parent呼叫super.dispatchTouchEvent,返回false給A)

  8. A拿回了轉了一圈的劵,我手機也沒壞啊也不買了~
    (grandpa呼叫super.dispatchTouchEvent,返回false)

上面就是例一中1~8步驟的情況,所以最終輸出的日誌就是

grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
— parent dispatchTouchEvent ACTION_DOWN
— parent onInterceptTouchEvent ACTION_DOWN
— — child dispatchTouchEvent ACTION_DOWN
— — child onTouchEvent ACTION_DOWN
— parent onTouchEvent ACTION_DOWN
grandpa onTouchEvent ACTION_DOWN

所有人都不消費劵,沒分發出去。
其中步驟6 7 8中都呼叫了super.dispatchTouchEvent方法,上面我們介紹過,
這個方法內部實際上是呼叫的onTouchEvent方法~
所以最後的輸出日誌順序就是從父到子依次呼叫分發和攔截,然後從子到父依次呼叫消費。

而例二也是同理,區別在於
當B拿到券的時候,選擇了攔截下來不再詢問其他朋友了,
但是B又發現自己比較窮,所以也沒消費,直接又還回給了A,
A同樣也不想要新手機也沒有消費這個劵~
所以最終的順序就是,從A到B再返回A就結束了,沒有經過C

例三的情況就不太一樣了
當A->B->C傳遞到C時,C不消費又返回給了B,B一想別浪費了吧,決定消費掉了劵~
相當於B這個parent呼叫了onTouchEvent消費方法,返回了true也就是用掉了它,
然後反饋給A說那個券我用了,就等於parent.dispatchTouchEvent返回true給上一級的A了,
A聽到訊息後哦了一下~都用掉了,那自己也不用再去考慮用不用的事了
也就是A不會再呼叫grandpa.onTouchEvent方法了

到這裡再回頭看dispatchTouchEvent返回值的作用就更明確了
它的返回值其實是用於標誌這個事件是否“用掉了”,
無論是我自己或者下面的子一級用掉了都算是用掉~

再比如這個例子中,如果我們讓C消費掉事件,
那麼B收到C的訊息後,也會呼叫parent.dispatchTouchEvent返回true給A,
所以這個方法返回值的true是隻要用掉就行,無論自己還是下面某一級,
而非我把事件傳遞下去就是true了,下面沒人用最終其實還是返回false的

好了,先總結一下

  1. dispatchTouchEvent方法內容裡處理的是分發過程。可以理解為從A->B->C一層層分發的動作
    dispatchTouchEvent的返回值則代表是否將事件分發出去用掉了,自己用或者給某一層子級用都算分發成功。比如B把券用了,或者他發出去給的C把券用了,這兩種情況下B的dispatchTouchEvent都會返回true給A
  2. onInterceptTouchEvent會在第一輪從父到子的時候在分發時呼叫,以它去決定是否攔截掉此事件不再向下分發。如果攔截下來,就會呼叫自己的onTouchEvent處理;如果不攔截,則繼續向下傳遞
  3. onTouchEvent代表消費掉事件。方法內容是具體的事件處理方法,如何處理點選滑動等。
    onTouchEvent的返回值則代表對上級的反饋,通知這個東西我用掉啦,然後他的父級就會讓分發方法也返回true

舉了這個例子主要是為了說明分發、攔截、消費的流程,可以更具象化的理解,
這樣我們再去用它去解釋為什麼例一、二中沒有Up,而例三中有就更容易了

還是做個類比
我們的這個買手機其實是一套流程,用券之後還要支付餘下的費用~
用券只是第一步,類似於Down
而支付餘下的費用就類似於Up
結合到一起才是一個完整的行為
類似於一個Down+一個Up才是一次完整的點選

前倆例子裡為什麼沒有Up呢,很好理解,
機會券啊!我都沒用券呢沒購買資格啊,有錢也沒用啊!!!

所以例一二中既然沒人用券,那自然也就不用考慮後續的購買行為了,因此只有Down,沒Up

而一旦有人消費了,那後續的事件也就會來了
好,我們拿例三做類比,B消費掉了這個券
那麼現在第二輪來了,銷售員帶著手機先跑來找A,聽說有人要買是誰是誰~

  1. 這個流程依然是先從A開始分配
    (grandpa.dispatchTouchEvent)

  2. A這個時候其實還可以不告訴銷售員誰買的~
    (grandpa.onInterceptTouchEvent 判斷是否攔截)

  3. 但是A還是沒攔下來,告訴銷售員是B買的
    (grandpa不攔截,然後呼叫child.dispatchTouchEvent)

  4. 銷售員找到了B,B說沒誰了,就是我了
    (parent沒有呼叫攔截方法)
    然後B付錢結賬尾款,完成了整個行為
    (parent呼叫onTouchEvent返回true消費掉事件)

所以在例三中的Up順序就是

grandpa dispatchTouchEvent ACTION_UP
grandpa onInterceptTouchEvent ACTION_UP
— parent dispatchTouchEvent ACTION_UP
— parent onTouchEvent ACTION_UP

這次有了目標,所以不用再來個U型迴圈了,直接定位到目標B然後結束~
那麼這個目標是怎麼個處理機制呢,我們會在後面詳細解釋~


回到例三,其實這裡有個地方可以做點手腳的
就是在售貨員上門找A的時候,A可以不告訴售貨員B在哪~攔截下來

這次我們在例三的基礎上進行修改,再整個試驗
【例四】
在grandpa類的onInterceptTouchEvent中新增個判斷,
如果動作是UP就return true攔截掉,DOWN則不攔截和之前一樣

run下程式碼,看下輸出日誌

grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
— parent dispatchTouchEvent ACTION_DOWN
— parent onInterceptTouchEvent ACTION_DOWN
— — child dispatchTouchEvent ACTION_DOWN
— — child onTouchEvent ACTION_DOWN
— parent onTouchEvent ACTION_DOWN

grandpa dispatchTouchEvent ACTION_UP
grandpa onInterceptTouchEvent ACTION_UP

— parent dispatchTouchEvent ACTION_CANCEL
— parent onTouchEvent ACTION_CANCEL

前面Down行為和例三一樣,後面就不同了
UP流程變了,然後多了個CANCEL的動作
這裡我們可以理解為

  1. 售貨員找到A問誰用的劵啊
    (grandpa呼叫dispatchTouchEvent分發UP事件)

  2. A說我不告訴你!你就留我這吧!我得不到的(沒券沒資格買)別人也別想得到!!!
    (grandpa呼叫onInterceptTouchEvent返回true,攔截UP)

  3. 然後A告訴B,別等了孫砸!你的券沒用啦!!!!
    (parent呼叫dispatchTouchEvent分發CANCEL動作)

  4. 然後B也不用再考慮是否消費了,劵丟了吧~
    (parent使用CANCEL動作呼叫onTouchEvent方法,結束)

當然,一般某層要用到事件時都會第一輪向下分發就攔截下來,然後用掉
所以例子三的情況比較少,不會那麼無私的先問完所有朋友再考慮自己

而例四的情況也比較少,你要不用就一直不用,要用就直接攔截使用,
一般不會開始說不用~ 後來第二輪的時候又攔腰一刀大家一起死吧!!!的這麼賤~


到這裡其實大概也就瞭解的差不多了,還剩一個TouchTarget目標的概念,
為什麼例三中Up和Down流程不同?
我們再回頭去看完整點的原始碼~ 這次雖然也是省略程式碼,但是比之前的完善點

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 1.每次起始動作就重置之前的TouchTarget等引數
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            // 2.如果是起始動作才攔截,或者已經有人消費掉了事件,再去判斷攔截
            // 起始動作是第一次向下分發的時候,每個view都可以決定是否攔截,然後進一步判斷是否消費,很好理解
            // 如果有人消費掉了事件,那麼也攔截~ 就像例四中的情況,也可以再次判斷是否攔截的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 3.這裡可以設定一個disallowIntercept標誌,如果是true,就是誰收到事件後都不準攔截!!!
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {
            // 4.如果未攔截,只有Down動作才去子一級去找目標物件~
            // 因為找目標這個操作只有Down中才會處理
            if (actionMasked == MotionEvent.ACTION_DOWN ) {
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        newTouchTarget = getTouchTarget(child);
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                    }
                }
            }
        }

        if (mFirstTouchTarget == null) {
            // 5.把自己當做目標,去判斷自己的onTouchEvent是否消費
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 6.如果有人消費掉了事件,找出他~
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                // 7.消費物件資訊其實是一個鏈式物件,記載著一個一個傳遞的人的資訊,遍歷呼叫它child的分發方法
                final TouchTarget next = target.next;
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
                target = next;
            }
        }
    }

    return handled;
}複製程式碼

注意,有一個dispatchTransformedTouchEvent方法,內部簡化程式碼為

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    return handled;
}複製程式碼

其實就是判斷如果沒child了(是ViewGroup但是沒子控制元件,或者自己就是View),
如果沒child,就呼叫View的dispatchTouchEvent方法,
實質就是呼叫onTouchEvent判斷是否消費掉事件
如果有child,就呼叫child的dispatchTouchEvent將事件一層層向下分發

例一二其實只用看之前的最簡化原始碼就理解了~
我們這裡用這個比較完善的原始碼分析解釋例三四中的複雜情況
其中關鍵主要在於多了一個TouchTarget的處理

其實我們在處理事件的時候,會在第一輪Down的時候先定位到目標,是誰消費了
然後在後續的Move、Up中,利用之前定位的資訊更方便的找到目標,直接處理

從上面的原始碼中註釋2程式碼的位置我們可以看出來,
第一次Down的時候我們才會去判斷是否攔截,或者有目標的時候才攔截
因為第一次傳券的時候可以攔截,而如果沒人用券也就是沒有目標那第二輪就不用攔截了,都買不了手機

如果有人消費呢,比如例三中parent消費掉了事件
那麼上面原始碼就會在Down時,進入到註釋4程式碼的位置,去child一層層找到目標,
當找到某層onTouchEvent返回true消費掉事件的物件後,就會呼叫addTouchTarget記錄下這個目標
那麼第二輪UP到來時,就會進入註釋2程式碼條件,再判斷是否攔截,例三中是不做攔截
再往下執行,因為不是Down,所以不會進入註釋4程式碼的判斷條件
到最後,就會在註釋5和6程式碼中二選一,例三裡是B消費了,有目標,所以進入條件6,
然後在註釋7程式碼處用dispatchTransformedTouchEvent方法,將Up直接向下層層傳遞給目標

向下傳遞的核心主要是在於dispatchTransformedTouchEvent方法
第一輪動作的Down時,只要不攔截,就會在註釋4程式碼處遍歷所有child呼叫該方法層層傳遞下去
而後續其他動作時,就會進入註釋6程式碼條件,然後遍歷TouchTarget中的資訊用該方法層層分發

但是要注意不要誤解
第一次Down的時候會for迴圈所有child,因為A可能有多個朋友B1、B2、B3。。。他會挨個問誰要券啊~
所以第二輪Up的時候也會while(target.next)的迭代迴圈挨個判斷~但是next是遍歷同級,不是子級
dispatchTrancformTouchEvent(target.child)這裡的.child才是向子一級一層一層分發傳遞的地方

這個TouchTarget物件,主要儲存的是傳遞路線資訊,它是一個鏈式結構
不過這個路線不是A->B->C的一個單子,而是ABC每個人都會儲存一個向下的路線資訊

比如例子三中B用了券,反饋給了A~ 那麼A這裡就會儲存一個A->B的資訊,就是從我這裡去找目標B
如果把例一中修改成C消費掉事件,那麼A就會儲存一個A->B,然後B中還會儲存一個B->C的資訊,
這樣銷售員來找A的時候,如果A不攔截,就會順著A->B的資訊找到B,再順著B手裡的B->C資訊找到C
當找到最後一個物件的時候,發現C手裡沒有下一個目標的路線資訊了,那你就是目標沒跑了~

Cancel部分就不解釋了,dispatchTrancformTouchEvent中會判斷,如果cancel=true動作,
則會把動作改成ACTION_CANCEL一層一層的傳下去~
其他還有一些不攔截標誌、id什麼的設定細節就不介紹了,下面可以自己閱讀下原始碼鞏固完善下,
當然我暫時也沒達到每一行程式碼都完全掌握的地步,如果文章有不合適的地方歡迎指正和共同討論~

最後宣傳一下個人的Github賬號,有多個不錯的開源專案喲~
歡迎follow我和star程式碼~
github.com/boredream

相關文章