TouchEvent事件分發機制全解析

網上介紹TouchEvent分發機制的文章不少,可能有的同窗看了仍是不明白
這裏我會結合源碼、畫圖、簡化代碼結構圖、三我的買手機的類比等多個角度全面解釋
其中用三我的買手機的例子作的類比,可讓你更具象化的直接理解整個流程java

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

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

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

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

好比
一次點擊就是按下、擡起組成的
一次長按就是按下、等待、擡起組成
一次滑動操做則是,按下、移動、擡起組成 ide

其實除此以外還有多點觸碰,光標操做等動做,這裏暫時用不到,不討論源碼分析

安卓裏常常會有多個控件重疊,即ViewGroup包含View的狀況
這個時候點擊到子View時,其實也是同時點到ViewGroup這個父控件的,那是把這個點擊事件分給Parent呢仍是Child呢?
這裏咱們就要了解下安卓中的TouchEvent事件分發機制啦佈局

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

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

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


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

下面咱們來擼個Demo實踐下~
【例一】
倆ViewGroup和一個View,方法所有默認不修改~

嵌套佈局

則當點擊到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型順序
那咱們把代碼也按照上面的三層結構嵌套起來,就很好理解了,以下

例一

其中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時,被他就攔截了下來而後本身消費使用。
再看一下源碼中的執行順序原理,以下圖

例二

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的邏輯,對應的源碼執行順序以下

例三

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

舉了這個例子主要是爲了說明分發、攔截、消費的流程,能夠更具象化的理解,
這樣咱們再去用它去解釋爲何例1、二中沒有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可能有多個朋友B一、B二、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

相關文章
相關標籤/搜索