Android Touch事件

1. 五造輪子

1.1. 一造:直接傳給目標Viewbash

咱們先實現一個最簡單的需求:Activity 中有一堆層層嵌套的 View,有且只有最裏邊那個 View 會消費事件 (黃色高亮 View 表明能夠消費事件,藍色 View 表明不消費事件)微信

思考方案:框架

  1. 首先事件從哪兒來,確定得從父親那來,由於子View被包裹在裏面,沒有直接與外界通訊的辦法,而實際中Activity鏈接着根ViewDecorView,它是通往外界的橋樑,能接收到屏幕硬件發送過來的觸摸事件
  2. 因此事件是從Activity開始,通過一層一層 ViewGroup ,傳到最裏邊的 View
  3. 這時只須要一個從外向裏傳遞事件的passEvent(ev)方法,父親一層層往裏調,能把事件傳遞過去,就完成了需求

示意圖ide

open class MView {
    open fun passEvent(ev: MotionEvent) {
        // do sth
    }
}
    
class MViewGroup(private val child: MView) : MView() {
    override fun passEvent(ev: MotionEvent) {
        child.passEvent(ev)
    }
}
複製代碼
  1. 暫時把Activity當成MViewGroup處理也沒有問題
  2. 爲何是MViewGroup繼承MView而不是反過來,由於 MView 是不須要 child 字段的

1.2. 二造:從裏向外傳給目標View佈局

而後咱們增長一條需求,讓狀況複雜一點:Activity中有一堆層層嵌套的View,有好幾個疊着的View能處理事件post

同時須要增長一條設計原則:用戶的一次操做,只能被一個View真正處理(消費)ui

  1. 要求這條原則是爲了讓操做的反饋符合用戶直覺
  2. 很容易理解,正常狀況下人只會想一次就作一件事
    1. 好比一個列表條目,列表能夠點擊進入詳情,列表上還有個編輯按鈕,點擊能夠編輯條目
      1. 這是一個上下兩個View都能點擊的場景,但用戶點一個地方,確定只想去作一件事,要麼進入詳情,要麼是編輯條目,若是你點編輯結果跳了兩個頁面,那確定是不合適的
    2. 再好比在一個可點擊Item組成的列表裏(好比微信的消息界面),Item能夠點擊進入某個聊天,列表還能滑動上下查看
      1. 若是你讓Item和列表都處理事件,那在你滑動的時候,你可能得跳一堆你不想去的聊天頁面

若是使用第一次試造的框架,要遵照這條原則,就須要在每個能夠處理事件的View層級,判斷出本身要處理事件後,不繼續調用child的passEvent()方法了,保證只有本身處理了事件。 但若是真這樣實現了,在大部分場景下會顯得怪怪的,由於處理事件的順序不對:this

  1. 好比仍是上面的列表,當用戶點擊按鈕想編輯條目的時候,點擊事件先傳到條目,若是你在條目中判斷須要事件,而後把事件消費了不傳給子View,用戶就永遠點不開編輯條目了
  2. 並且換個角度看更加明顯,用戶確定但願點哪,哪兒最靠上、離手指最近的東西被觸發

因此實現新增需求的一個關鍵是:找到那個適合處理事件的View,而咱們經過對業務場景進行分析,獲得答案是:那個最裏面的View適合處理事件spa

這就不能是等parent不處理事件了才把事件傳給child,應該反過來,你須要事件的處理順序是從裏向外:裏邊的child不要事件了,才調用parent的passEvent()方法把事件傳出來。 因而得加一條向外的通道,只能在這條向外的通道上處理事件,前面向裏的通道什麼都不幹,只管把事件往裏傳。 因此這時你有了兩條通道,改個名字吧,向裏傳遞事件是passIn()方法,向外傳遞並處理事件是passOut()方法。設計

示意圖

open class MView {
    var parent: MView? = null

    open fun passIn(ev: MotionEvent) {
        passOut(ev)
    }

    open fun passOut(ev: MotionEvent) {
        parent?.passOut(ev)
    }
}

class MViewGroup(private val child: MView) : MView() {
    init {
        child.parent = this // 示意寫法
    }

    override fun passIn(ev: MotionEvent) {
        child.passIn(ev)
    }
}
複製代碼

這段代碼沒有問題,很是簡單,可是它對需求意圖的表達不夠清晰,增長了框架的使用難度

  1. 如前所述,咱們但願passIn()的時候只傳遞事件,但願在passOut()的時候每一個View決定是否要處理事件,並進行處理,並且在處理事件後,再也不調用parent的passOut()方法把事件傳出來
  2. 你會發現,這其中包含了兩類職責:
    1. 一類是事件傳遞控制邏輯,另外一類是事件處理鉤子
    2. 其中事件傳遞控制邏輯基本不會變化,但事件處理的鉤子中可能作任何事情
  3. 咱們須要把不一樣職責的代碼分開,更須要把變化的和不變的分開,減小框架使用者的關注點

因而咱們用一個叫作dispatch()的方法單獨放事件傳遞的控制邏輯,用一個叫作onTouch()的方法做爲事件處理的鉤子,並且鉤子有一個返回值,表示鉤子中是否處理了事件:

open class MView {
    open fun dispatch(ev: MotionEvent): Boolean {
        return onTouch(ev)
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false
    }
}

class MViewGroup(private val child: MView) : MView() {
    override fun dispatch(ev: MotionEvent): Boolean {
        var handled = child.dispatch(ev)
        if (!handled) handled = onTouch(ev)

        return handled
    }

    override fun onTouch(ev: MotionEvent): Boolean {
        return false
    }
}
複製代碼

這樣寫完,整個行爲其實沒有變化,但你會發現:

  1. 控制邏輯集中在dispatch()中,一目瞭然
  2. onTouch()單純是一個鉤子,框架使用者只須要關心這個鉤子和它的返回值,不用太關心控制流程
  3. 另外,連parent也不須要了

1.3. 三造:區分事件類型

上文的實現看上去已經初具雛形了,但其實連開始提的那條原則都沒實現完,由於原則要求一次操做只能有一個 View 進行處理,而咱們實現的是一個觸摸事件只能有一個View進行處理。 這裏就涉及到一次觸摸操做和一個觸摸事件的區別:

  1. 假設尚未觸摸事件的概念,咱們要怎麼區分一次觸摸操做呢?
    1. 把觸摸操做細分一下,大概有按下動做、擡起動做、與屏幕接觸時的移動和停留動做
    2. 很容易想到,要區分兩次觸摸操做,能夠經過按下和擡起動做進行區分,按下動做開始了一次觸摸操做,擡起動做結束了一次觸摸,按下和擡起中間的移動和停留都屬於這一次觸摸操做,至於移動和停留是否要區分,目前沒有看到區分的必要,能夠都做爲觸摸中來處理
  2. 因而在一次觸摸操做中就有了三種動做的類型:DOWN/UP/ING,其中ING有點不夠專業,改個名字叫MOVE吧
  3. 而每一個觸摸動做會在軟件系統中產生一個一樣類型的觸摸事件
  4. 因此最後,一次觸摸操做就是由一組從DOWN事件開始、中間是多個MOVE事件、最後結束於UP事件的事件流組成

因而設計原則更確切地說就是:一次觸摸產生的事件流,只能被一個View消費

在上次試造的基礎上把一個事件變成一個組事件流,其實很是簡單:處理DOWN事件時跟前面處理一個事件時同樣,但須要同時記住DOWN事件的消費對象,後續的MOVE/UP事件直接交給它就好了

open class MView {
    open fun dispatch(ev: MotionEvent): Boolean {
        return onTouch(ev)
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false
    }
}

class MViewGroup(private val child: MView) : MView() {
    private var isChildNeedEvent = false

    override fun dispatch(ev: MotionEvent): Boolean {
        var handled = false
        
        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            clearStatus()
        
            handled = child.dispatch(ev)
            if (handled) isChildNeedEvent = true

            if (!handled) handled = onTouch(ev)
        } else {
            if (isChildNeedEvent) handled = child.dispatch(ev)
            if (!handled) handled = onTouch(ev)
        }
        
        if (ev.actionMasked == MotionEvent.ACTION_UP) {
            clearStatus()
        }
            
        return handled
    }
    
    private fun clearStatus() {
        isChildNeedEvent = false
    }

    override fun onTouch(ev: MotionEvent): Boolean {
        return false
    }
}
複製代碼

代碼好像增長了不少,其實只多作了兩件事:

  1. 增長了一個isChildNeedEvent狀態,對是子View是否處理了DOWN事件進行記錄,並在其餘觸摸事件時使用這個狀態
  2. 在收到DOWN事件的最開始和收到UP事件的最後,重置狀態

此時框架使用者仍是隻須要關心onTouch()鉤子,在須要處理事件時進行處理並返回true,其餘事情框架都作好了。

1.4. 四造:增長外部事件攔截

上面的框架已經能完成基本的事件分發工做了,但下面這個需求,你嘗試一下用如今框架能實現嗎? 需求:在可滑動View中有一個可點擊View,須要讓用戶即便按下的位置是可點擊View,再進行滑動時,也能夠滑動外面的的可滑動View。

這個需求其實很是常見,好比全部「條目可點擊的滑動列表」就是這樣的(微信/QQ聊天列表)。

假如使用上面的框架:

  1. 可滑動View會先把事件傳到裏邊的可點擊View
  2. 可點擊View一看來事件了,我又能點擊,那捨我其誰啊
  3. 而後外面的可滑動View就永遠沒法處理事件,也就沒法滑動

因此直接使用如今的模型去實現的「條目可點擊的滑動列表」會永遠滑動不了。

那怎麼辦呢?

  1. 難道要讓裏面的可點擊View去感知一下(層層往上找),本身是否是被一個能消費事件的View包裹?是的話本身就不消費事件了?
    1. 這確定是不行的,先不說子View層層反向遍歷父親是否是個好實現,至少不能外面是能夠滑動的,裏邊View的點擊事件就所有失效
  2. 或者咱們調整dispatch()方法在傳入事件過程當中的人設,讓它不是隻能往裏傳遞事件,而是在本身能消費事件的時候把事件給本身
    1. 這確定也是不行的,跟第一個辦法的主要問題同樣

直接想實現以爲處處是矛盾,找不到突破口,那就從頭開始吧,從什麼樣的觸摸反饋是用戶以爲天然的出發,看看這種符合直覺的反饋方案是否存在,找出來它是什麼,再考慮咱們要怎麼實現:

  1. 當用戶面對一個滑動View裏有一個可點擊View,當他摸在可點擊View上時,他是要作什麼?
  2. 顯然,只有兩個可能性,要麼用戶想點這個可點擊View,要麼用戶想滑動這個可滑動View
  3. 那麼,當用戶剛用手指接觸的時候,也就是DOWN事件剛來的時候,能判斷用戶想幹什麼嗎?很抱歉,不能
  4. 因此,客觀條件下,你就是不可能在DOWN事件傳過來的時候,判斷出用戶到底想作什麼,因而兩個View其實都不能肯定本身是否要消費事件

你先忘記前面說的原則,你想一想,不考慮其餘因素,也不是隻能用DOWN事件,只要你能判斷用戶的想法就行,你有什麼辦法?

  1. 辦法確定是有的,你能夠多等一會,看用戶接下來的行爲能匹配哪一種操做模式
    1. 點擊操做的模式是這樣:用戶先DOWN,而後MOVE很小一段,也不會MOVE出這個子View,關鍵是比較短的時間就UP
    2. 滑動操做的模式是這樣:用戶先DOWN,而後開始MOVE,這時候可能會MOVE出這個子View,也可能不,但關鍵是比較長的時間也沒有在UP,一直是在MOVE
  2. 因此你的結論是,只有DOWN不行,還得看接下來的事件流,得走着瞧
  3. 再多考慮個長按的狀況,總結就是:
    1. 若是在某個時間內UP,就是點擊裏邊的View
    2. 若是比較長的時間UP,但沒怎麼MOVE,就是長按裏邊的View
    3. 若是在比較短的時間MOVE比較長的距離,就是滑動外面的View

看上去這個目標 View 斷定方案很不錯,安排得明明白白,但咱們現有的事件處理框架實現不了這樣的斷定方案,至少存在如下兩個衝突:

  1. 由於子View和父View都沒法在DOWN的時候判斷當前事件流是否是該給本身,因此一開始它們都只能返回false。但爲了能對後續事件作判斷,你但願事件繼續流過它們,按照當前框架的邏輯,你又不能返回false。
  2. 假設事件會流過它們,當事件流了一下子後,父 View 判斷出這符合本身的消費模式啊,因而想把事件給本身消費,但此時子 View 可能已經在消費事件了,而目前的框架是作不到阻止子 View 繼續消費事件的

因此要解決上述的衝突,就確定要對上一版的事件處理框架進行修改,並且看上去一不當心就會大改

  1. 首先看第二個衝突,解決它的一個直接方案是:調整 dispatch() 方法在傳入事件過程當中的人設,讓它不是隻傳遞事件了,還能夠在往裏傳遞事件前進行攔截,可以看狀況攔截下事件並交給本身的 onTouch() 處理

  2. 基於這個解決方案,大概有如下兩個改動相對小的方案調整思路:

    1. 思路一:
      1. 當事件走到可滑動父View的時候,它先攔截並處理事件,並且還把事件給攢着
      2. 當通過了幾個事件
        1. 若是判斷出符合本身的消費模式,那就直接開始本身消費了,也不用繼續攢事件了
        2. 若是判斷出不是本身的消費模式,再把全部攢着的事件一股腦給子 View,觸發裏邊的點擊操做
    2. 思路二:
      1. 全部的 View 只要可能消費事件,就在onTouch()裏對DOWN事件返回true,無論是否識別出當前屬於本身的消費模式
      2. 當事件走到到可滑動父 View 的時候,它先把事件往裏傳,裏邊可能會處理事件,可能不會,可滑動父 View 都暫時不關心
      3. 而後看子 View 是否處理事件
        1. 假如子 View 不處理事件,那啥問題沒有,父 View 直接處理事件就行了
        2. 假如子 View 處理事件,可滑動父View就會繃緊神經暗中觀察乘機而動,觀察事件是否是符合本身的消費模式,一旦發現符合,它就把事件流攔截下來,即便子View也在處理事件,它也不往裏disptach事件了,而是直接給本身的onTouch()
  3. 兩個思路總結一下:

    1. 思路一:外面的父 View 先攔事件,若是判斷攔錯了,再把事件往裏發
    2. 思路二:外面的父 View 先不攔事件,在判斷應該攔的時候,忽然把事件攔下來
  4. 這兩個思路都要對當前框架作改變,看似差很少,但其實仍是有比較明顯的優劣的

    1. 思路一問題比較明顯:
      1. 父 View 把事件攔下來了,而後發現攔錯了再給子 View,但其實子 View 又並不必定能消費事件,這不就是白作一步嗎。等到子View不處理事件,又把事件們還給父View,父View還得繼續處理事件。整個過程不只繁瑣,並且會讓開發者感受到彆扭
      2. 因此這個思路不太行,還得是把事件先給子View
    2. 思路二就相對正常多了,只有一個問題(下一節再講,你能夠猜一猜,這裏我先當沒發現),並且框架要作的改變也不多:
      1. 增長一個攔截方法onIntercept()在父 View 往裏dispatch事件前,開發者能夠覆寫這個方法,加入本身的事件模式分析代碼,而且能夠在肯定要攔截的時候進行攔截
        1. 把分析攔截邏輯抽成一個方法很是合理:何時攔,何時不攔,內裏的邏輯不少,但對外暴露的 API 能夠很小,很是適合抽出去
      2. 在肯定本身要攔截事件的時候,即便裏邊在一開始消費了事件,也不把事件往裏傳了,而是直接給本身的onTouch()

示意圖:

open class MView {
    open fun dispatch(ev: MotionEvent): Boolean {
        return onTouch(ev)
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false
    }
}

class MViewGroup(private val child: MView) : MView() {
    private var isChildNeedEvent = false
    private var isSelfNeedEvent = false

    override fun dispatch(ev: MotionEvent): Boolean {
        var handled = false

        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            clearStatus()
            
            if (onIntercept(ev)) {
                isSelfNeedEvent = true
                handled = onTouch(ev)
            } else {
                handled = child.dispatch(ev)
                if (handled) isChildNeedEvent = true

                if (!handled) {
                    handled = onTouch(ev)
                    if (handled) isSelfNeedEvent = true
                }
            }
        } else {
            if (isSelfNeedEvent) {
                handled = onTouch(ev)
            } else if (isChildNeedEvent) {
                if (onIntercept(ev)) {
                    isSelfNeedEvent = true
                    handled = onTouch(ev)
                } else {
                    handled = child.dispatch(ev)
                }
            }
        }

        if (ev.actionMasked == MotionEvent.ACTION_UP) {
            clearStatus()
        }
        
        return handled
    }

    private fun clearStatus() {
        isChildNeedEvent = false
        isSelfNeedEvent = false
    }

    override fun onTouch(ev: MotionEvent): Boolean {
        return false
    }

    open fun onIntercept(ev: MotionEvent): Boolean {
        return false
    }
}
複製代碼

寫的過程當中增長了一些對細節的處理:

  1. 不只是在DOWN事件的dispatch()前須要攔截,在後續事件中,也須要加入攔截,不然沒法實現中途攔截的目標
  2. 在某一個事件判斷攔截以後,還須要在後續事件中再判斷一次是否要攔截嗎?
    1. 徹底不須要,咱們但願的就是在一次觸摸中,儘量只有1個對象去消費事件,決定是你了,那就不要變
    2. 因此增長一個isSelfNeedEvent記錄本身是否攔截過事件,若是攔截過,後續事件直接就交給本身處理
  3. 在後續事件時,子 View 沒有處理事件,外面也不會再處理了,一樣由於只能有一個 View 處理(Actvity會處理這樣的事件,後面會提到)

這一下代碼是否是看上去瞬間複雜了,但其實只是增長了一個事件攔截機制,對比上一次試造的輪子,會更容易理解。(要是 Markdown 支持代碼塊內自定義着色就行了)

並且對於框架的使用者來講,關注點仍是很是少

  1. 重寫onIntercept()方法,判斷何時須要攔截事件,須要攔截時返回true
  2. 重寫onTouch()方法,若是處理了事件,返回true

1.5. 五造:增長內部事件攔截

上面的處理思路雖然實現了需求,但可能會致使一個問題:裏邊的子 View 接收了一半的事件,可能都已經開始處理並作了一些事情,父 View 突然就不把後續事件給它了,會不會違背用戶操做的直覺?甚至出現更奇怪的現象?

這個問題確實比較麻煩,分兩類狀況討論

  1. 裏邊的 View 接收了一半事件,但尚未真正開始反饋交互,或者在進行能夠被取消的反饋

    1. 好比對於一個可點擊的View,View的默認實現是隻要被touch了,就會有pressed狀態,若是你設置了對應的background,你的 View 就會有高亮效果
    2. 這種高亮即便被中斷也沒事,不會讓用戶感受到奇怪,不信你本身試試微信的聊天列表
    3. 但一個值得注意的點是,若是你只是直接不發送MOVE事件了,這會有問題,就這個按下高亮的例子,若是你只是不傳MOVE事件了,那誰來告訴裏邊的子View取消高亮呢?因此你須要在中斷的時候也傳一個結束事件
      1. 可是,你能直接傳一個UP事件嗎?也是不行的,由於這樣就匹配了裏邊點擊的模式了,會直接觸發一個點擊事件,這顯然不是咱們想要的
      2. 因而外面須要給一個新的事件,這個事件的類型就叫取消事件好了CANCEL
    4. 總結一下,對於這種簡單的可被取消狀況,你能夠這樣去處理:
      1. 在肯定要攔截的時候,在把真正的事件轉發給本身的onTouch()的同時,另外生成一個新的事件發給本身的子View,事件類型是CANCEL,它將是子View收到的最後一個事件
      2. 子View能夠在收到這個事件後,對當前的一些行爲進行取消
  2. 裏邊的View接收了一半事件,已經開始反饋交互了,這種反饋最好不要去取消它,或者說取消了會顯得很怪

    1. 這個時候,事情會複雜一些,並且這個場景發生的遠比你想象中的多,形式也多種多樣,不處理好的後果也比只是讓用戶感受上奇怪要嚴重得多,可能會有的功能會實現不了,下面舉兩個例子

      1. 在ViewPager裏有三個page,page裏是ScrollView,ViewPager能夠橫向滑動,page裏的ScrollView能夠豎向滑動
        1. 若是按前面邏輯,當ViewPager把事件給裏邊ScrollView以後,它也會偷偷觀察,若是你一直是豎向滑動,那沒話說,ViewPager不會觸發攔截事件
        2. 但若是你豎着滑着滑着,手抖了,開始橫滑(或者只是斜滑),ViewPager就會開始緊張,想「組織終於決定是我了嗎?真的假的,那我可就不客氣了」,因而在你斜滑必定距離以後,突然發現,你劃不動ScrollView了,而ViewPager開始動
        3. 緣由就是ScrollView的豎滑被取消了,ViewPager把事件攔下來,開始橫滑
        4. 這個體驗仍是比較怪的,會有種過於靈敏的感受,會讓用戶只能當心翼翼地滑動
      2. 在一個ScrollView裏有一些按鈕,按鈕有長按事件,長按再拖動就能夠移動按鈕
        1. (更常見的例子是一個列表,裏邊的條目能夠長按拖動)
        2. 一樣按前面的邏輯,當你長按後準備拖動按鈕時,你怎麼保證不讓ScrollView把事件攔下來呢?
    2. 因此這類問題是必定要解決的,但要怎麼解決呢

      1. 仍是先從業務出發,從用戶的角度看,當裏邊已經開始作一些特殊處理了,外面應不該該把事件搶走?
        1. 不該該對吧,OK,解決方針就是不該該讓外邊的View搶事件
      2. 因此接下來的問題是:誰先判斷出外邊的View不應搶事件,裏邊的子View仍是外邊的父View?而後怎麼不讓外邊的View搶?
        1. 首先,確定是裏邊的View作出判斷:這個事件,真的,外邊的View你最好別搶,要不用戶不開心了
        2. 而後裏邊就得告知外邊,你別搶了,告知能夠有幾個方式
          1. 外邊搶以前問一下里邊,我能不能搶
          2. 裏邊在肯定這個事件不能被搶以後,從dispatch方法返回一個特別的值給外邊(以前只是true和false,如今要加一個)
          3. 裏邊經過別的方式通知外邊,你不要搶
        3. 講道理,我以爲三個方式都行,但第三個方式最爲簡單直接,並且對框架沒有過大的改動,Android也使用了這個方式,父View給子View提供了一個方法requestDisallowInterceptTouchEvent(),子View調用它改變父View的一個狀態,同時父View每次在準備攔截前都會判斷這個狀態(固然這個狀態只對當前事件流有效)
        4. 而後,這個狀況還得再注意一點,它應該是向上遞歸的,也就是,在複雜的狀況中,有可能有多個上級在暗中觀察,當裏邊的View決定要處理事件並且不許備交出去的時候,外面全部的暗中觀察的父View都應該把腦殼轉回去

因此,連同上一次試造,總結一下

  1. 對於多個可消費事件的View進行嵌套的狀況,怎麼斷定事件的歸屬會變得很是麻煩,沒法馬上在DOWN事件時就肯定,只能在後續的事件流中進一步判斷
  2. 因而在沒判斷歸屬的時候,先由裏邊的子View消費事件,外面暗中觀察,同時兩方一塊對事件類型作進一步匹配,並準備在匹配成功後對事件流的歸屬進行搶拍
  3. 搶拍是先搶先得
    1. 父親先搶到,發個CANCEL事件給兒子就完了
    2. 兒子先搶到,就得大喊大叫,撒潑耍賴,爸爸們行行好吧,最後得以安心處理事件

另外有幾個值得一提的地方:

  1. 這種先搶先得的方式感受上有點亂來是吧,但目前也沒有想到更好的辦法了,通常都是開發者本身根據實際用戶體驗調整,讓父親或兒子在最適合的時機準確及時地搶到應得的事件
  2. 父View在攔截下事件後,把接下來的事件傳給本身的onTouch()後,onTouch()只會收到後半部分的事件,這樣會不會有問題呢?
    1. 確實直接給後半部分會有問題,因此通常狀況是,在沒攔截的時候就作好若是要處理事件的一些準備工做,以便以後攔截事件了,只使用後半部分事件也能實現符合用戶直覺的反饋
interface ViewParent {
    fun requestDisallowInterceptTouchEvent(isDisallowIntercept: Boolean)
}

open class MView {
    var parent: ViewParent? = null

    open fun dispatch(ev: MotionEvent): Boolean {
        return onTouch(ev)
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false
    }
}

open class MViewGroup(private val child: MView) : MView(), ViewParent {
    private var isChildNeedEvent = false
    private var isSelfNeedEvent = false
    private var isDisallowIntercept = false

    init {
        child.parent = this
    }

    override fun dispatch(ev: MotionEvent): Boolean {
        var handled = false
        
        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            clearStatus()
            
            // add isDisallowIntercept
            if (!isDisallowIntercept && onIntercept(ev)) {
                isSelfNeedEvent = true
                handled = onTouch(ev)
            } else {
                handled = child.dispatch(ev)
                if (handled) isChildNeedEvent = true

                if (!handled) {
                    handled = onTouch(ev)
                    if (handled) isSelfNeedEvent = true
                }
            }
        } else {
            if (isSelfNeedEvent) {
                handled = onTouch(ev)
            } else if (isChildNeedEvent) {
                // add isDisallowIntercept
                if (!isDisallowIntercept && onIntercept(ev)) {
                    isSelfNeedEvent = true

                    // add cancel
                    val cancel = MotionEvent.obtain(ev)
                    cancel.action = MotionEvent.ACTION_CANCEL
                    handled = child.dispatch(cancel)
                    cancel.recycle()
                } else {
                    handled = child.dispatch(ev)
                }
            }
        }
        
        if (ev.actionMasked == MotionEvent.ACTION_UP 
            || ev.actionMasked == MotionEvent.ACTION_CANCEL) {
            clearStatus()
        }
        
        return handled
    }
    
    private fun clearStatus() {
        isChildNeedEvent = false
        isSelfNeedEvent = false
        isDisallowIntercept = false
    }

    override fun onTouch(ev: MotionEvent): Boolean {
        return false
    }

    open fun onIntercept(ev: MotionEvent): Boolean {
        return false
    }

    override fun requestDisallowInterceptTouchEvent(isDisallowIntercept: Boolean) {
        this.isDisallowIntercept = isDisallowIntercept
        parent?.requestDisallowInterceptTouchEvent(isDisallowIntercept)
    }
}
複製代碼

此次改動主要是增長了發出CANCEL事件和requestDisallowInterceptTouchEvent機制

  1. 在發出CANCEL事件時有一個細節:沒有在給 child 分發CANCEL事件的同時繼續把原事件分發給本身的onTouch
    1. 這是源碼中的寫法,不是我故意的,多是爲了讓一個事件也只能有一個View處理,避免出現bug
  2. 實現requestDisallowInterceptTouchEvent機制時,增長了ViewParent接口
    1. 不使用這種寫法也行,但使用它從代碼整潔的角度看會更優雅,好比避免反向依賴,並且這也是源碼的寫法,因而直接搬來了

雖然目前整個框架的代碼有點複雜,但對於使用者來講,依然很是簡單,只是在上一版框架的基礎上增長了:

  1. 若是View判斷本身要消費事件,並且執行的是不但願被父View打斷的操做時,須要馬上調用父View的requestDisallowInterceptTouchEvent()方法
  2. 若是在onTouch方法中對事件消費而且作了一些操做,須要注意在收到CANCEL事件時,對操做進行取消

到這裏,事件分發的主要邏輯已經講清楚了,不過還差一段 Activity 中的處理,其實它作的事情相似ViewGroup,只有這幾個區別:

  1. 不會對事件進行攔截
  2. 只要有子View沒有處理的事件,它都會交給本身的onTouch()
open class MActivity(private val childGroup: MViewGroup) {
    private var isChildNeedEvent = false
    private var isSelfNeedEvent = false

    open fun dispatch(ev: MotionEvent): Boolean {
        var handled = false

        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            clearStatus()

            handled = childGroup.dispatch(ev)
            if (handled) isChildNeedEvent = true

            if (!handled) {
                handled = onTouch(ev)
                if (handled) isSelfNeedEvent = true
            }
        } else {
            if (isSelfNeedEvent) {
                handled = onTouch(ev)
            } else if (isChildNeedEvent) {
                handled = childGroup.dispatch(ev)
            }

            if (!handled) handled = onTouch(ev)
        }

        if (ev.actionMasked == MotionEvent.ACTION_UP
            || ev.actionMasked == MotionEvent.ACTION_CANCEL) {
            clearStatus()
        }

        return handled
    }

    private fun clearStatus() {
        isChildNeedEvent = false
        isSelfNeedEvent = false
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false
    }
}
複製代碼

因此回頭看,你會發現事件分發其實很簡單,它的關鍵不在於「不一樣的事件類型、不一樣的View種類、不一樣的回調方法、方法不一樣的返回值」對事件分發是怎麼影響的。

關鍵在於「它要實現什麼功能?對實現效果有什麼要求?使用了什麼解決方案?」,從這個角度,就能清晰並且簡單地把事件分發整個流程梳理清楚。

事件分發要實現的功能是:對用戶的觸摸操做進行反饋,使之符合用戶的直覺。

從用戶的直覺出發能獲得這麼兩個要求:

用戶的一次操做只有一個View去消費

  1. 讓消費事件的View跟用戶的意圖一致
  2. 第二個要求是最難實現的,若是有多個View均可以消費觸摸事件,怎麼斷定哪一個View更適合消費,而且把事件交給它。

咱們使用了一套簡單但有效的先到先得策略,讓內外的可消費事件的View擁有近乎平等的競爭消費者的資格:它們都能接收到事件,並在本身斷定應該消費事件的時候去發起競爭申請,申請成功後事件就所有由它消費。

場景一

模擬View和ViewGroup都不消費事件的場景:

[down]
|layer:SActivity |on:Dispatch_BE |type:down
|layer:SViewGroup |on:Dispatch_BE |type:down
|layer:SViewGroup |on:Intercept_BE |type:down
|layer:SViewGroup |on:Intercept_AF |result(super):false |type:down
|layer:SView |on:Dispatch_BE |type:down
|layer:SView |on:Touch_BE |type:down
|layer:SView |on:Touch_AF |result(super):false |type:down
|layer:SView |on:Dispatch_AF |result(super):false |type:down
|layer:SViewGroup |on:Touch_BE |type:down
|layer:SViewGroup |on:Touch_AF |result(super):false |type:down
|layer:SViewGroup |on:Dispatch_AF |result(super):false |type:down
|layer:SActivity |on:Touch_BE |type:down
|layer:SActivity |on:Touch_AF |result(super):false |type:down
|layer:SActivity |on:Dispatch_AF |result(super):false |type:down
 
[move]
|layer:SActivity |on:Dispatch_BE |type:move
|layer:SActivity |on:Touch_BE |type:move
|layer:SActivity |on:Touch_AF |result(super):false |type:move
|layer:SActivity |on:Dispatch_AF |result(super):false |type:move

[move]
...
 
[up]
|layer:SActivity |on:Dispatch_BE |type:up
|layer:SActivity |on:Touch_BE |type:up
|layer:SActivity |on:Touch_AF |result(super):false |type:up
|layer:SActivity |on:Dispatch_AF |result(super):false |type:up
複製代碼

這裏用BE表明 before,表示該方法開始處理事件的時候,用AF表明after,表示該方法結束處理事件的時候,而且打印處理的結果

從日誌中能清楚看到,當View和ViewGroup都不消費DOWN事件時,後續事件將再也不傳遞給View和ViewGroup

場景二

模擬View和ViewGroup都消費事件,同時ViewGroup在第二個MOVE事件時認爲本身須要攔截事件的場景:

[down]
|layer:SActivity |on:Dispatch_BE |type:down
|layer:SViewGroup |on:Dispatch_BE |type:down
|layer:SViewGroup |on:Intercept |result(false):false |type:down
|layer:SView |on:Dispatch_BE |type:down
|layer:SView |on:Touch |result(true):true |type:down
|layer:SView |on:Dispatch_AF |result(super):true |type:down
|layer:SViewGroup |on:Dispatch_AF |result(super):true |type:down
|layer:SActivity |on:Dispatch_AF |result(super):true |type:down
 
[move]
|layer:SActivity |on:Dispatch_BE |type:move
|layer:SViewGroup |on:Dispatch_BE |type:move
|layer:SViewGroup |on:Intercept |result(false):false |type:move
|layer:SView |on:Dispatch_BE |type:move
|layer:SView |on:Touch |result(true):true |type:move
|layer:SView |on:Dispatch_AF |result(super):true |type:move
|layer:SViewGroup |on:Dispatch_AF |result(super):true |type:move
|layer:SActivity |on:Dispatch_AF |result(super):true |type:move
 
[move]
|layer:SActivity |on:Dispatch_BE |type:move
|layer:SViewGroup |on:Dispatch_BE |type:move
|layer:SViewGroup |on:Intercept |result(true):true |type:move
|layer:SView |on:Dispatch_BE |type:cancel
|layer:SView |on:Touch_BE |type:cancel
|layer:SView |on:Touch_AF |result(super):false |type:cancel
|layer:SView |on:Dispatch_AF |result(super):false |type:cancel
|layer:SViewGroup |on:Dispatch_AF |result(super):false |type:move
|layer:SActivity |on:Touch_BE |type:move
|layer:SActivity |on:Touch_AF |result(super):false |type:move
|layer:SActivity |on:Dispatch_AF |result(super):false |type:move
 
[move]
|layer:SActivity |on:Dispatch_BE |type:move
|layer:SViewGroup |on:Dispatch_BE |type:move
|layer:SViewGroup |on:Touch |result(true):true |type:move
|layer:SViewGroup |on:Dispatch_AF |result(super):true |type:move
|layer:SActivity |on:Dispatch_AF |result(super):true |type:move
 
[up]
|layer:SActivity |on:Dispatch_BE |type:up
|layer:SViewGroup |on:Dispatch_BE |type:up
|layer:SViewGroup |on:Touch |result(true):true |type:up
|layer:SViewGroup |on:Dispatch_AF |result(super):true |type:up
|layer:SActivity |on:Dispatch_AF |result(super):true |type:up
複製代碼

從日誌中能清楚看到,在ViewGroup攔截事件先後,事件是如何分發的

Android 源碼

View的dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent event) {

        boolean result = false;

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            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;
            }
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest // of the gesture. if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); } return result; } 複製代碼

定義ListenerInfo局部變量,ListenerInfo是View的靜態內部類,用來定義一堆關於View的XXXListener等方法;接着if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))語句就是重點,首先li對象天然不會爲null,li.mOnTouchListener經過下面方法賦值

public void setOnTouchListener(OnTouchListener l) {
    getListenerInfo().mOnTouchListener = l;
}
複製代碼

li.mOnTouchListener是否是null取決於控件(View)是否設置setOnTouchListener監聽。接着經過位與運算肯定控件(View)是否是ENABLED 的,默認控件都是ENABLED 的;接着判斷onTouch的返回值是否是true。經過如上判斷以後若是都爲true則設置默認爲false的result爲true,那麼接下來的if (!result && onTouchEvent(event))就不會執行,最終dispatchTouchEvent也會返回true。而若是if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))語句有一個爲false則if (!result && onTouchEvent(event))就會執行,若是onTouchEvent(event)返回false則dispatchTouchEvent返回false,不然返回true。

控件觸摸就會調運dispatchTouchEvent方法,而在dispatchTouchEvent中先執行的是onTouch方法,因此驗證了實例結論總結中的onTouch優先於onClick執行道理。若是控件是ENABLE且在onTouch方法裏返回了true則dispatchTouchEvent方法也返回true,不會再繼續往下執行;反之,onTouch返回false則會繼續向下執行onTouchEvent方法,且dispatchTouchEvent的返回值與onTouchEvent返回值相同。

總結結論

在View的觸摸屏傳遞機制中經過分析dispatchTouchEvent方法源碼咱們會得出以下基本結論:

  1. 觸摸控件(View)首先執行dispatchTouchEvent方法。
  2. 在dispatchTouchEvent方法中先執行onTouch方法,後執行onClick方法(onClick方法在onTouchEvent中執行,下面會分析)。
  3. 若是控件(View)的onTouch返回false或者mOnTouchListener爲null(控件沒有設置setOnTouchListener方法)或者控件不是enable的狀況下會調運onTouchEvent,dispatchTouchEvent返回值與onTouchEvent返回同樣。
  4. 若是控件不是enable的設置了onTouch方法也不會執行,只能經過重寫控件的onTouchEvent方法處理(上面已經處理分析了),dispatchTouchEvent返回值與onTouchEvent返回同樣。
  5. 若是控件(View)是enable且onTouch返回true狀況下,dispatchTouchEvent直接返回true,不會調用onTouchEvent方法。

View的dispatchTouchEvent方法中的onTouchEvent方法

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress) {
                            removeLongPressCallback();

                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    boolean isInScrollingContainer = isInScrollingContainer();

                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        setPressed(true, x, y);
                        checkForLongClick(0);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    if (!pointInView(x, y, mTouchSlop)) {
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }
複製代碼

若是一個控件是enable且disclickable則onTouchEvent直接返回false了;反之,若是一個控件是enable且clickable則繼續進入過於一個event的switch判斷中,而後最終onTouchEvent都返回了true。switch的ACTION_DOWN與ACTION_MOVE都進行了一些必要的設置與置位,接着到手擡起來ACTION_UP時你會發現,首先判斷了是否按下過,同時是否是能夠獲得焦點,而後嘗試獲取焦點,而後判斷若是不是longPressed則經過post在UI Thread中執行一個PerformClick的Runnable,也就是performClick方法。

public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }
複製代碼
public void setOnClickListener(OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
複製代碼

控件只要監聽了onClick方法則mOnClickListener就不爲null,並且有意思的是若是調運setOnClickListener方法設置監聽且控件是disclickable的狀況下默認會幫設置爲clickable。onClick就在onTouchEvent中執行的,並且是在onTouchEvent的ACTION_UP事件中執行的。

總結結論

  1. onTouchEvent方法中會在ACTION_UP分支中觸發onClick的監聽。
  2. 當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,纔會觸發下一個action。

Android View的觸摸屏事件傳遞機制有以下特徵:

  1. 觸摸控件(View)首先執行dispatchTouchEvent方法。
  2. 在dispatchTouchEvent方法中先執行onTouch方法,後執行onClick方法(onClick方法在onTouchEvent中執行,下面會分析)。
  3. 若是控件(View)的onTouch返回false或者mOnTouchListener爲null(控件沒有設置setOnTouchListener方法)或者控件不是enable的狀況下會調運onTouchEvent,dispatchTouchEvent返回值與onTouchEvent返回同樣。
  4. 若是控件不是enable的設置了onTouch方法也不會執行,只能經過重寫控件的onTouchEvent方法處理(上面已經處理分析了),dispatchTouchEvent返回值與onTouchEvent返回同樣。
  5. 若是控件(View)是enable且onTouch返回true狀況下,dispatchTouchEvent直接返回true,不會調用onTouchEvent方法。
  6. 當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,纔會觸發下一個action(也就是說dispatchTouchEvent返回true纔會進行下一次action派發)。

ViewGroup的dispatchTouchEvent方法

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ......
    boolean handled = false;
    ......
    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;

    // 1)處理初始的ACTION_DOWN
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // 把ACTION_DOWN做爲一個Touch手勢的始點,清除以前的手勢狀態。
        cancelAndClearTouchTargets(ev); //清除前一個手勢,*關鍵操做:mFirstTouchTarget重置爲null*
        resetTouchState(); //重置Touch狀態標識
    }

    // 2)檢查是否會被攔截
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
        // 是ACTION_DOWN的事件,或者mFirstTouchTarget不爲null(已經找到可以接收touch事件的目標組件)
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        // 判斷禁止攔截的FLAG,由於requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法能夠禁止執行是否須要攔截的判斷
        if (!disallowIntercept) {
            // 禁止攔截的FLAG爲false,說明能夠執行攔截判斷,則執行此ViewGroup的onInterceptTouchEvent方法
            intercepted = onInterceptTouchEvent(ev); // 此方法默認返回false,若是想修改默認的行爲,須要override此方法,修改返回值。
            ev.setAction(action);
        } else {
            // 禁止攔截的FLAG爲ture,說明沒有必要去執行是否須要攔截了,這個事件是沒法攔截的,可以順利經過,因此設置攔截變量爲false
            intercepted = false;
        }
    } else {
        // 當不是ACTION_DOWN事件而且mFirstTouchTarget爲null(意味着沒有touch的目標組件)時,這個ViewGroup應該繼續執行攔截的操做。
        intercepted = true;
    }
    // 經過前面的邏輯處理,獲得了是否須要進行攔截的變量值

    final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
    final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    if (!canceled && !intercepted) {
        // 不是ACTION_CANCEL而且攔截變量爲false
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 在ACTION_DOWN時去尋找此次DOWN事件新出現的TouchTarget
            final int actionIndex = ev.getActionIndex(); // always 0 for down

            .....

            final int childrenCount = mChildrenCount;
            if (newTouchTarget == null && childrenCount != 0) {
                // 根據觸摸的座標尋找可以接收這個事件的子組件
                final float x = ev.getX(actionIndex);
                final float y = ev.getY(actionIndex);

                final View[] children = mChildren;
                // 逆序遍歷全部子組件
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final int childIndex = i;
                    final View child = children[childIndex];
                    // 尋找可接收這個事件而且組件區域內包含點擊座標的子View
                    if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
                        continue;
                    }

                    newTouchTarget = getTouchTarget(child); // 找到了符合條件的子組件,賦值給newTouchTarget

                    ......

                    // 把ACTION_DOWN事件傳遞給子組件進行處理
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        // 若是此子ViewGroup消費了這個touch事件
                        mLastTouchDownTime = ev.getDownTime();
                        mLastTouchDownIndex = childIndex;
                        mLastTouchDownX = ev.getX();
                        mLastTouchDownY = ev.getY();
                        // 則爲mFirstTouchTarget賦值爲newTouchTarget,此子組件成爲新的touch事件的起點
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        alreadyDispatchedToNewTouchTarget = true;
                        break;
                    }
                }
            }
            ......
        }
    }

    // 通過前面的ACTION_DOWN的處理,有兩種狀況。
    if (mFirstTouchTarget == null) {
        // 狀況1:(mFirstTouchTarget爲null) 沒有找到可以消費touch事件的子組件或者是touch事件被攔截了,
        // 那麼在ViewGroup的dispatchTransformedTouchEvent方法裏面,處理Touch事件則和普通View同樣,
        // 本身沒法消費,調用super.dispatchOnTouchEvent()把事件回遞給父ViewGroup進行處理
        handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
    } else {
        // 狀況2:(mFirstTouchTarget!=null) 找到了可以消費touch事件的子組件,那麼後續的touch事件均可以傳遞到子View
        TouchTarget target = mFirstTouchTarget;
        // (這裏爲了理解簡單,省略了一個Target List的概念,有須要的同窗再查看源碼)
        while (target != null) {
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                // 若是前面利用ACTION_DOWN事件尋找符合接收條件的子組件的同時消費掉了ACTION_DOWN事件,這裏直接返回true
                handled = true;
            } else {
                final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
                // 對於非ACTION_DOWN事件,則繼續傳遞給目標子組件進行處理(注意這裏的非ACTION_DOWN事件已經不須要再判斷是否攔截)
                if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                    // 若是target子組件進行處理,符合某些條件的話,會傳遞ACTION_CANCEL給target子組件
                    // 條件是:若是ACTION_DOWN時沒有被攔截,然後面的touch事件被攔截,則須要發送ACTION_CANCEL給target子組件
                    handled = true;
                }
                ......
            }
        }
    }

    if (canceled || actionMasked == MotionEvent.ACTION_UP) {
        // 若是是ACTION_CANCEL或者ACTION_UP,重置Touch狀態標識,mFirstTouchTarget賦值爲null,後面的Touch事件都沒法派發給子View
        resetTouchState();
    }
    ......

    return handled;
}
複製代碼

ACTION_DOWN時進行一些初始化操做,清除以往的Touch狀態而後開始新的手勢。在這裏你會發現cancelAndClearTouchTargets(ev)方法中有一個很是重要的操做就是將mFirstTouchTarget設置爲了null,接着在resetTouchState()方法中重置Touch狀態標識。

使用變量intercepted來標記ViewGroup是否攔截Touch事件的傳遞,該變量相似第一步的mFirstTouchTarget變量,在後續代碼中起着很重要的做用。if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)這一條判斷語句說明當事件爲ACTION_DOWN或者mFirstTouchTarget不爲null(即已經找到可以接收touch事件的目標組件)時if成立,不然if不成立,而後將intercepted設置爲true,也即攔截事件。當事件爲ACTION_DOWN或者mFirstTouchTarget不爲null時判斷disallowIntercept(禁止攔截)標誌位,而這個標記在ViewGroup中提供了public的設置方法

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }
複製代碼

在其餘地方調用requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法,從而禁止執行是否須要攔截的判斷。當disallowIntercept爲true(禁止攔截判斷)時則intercepted直接設置爲false,不然調用onInterceptTouchEvent(ev)方法,而後將結果賦值給intercepted。那就來看下ViewGroup與View特有的onInterceptTouchEvent方法,以下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
複製代碼

經過標記和action檢查cancel,而後將結果賦值給局部boolean變量canceled。

獲取一個boolean變量標記split來標記,默認是true,做用是是否把事件分發給多個子View,這個一樣在ViewGroup中提供了public的方法設置,以下:

public void setMotionEventSplittingEnabled(boolean split) {
        if (split) {
            mGroupFlags |= FLAG_SPLIT_MOTION_EVENTS;
        } else {
            mGroupFlags &= ~FLAG_SPLIT_MOTION_EVENTS;
        }
    }
複製代碼

if (!canceled && !intercepted)判斷代表,事件不是ACTION_CANCEL而且ViewGroup的攔截標誌位intercepted爲false(不攔截)則會進入其中。

if語句if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE)處理ACTION_DOWN事件,判斷了childrenCount個數是否不爲0,而後接着拿到了子View的list集合preorderedList;接着經過一個for循環i從childrenCount - 1開始遍歷到0,倒序遍歷全部的子view,這是由於preorderedList中的順序是按照addView或者XML佈局文件中的順序來的,後addView添加的子View,會由於Android的UI後刷新機制顯示在上層;假如點擊的地方有兩個子View都包含的點擊的座標,那麼後被添加到佈局中的那個子view會先響應事件;這樣其實也是符合人的思惟方式的,由於後被添加的子view會浮在上層,因此咱們去點擊的時候通常都會但願點擊最上層的那個組件先去響應事件。

經過getTouchTarget去查找當前子View是否在mFirstTouchTarget.next這條target鏈中的某一個targe中,若是在則返回這個target,不然返回null。在這段代碼的if判斷經過說明找到了接收Touch事件的子View,即newTouchTarget,那麼,既然已經找到了,因此執行break跳出for循環。若是沒有break則繼續向下執行,這裏你能夠看見一段if判斷的代碼if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)),調用方法dispatchTransformedTouchEvent()將Touch事件傳遞給特定的子View。該方法十分重要,在該方法中爲一個遞歸調用,會遞歸調用dispatchTouchEvent()方法。在dispatchTouchEvent()中若是子View爲ViewGroup而且Touch沒有被攔截那麼遞歸調用dispatchTouchEvent(),若是子View爲View那麼就會調用其onTouchEvent()。dispatchTransformedTouchEvent方法若是返回true則表示子View消費掉該事件,同時進入該if判斷。知足if語句後重要的操做有:

  • 給newTouchTarget賦值;
  • 給alreadyDispatchedToNewTouchTarget賦值爲true;
  • 執行break,由於該for循環遍歷子View判斷哪一個子View接受Touch事件,既然已經找到了就跳出該外層for循環;

若是if判斷中的dispatchTransformedTouchEvent()方法返回false,即子View的onTouchEvent返回false(即Touch事件未被消費),那麼就不知足該if條件,也就沒法執行addTouchTarget(),從而致使mFirstTouchTarget爲null(無法對mFirstTouchTarget賦值,由於上面分析了mFirstTouchTarget一進來是ACTION_DOWN就置位爲null了),那麼該子View就沒法繼續處理ACTION_MOVE事件和ACTION_UP事件。

若是if判斷中的dispatchTransformedTouchEvent()方法返回true,即子View的onTouchEvent返回true(即Touch事件被消費),那麼就知足該if條件,從而mFirstTouchTarget不爲null。

if (newTouchTarget == null && mFirstTouchTarget != null)。該if表示通過前面的for循環沒有找到子View接收Touch事件而且以前的mFirstTouchTarget不爲空則爲真,而後newTouchTarget指向了最初的TouchTarget。

對於此處ACTION_DOWN的處理具體體如今dispatchTransformedTouchEvent()方法,該方法返回值具有以下特徵:

由於在dispatchTransformedTouchEvent()會調用遞歸調用dispatchTouchEvent()和onTouchEvent(),因此dispatchTransformedTouchEvent()的返回值其實是由onTouchEvent()決定的。簡單地說onTouchEvent()是否消費了Touch事件的返回值決定了dispatchTransformedTouchEvent()的返回值,從而決定mFirstTouchTarget是否爲null,進一步決定了ViewGroup是否處理Touch事件,通過上面對於ACTION_DOWN的處理後mFirstTouchTarget可能爲null或者不爲null。

mFirstTouchTarget爲null時,也就是說Touch事件未被消費,即沒有找到可以消費touch事件的子組件或Touch事件被攔截了,則調用ViewGroup的dispatchTransformedTouchEvent()方法處理Touch事件(和普通View同樣),即子View沒有消費Touch事件,那麼子View的上層ViewGroup纔會調用其onTouchEvent()處理Touch事件。具體就是在調用dispatchTransformedTouchEvent()時第三個參數爲null .子view對於Touch事件處理返回true那麼其上層的ViewGroup就沒法處理Touch事件了,子view對於Touch事件處理返回false那麼其上層的ViewGroup才能夠處理Touch事件。mFirstTouchTarget不爲null時,也就是說找到了能夠消費Touch事件的子View且後續Touch事件能夠傳遞到該子View。能夠看見在源碼的else中對於非ACTION_DOWN事件繼續傳遞給目標子組件進行處理,依然是遞歸調用dispatchTransformedTouchEvent()方法來實現的處理。

ViewGroup的dispatchTouchEvent中可能執行的onInterceptTouchEvent方法

public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
複製代碼

若是ViewGroup的onInterceptTouchEvent返回false就不阻止事件繼續傳遞派發,不然阻止傳遞派發。

ViewGroup的dispatchTouchEvent中執行的dispatchTransformedTouchEvent方法

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 { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } // Calculate the number of pointers to deliver. final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; // If for some reason we ended up in an inconsistent state where it looks like we // might produce a motion event with no pointers in it, then drop the event. if (newPointerIdBits == 0) { return false; } // If the number of pointers is the same and we don't need to perform any fancy
        // irreversible transformations, then we can reuse the motion event for this
        // dispatch as long as we are careful to revert any changes we make.
        // Otherwise we need to make a copy.
        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }

        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }
複製代碼

在dispatchTouchEvent()中調用dispatchTransformedTouchEvent()將事件分發給子View處理。在此咱們須要重點分析該方法的第三個參數(View child)。在dispatchTouchEvent()中屢次調用了dispatchTransformedTouchEvent()方法,並且有時候第三個參數爲null,有時又不是,他們到底有啥區別呢?這段源碼中很明顯展現告終果。在dispatchTransformedTouchEvent()源碼中能夠發現屢次對於child是否爲null的判斷,而且均作出以下相似的操做。其中,當child == null時會將Touch事件傳遞給該ViewGroup自身的dispatchTouchEvent()處理,即super.dispatchTouchEvent(event)(也就是View的這個方法,由於ViewGroup的父類是View);當child != null時會調用該子view(固然該view多是一個View也多是一個ViewGroup)的dispatchTouchEvent(event)處理,即child.dispatchTouchEvent(event)。 ViewGroup沒有重寫View的onTouchEvent(MotionEvent event) 方法.

總結結論

Android事件派發是先傳遞到最頂級的ViewGroup,再由ViewGroup遞歸傳遞到View的。 在ViewGroup中能夠經過onInterceptTouchEvent方法對事件傳遞進行攔截,onInterceptTouchEvent方法返回true表明不容許事件繼續向子View傳遞,返回false表明不對事件進行攔截,默認返回false。 子View中若是將傳遞的事件消費掉,ViewGroup中將沒法接收到任何事件。

Activity的dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
複製代碼

Activity的attach方法能夠發現getWindow()返回的就是PhoneWindow對象(PhoneWindow爲抽象Window的實現子類),那就簡單了,也就至關於PhoneWindow類的方法,而PhoneWindow類實現於Window抽象類,因此先看下Window類中抽象方法的定義,以下:

public abstract boolean superDispatchTouchEvent(MotionEvent event);
複製代碼

PhoneWindow裏看下Window抽象方法的實現吧,以下:

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
複製代碼

在PhoneWindow類裏發現,mDecor是DecorView類的實例,同時DecorView是PhoneWindow的內部類。最驚人的發現是DecorView extends FrameLayout implements RootViewSurfaceTaker,看見沒有?它是一個真正Activity的root view,它繼承了FrameLayout。

DecorView類的superDispatchTouchEvent方法吧,以下:

public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }
複製代碼

Activity的dispatchTouchEvent方法的if (getWindow().superDispatchTouchEvent(ev))本質執行的是一個ViewGroup的dispatchTouchEvent方法(這個ViewGroup是Activity特有的root view,也就是id爲content的FrameLayout佈局).

在Activity的觸摸屏事件派發中:

  1. 首先會觸發Activity的dispatchTouchEvent方法。
  2. dispatchTouchEvent方法中若是是ACTION_DOWN的狀況下會接着觸發onUserInteraction方法。
  3. 接着在dispatchTouchEvent方法中會經過Activity的root View(id爲content的FrameLayout),實質是ViewGroup,經過super.dispatchTouchEvent把touchevent派發給各個activity的子view,也就是咱們再Activity.onCreat方法中setContentView時設置的view。
  4. 若Activity下面的子view攔截了touchevent事件(返回true)則Activity.onTouchEvent方法就不會執行。

Activity的dispatchTouchEvent方法中調運的onUserInteraction方法

public void onUserInteraction() {
    }
複製代碼

此方法是activity的方法,當此activity在棧頂時,觸屏點擊按home,back,menu鍵等都會觸發此方法。下拉statubar、旋轉屏幕、鎖屏不會觸發此方法。因此它會用在屏保應用上,由於當你觸屏機器 就會立馬觸發一個事件,而這個事件又不太明確是什麼,正好屏保知足此需求;或者對於一個Activity,控制多長時間沒有用戶點響應的時候,本身消失等。

Activity的dispatchTouchEvent方法中調運的onTouchEvent方法

public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }
複製代碼

若是一個屏幕觸摸事件沒有被這個Activity下的任何View所處理,Activity的onTouchEvent將會調用。這對於處理window邊界以外的Touch事件很是有用,由於一般是沒有View會接收到它們的。返回值爲true代表你已經消費了這個事件,false則表示沒有消費,默認實現中返回false。

繼續分析吧,重點就一句,mWindow.shouldCloseOnTouch(this, event)中的mWindow實際就是上面分析dispatchTouchEvent方法裏的getWindow()對象,因此直接到Window抽象類和PhoneWindow子類查看吧,發現PhoneWindow沒有重寫Window的shouldCloseOnTouch方法,因此看下Window類的shouldCloseOnTouch實現吧,以下:

public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
        if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
                && isOutOfBounds(context, event) && peekDecorView() != null) {
            return true;
        }
        return false;
    }
複製代碼

判斷mCloseOnTouchOutside標記及是否爲ACTION_DOWN事件,同時判斷event的x、y座標是否是超出Bounds,而後檢查FrameLayout的content的id的DecorView是否爲空。其實沒啥過重要的,這只是對於處理window邊界以外的Touch事件有判斷價值而已。

相關文章
相關標籤/搜索