Android的事件分發——附帶學習過程與感想

Android的事件分發

ps:本博客是筆者本身根據官方開發文檔一步一步本身學習記錄下來的過程,可能會有點錯誤,不過我感受大部分應該是正確的,中途思惟有點跳躍,最後由於實踐的時候遇到一點小問題,致使我當時都在懷疑本身,因此又多看了幾遍文檔的函數介紹。前端

事件分發

說到事件分發機制我第一反應會想到Dom的事件流,早期的IE和網景的事件流是相反的。(IE的事件流是咱們經常據說的事件冒泡,從下至上;網景的事件流則相反,爲事件捕獲)android

在如今瀏覽器都使用的Dom2事件監聽方法中,實現了事件的捕獲與事件冒泡,但只能二選一,默認爲事件冒泡,因此咱們常常聽到的說法是事件冒泡。程序員

再來講Android,在Android中咱們稱這爲事件的分發機制,他的分發機制與dom2很類似,不過是它兩個事件都啓用,就是一個U型的分發機制。瀏覽器

查資料的過程

我上開發文檔中去搜索了一下「dispatchTouchEvent」,發現「ViewGroup、Activity、Dialog、View」 中有着如出一轍的一個方法。bash

https://developer.android.com/s/results?q=dispatchTouchEvent

ViewGroup的點擊事件

先無論上面的那幾個,咱們查看一下第四個官方文檔, 「Manage touch events in a ViewGroup | Android Developers」 看標題他正好是咱們想要去了解的。dom

每當在ViewGroup的表面上檢測到觸摸事件時(包括其子級的表面),都會調用 onInterceptTouchEvent() 方法。若是 onInterceptTouchEvent() 返回true,則將MotionEvent攔截,這意味着它不會傳遞給子級,而是傳遞給父級的onTouchEvent()方法。ide

該onInterceptTouchEvent()方法使父母有機會在其孩子以前看到任何觸摸事件。若是true從返回onInterceptTouchEvent(),則之前處理觸摸事件的子視圖將收到ACTION_CANCEL,而且從該點開始的事件將發送到父級的onTouchEvent()方法進行常規處理。onInterceptTouchEvent()當false事件沿視圖層次結構行進到一般的目標時,它們也能夠返回並監視事件,事件將由其本身處理 onTouchEvent()。函數

Activity、ViewGroup、View 的關係

才接觸事件分發(原來接觸過一次是在ViewPage製做輪播圖的時候,如今可使用ViewPage2了),看完第一段真是一頭霧水,本身或子集的onInterceptTouchEvent()方法?父級的onTouchEvent()方法?這一看就知道ViewGroup是一箇中間商或零售商的角色啊,這須要咱們瞭解一下Activity、ViewGroup、View 的關係了,正好是上面四大天團的三個。(Dialog咱們就無論了)學習

Android視圖間的關係

其實不少知識點是相通的,一步一步來,就像建造者模式同樣,日積月累,最後就會作出一份完美的蛋炒飯。上面這個關係圖呢是我在瞭解Activity的繪製流程的時候,根據當時整理的流程圖畫出來的。ui

擴展1

Android的繪製流程

有興趣的話能夠去了解一下Activity的啓動流程。扯遠了,後面我要是聯想到其餘地方去你們就將就複習或者預習一些知識點,再回到咱們的事件分發,從圖3中咱們能夠看到,一個用戶交互的頁面的最上層的是Activity,咱們的四大組件之一,咱們在AndroidManifest配置中註冊Activity的時候能夠加一個theme屬性(android:theme=「@android:style/Theme.Dialog」)將一個Activity設置爲窗口模式,因此咱們就先無論上面那個Dialog了,先把它看成一個Activity吧。(彈出Dialog就把它當成一個新的Activity,後面那個可見Activity處於onPause狀態,是不能進行交互的)

Android的分發機制

有同窗可能會問什麼是Android的事件分發機制? 我理解的事件分發機制就相似於在學校的項目組中,老師有了一個新需求。而後處理狀況會有幾種。(我先羅列出咱們等等會用到的幾種狀況) 老師會將這個需求告訴前端組組長與後臺組組長,組長根據這個需求去作安排。

  1. 若是組長感受這個需求有點難,可能下面的人作很差,就本身接下了這個活,作好後組長直接就給老師反饋了,下面的人啥都不知道也就過去了;
  2. 若是這個需求組長感受到下面的同窗能夠作,就指派給下面的一個同窗,這個同窗處理好了,給組長反饋,而後組長就給老師反饋作完了;
  3. ...................

分發機制的我的理解

前提:幾個視圖嵌套在一塊兒
問題
這個操做事件(點擊、移動)被處理了(誰處理的?) ——> View(View是怎麼知道這個事件的?) ——> ViewGroup告訴他的(ViewGroup是怎麼知道這個事件的?) ——> Activity告訴他的(Activity怎麼知道的?) ——> 用戶點擊了屏幕 (倒回去看,這不就正是一個用戶點擊到系統處理的流程嗎?)

這個事件處理完了(View:我要告訴誰?) ——> ViewGroup(收到)——> Activity(收到)——> 用戶(哇,這個交互好棒)

結果:咱們將上面的合在一塊兒不就是一個完整的事件處理嗎?他的傳遞過程就是事件的分發機制。

圖示:Activity <==> ViewGroup <==> View

ps:我又想了一下最早知道用戶點擊事件的爲何不是那個View,爲何不是View收到事件處理完一層一層的返回上來,攔截是上層的對下層已經處理過的事件不進行上報。(這樣就和Dom事件流的冒泡事件同樣了) 我有個大膽的理解是,你在網頁中,你使用的是鼠標,鼠標是在屏幕裏面的,就能夠把它想成一個最底層的組件,只是z-index最高就好;而在手機中,點擊的通常是咱們手指,你只能點到屏幕,他最開始收到信號的只能是屏幕,而後只能根據圖3往下找,因此事件傳遞的順序應該就是上面那個圖示同樣。

分發機制的三大方法

android的事件分發最重要的三個方法是「dispatchTouchEvent()」、「onInterceptTouchEvent()」、「onTouchEvent()」,分發、攔截,處理與咱們上面🌰中的人物操做簡直如出一轍。

說了那麼多,都是本身根據大腦思惟的慣性去猜測的,還須要實際去驗證,咱們擼段Demo實踐一下。

角色
Activity(老師)
ViewGroup(組長)
View(組員)

動做: 咱們去Activity、ViewGroup、View中查看一下這三個方法的介紹及用法。

  • 發現這三個方法不是每一個組件都具備的,Activity與View中無onInterceptTouchEvent()方法,確實在現實生活中「程序員」(View)他沒有下面的人手去分配了,任務到他這就結束了,無論可否作出來。那有人會問老師呢?這個看了下面的分發(dispatchTouchEvent)介紹就知道了。

分發(dispatchTouchEvent)

  • 適用羣體:Activity、ViewGroup、View (三個都具備這個方法)
  • 雖然都有該方法,但Activity中對該方法的介紹就與另外兩個不同,不愧是四大組件之一。
  • 若是消耗了此事件,返回true。
  • 若是事件是由視圖處理的,返回true,不然返回false。
  • Activity:被調用來處理觸摸屏事件。您能夠重寫此設置,以在全部觸摸屏事件被髮送到窗口以前攔截它們。對於應該正常處理的觸摸屏事件,請確保調用此實現。soga,他是最初的源泉,就像「老師同樣」,他收到一份用戶的需求的時候,會先自行判斷是否須要去實現這個需求,不須要去實現他就不用分配任務下去,分配任務這就阻塞了,這也就解釋了爲何Activity中沒有onInterceptTouchEvent()方法了,能夠理解爲他與dispatchTouchEvent()方法合併了。
  • ViewGroup、View:將觸摸屏運動事件向下傳遞到目標視圖,若是是目標視圖,則傳遞到該視圖。

攔截(onInterceptTouchEvent)

  • 這已經基本說了,View沒有權限攔截,Activity沒有必要攔截,咱們就看他在ViewGroup中如何表現的就行了
  • 返回true能夠從子元素中竊取動做事件,並經過onTouchEvent()將它們分配給這個ViewGroup。當前目標將接收一個ACTION_CANCEL事件,這裏再也不傳遞任何消息。
  • 哇!一開始我覺得就他一我的有這個方法,簡單瞭解下就好,沒想到他介紹那麼長,看完後感受是個重點,事件的分發咱們大概瞭解其流程,但那只是基礎的流程,一些特殊的狀況是攔截機制引發的,搞懂他應該就能清楚各類狀況的事件處理流程了。(其實和咱們生活中的事件處理流程十分類似的,主要是生活中的處理方式比較多,不一樣的公司,不一樣的人的思惟不一樣,這纔有了規章制度,也能夠叫協議,統一下思惟,就不會那麼亂了)
  • 實現此方法來攔截全部觸摸屏動做事件。這容許您在事件發送給孩子時查看它們,並在任什麼時候候得到當前動做的全部權。
  • 使用這個函數須要注意,由於它與View.onTouchEvent(MotionEvent)的交互至關複雜,使用它須要以正確的方式實現這個方法和這個方法。事件將按如下順序接收:
    • 您將在這裏接收down事件。
    • down事件將由這個ViewGroup的一個子View組處理,或者交給您本身的onTouchEvent()方法處理;這意味着您應該實現onTouchEvent()來返回true,這樣您將繼續看到手勢的其他部分(而不是尋找父視圖來處理它)。另外,經過從onTouchEvent()返回true,您將不會在onInterceptTouchEvent()中收到任何後續事件,而且全部的觸摸處理都必須在onTouchEvent()中正常發生。
    • 只要從這個函數返回false,接下來的每一個事件(直到幷包括最終的up)都將首先在這裏傳遞,而後傳遞到目標的onTouchEvent()。
    • 若是您從這裏返回true,您將不會接收到任何如下事件:目標視圖將接收到相同的事件,但動做爲MotionEvent.ACTION_CANCEL,而且全部進一步的事件將被傳遞到您的onTouchEvent()方法,而且再也不出如今這裏。
    • ps:MotionEvent.ACTION_CANCEL表明着事件已經終止,你將不會再收到這個事件的任何消息,能夠當作一個上升事件。
  • 官網的介紹就先到這裏,等等實操一下,驗證一下而後理清一下這幾個函數的調用,由於咱們剛剛只是理清楚了android的事件分發的流程,但還有後續的操做,就像老師給了需求,後面需求的變動是須要怎麼處理,是老師再給組長說呢?仍是直接與接手這個需求的程序猿進行溝通?咱們也能夠先猜想一下,我猜的是直接找程序猿,由於我說了這來源於咱們現實生活,在咱們項目組裏,是把bug、需求放在禪道里,老師看到任務指派的會直接去找他,一句話通過多我的的描述會變成另外的一句話,這樣直接又快捷,我認爲這種點擊事件的處理須要的是快捷,要不帶給用戶的體驗是糟糕的,這樣這個系統應該是會被市場淘汰的。(下面看完onTouchEvent的介紹咱們就來實踐驗證下)

處理事件(onTouchEvent)

  • 查看了官方文檔後,又發現了那個問題,Activity他的方法介紹就是與其餘妖豔的*貨不同,誰叫他是老大呢!
  • 果真,看簡介就知道它是老大了,地位比別人高,一樣的工做作的比別人少。可能少而精吧!
  • Activity:當觸摸屏事件未被其下的任何視圖處理時調用。這對於處理髮生在窗口邊界以外的觸摸事件是最有用的,由於在窗口邊界以外沒有視圖能夠接收觸摸事件。
  • ViewGroup、View:實現此方法來處理觸摸屏運動事件。若是此方法用於檢測單擊操做,則建議經過實現和調用performClick()來執行操做。這將確保一致的系統行爲。後面包括的拿三項有點沒看懂,不要緊,他說了是建議,大概就是哪些東西不建議在這個方法裏面寫,第三條勉強理解下,就是在處理Android無障礙服務中的按鈕,正常狀況下只要是View是clicked的,就可使用AccessibilityNodeInfo.ACTION_CLICK;第二條多是監聽電話的時候,就是你打電話的時候吧;第一條多是你調節音量的時候。應該不用管這些,你就記得這個方法用來處理觸摸屏運動事件(MotionEvent.ACTION_MOVE )就好。

方法總結

到這咱們就瞭解了這三個方法的用途,以及一些注意事項,和我最初的想法有些不一樣(一開始我覺得,dispatchTouchEvent先監聽而後若是不是本身處理就分發,是的話就到onInterceptTouchEvent中去,onInterceptTouchEvent攔截就表示本身接受了,攔截後子控件就收不到消息了,就到onTouchEvent中開始工做了,onTouchEvent處理事件都在這,無論是接到任務,仍是作的過程,甚至作完,返回true就表明作完了)。如今改變下思路,人最可怕的就是知錯不改。

我從新整理了下對這三個方法的認識:

  • dispatchTouchEvent()方法:若是事件是被該視圖處理的就返回true,不然返回false。
  • onInterceptTouchEvent()方法:用來攔截事件的,若是你不攔截(返回false)的話,之後要更改的需求就直接跳到你這,這在多層嵌套的裏面就省掉了上面好幾層。若是攔截(返回true)之後的操做就直接跳到onTouchEvent中去處理,由於你接下了開始幹活了,改需求直接去找那個在辦公區工做中的你,你已經再也不攔截的會議室了。
  • onTouchEvent()方法:處理後續的操做,最多見的就是Move,直到UP結束的時候。返回false就證實你沒作好,可能雖然你接了任務,但你作不出來,會一層一層返回直到有一個返回ture的,而後後續的操做就會直接到那個返回true的onTouchEvent中執行,若是下面的一直沒有返回true的就會執行Activity中的onTouchEvent()。

代碼

代碼:
Activity(MainActivity.kt)

class MainActivity : AppCompatActivity() {

    val ROLE_TAG:String = "老師"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> Log.i(ROLE_TAG,"有個新需求,分配新需求")
        }
        return super.dispatchTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_UP -> Log.i(ROLE_TAG,"我靠!這個需求這麼南嗎?一我的都作不了?")
        }
        return super.onTouchEvent(event)
    }

}
複製代碼


ViewGroup(GroupLeader.kt)

class GroupLeader : LinearLayout {

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet) : super(context,attrs)
    constructor(context: Context?, attrs: AttributeSet, defStyleAttr: Int):super(context, attrs, defStyleAttr)

    val ROLE_TAG:String = "組長"

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> Log.i(ROLE_TAG,"清楚需求了")
        }
        return super.dispatchTouchEvent(ev)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        return super.onInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_MOVE -> Log.i(ROLE_TAG,"正在努力工做")
            MotionEvent.ACTION_UP -> Log.i(ROLE_TAG,"作好了")
        }
        return true
    }
}
複製代碼


View(Programmer.kt)

class Programmer : View {

    val ROLE_TAG:String = "程序員"

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet) : super(context,attrs)
    constructor(context: Context?, attrs: AttributeSet, defStyleAttr: Int):super(context, attrs, defStyleAttr)

    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> Log.i(ROLE_TAG,"知道要求了,我立刻作")
        }
        return super.dispatchTouchEvent(event)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_MOVE -> Log.i(ROLE_TAG,"正在努力搬磚。。。")
            MotionEvent.ACTION_UP -> Log.i(ROLE_TAG,"作好了,累死了")
        }
        return true
    }
}
複製代碼


結果:

實驗結果

遇到的問題

這是最經典最基礎的一個流程了吧,一開始還好好的在乎料之中,但有個東西讓我吃了好大的苦頭,卡在這半天了。就是在重寫這三個方法的時候他們的默認返回值都是super.dispatchTouchEvent(ev),在啥都不怎麼修改的時候,最原始的代碼是沒有問題的。(當改成true或false都不行,在有種狀況中,我打印了一下super的返回值,發現是true,但結果卻出乎個人意料,我只是先調用了super並記錄下了它返回的值,在用這個值做爲當前函數的返回值,獲得的事件結果倒是不同的)

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        var mbool:Boolean = super.dispatchTouchEvent(ev)
        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> Log.i(ROLE_TAG,"有個新需求,分配新需求" + mbool)
        }
        return mbool
    }
複製代碼

變爲了組長未卜先知了,這讓我有點納悶了,而後我直接將函數返回值改成了true和false,再也不調用super,兩個的返回值都同樣,但事件被消費了,沒有往下傳遞了,只有使用super的時候進行了向下的傳遞。(這個問題目前那麼多時間去了解,我就先將super當成一個返回值的狀態吧)

分發機制流程圖

其餘流程是我一個一個更改每一個函數的返回值爲true或false或默認的super試出來的。畫了一個事件分發與攔截的流程圖。

Android事件分發
相關文章
相關標籤/搜索