1. 程式人生 > >Android開發知識(七):Android事件處理機制:事件分發、傳遞、攔截、處理機制的原理分析(上)

Android開發知識(七):Android事件處理機制:事件分發、傳遞、攔截、處理機制的原理分析(上)

  在我們剛開始學習安卓的時候,總會一開始就接觸到Button,也就是對按鈕進行一個事件監聽的事件,當我們點選螢幕上的按鈕時就可以觸發一個點選事件。那麼,從我們點選螢幕到按鈕觸發事件這個過程,是什麼樣子的呢?本文我們就來談一下關於事件攔截處理機制的基本知識。

  我們知道,在Android中,View檢視是以樹形結構來展示的,也就是說,一個ViewGroup既可以可以裝入若干個View,也可以在ViewGroup裡面再巢狀若干個ViewGroup,那麼對於一個事件,子View或者父ViewGroup都可能要處理,因此就須有有一些“規則”來定義這個事件處理機制。

  為了讓初學者可以更容易理解這個事件處理機制的過程,本文先講最基礎的方面,把事件處理機制的分析過程放在後一章節再講。
  在我們觸控式螢幕幕的過程中,可以分為三種情況,分別是按下、滑動、彈起。Android中為我們封裝好了一個MotionEvent類,使得我們對螢幕的一系列操作事件都可以記錄在這個MotionEvent裡面。
  這三種情況分別對應MotionEvent的:
  MotionEvent.ACTION_UP
  MotionEvent.ACTION_MOVE
  MotionEvent.ACTION_DOWN
在我們手指按下的時候觸發MotionEvent.ACTION_UP事件,可能我們手指不小心一動,會觸發MotionEvent.ACTION_MOVE滑動事件,接著手指離開螢幕,會觸發MotionEvent.ACTION_DOWN事件,我們把這個過程成為一個事件序列,也就是說一個事件序列裡面必然包含有ACTION_UP事件和ACTION_DOWN事件,如果有伴隨著滑動的話則就有包含ACTION_MOVE事件。MotionEvent還封裝了其他很多事件的資訊,比如座標、時間等等。

  在ViewGroup中,涉及到事件處理過程的有三個重要的方法,分別是:

public boolean dispatchTouchEvent(MotionEvent ev) 

public boolean onTouchEvent(MotionEvent event) 

public boolean onInterceptTouchEvent(MotionEvent ev) 

  其中,從方法名來看也不難看出,dispatchTouchEvent方法是用來進行事件分發,onTouchEvent是對事件的處理,onInterceptTouchEvent是對事件的攔截。

  在本文中,我們先不要去分析這幾個方法的原始碼,我們只需要瞭解這個過程:
  當事件傳遞到一個ViewGroup上面時,ViewGroup會觸發dispatchTouchEvent方法,隨後呼叫onInterceptTouchEvent方法確認是否攔截此事件,最後如果事件是自己來處理的話,則呼叫onTouchEvent方法。
  在ViewGroup類中,onInterceptTouchEvent方法總是返回false,表示預設是不攔截事件的,除非去重寫ViewGroup類來返回true。而onTouchEvent方法的返回值表示是否消費(返回true則消費)此事件,消費的意思就是說ViewGroup自己處理了這個事件,不再傳遞到上一層的onTouchEvent去。

  而在View中,與ViewGroup相比,同樣有dispatchTouchEvent方法和onTouchEvent方法。但是沒有onInterceptTouchEvent這個方法,因為在一個View中,已經是View樹的葉子節點,它沒有下一級的檢視巢狀,所以不需要決定是否攔截事件,它自己就可以處理事件了。

  在View類中,只要該View是可以點選的,那麼預設都會在onTouchEvent返回true,表示自己消費了這個事件,不再傳遞到上一級ViewGroup去。

  事件傳遞的過程其實非常好理解,想像一下這麼一個情景:在公司裡BOSS給總監下達了一個任務,總監把這個任務派給了經理,而經理把這個任務派給了你,你把這個任務幹完了,向經理彙報,隨後經理簽名確認後向總監彙報,總監簽名確認後再彙報給BOSS,這個任務就算完成了。

  在這裡,總監就相當於是一個ViewGroup,經理也是一個ViewGroup,而你是一個View。總監把這個任務利用dispatchTouchEvent方法分發給了經理,自己呼叫onInterceptTouchEvent方法返回false,表示自己不做這個任務。然後經理把這個任務利用dispatchTouchEvent方法分發給了你,同樣自己呼叫onInterceptTouchEvent方法返回false,表示自己不做這個任務。而你則呼叫了dispatchTouchEvent方法後只能自己來幹這個苦差事,所以呼叫了onTouchEvent方法,並且返回了true,表示自己完成了任務(消費這個事件)。那麼經理和總監就不用去幹這個件事,所以他們就不呼叫onTouchEvent方法。

  假如出現了一個情況,你搞不定這個任務,只能請求經理去幫忙,那麼你就在onTouchEvent方法後返回了false,表示自己沒完成這個任務(不消費此事件),那麼隨後經理就會呼叫onTouchEvent方法,如果自己能完成,那麼他就返回true(消耗事件),如果經理自己也搞不定那麼就只能返回false,讓總監去呼叫onTouchEvent方法自己去處理這個任務。

  好了說到這裡,究竟是不是這樣呢,我們來用程式碼驗證一下,我們分別建立RelativeLayoutA、RelativeLayoutB,都繼承自RelativeLayout,也等同於是ViewGroup,再建立一個MyView繼承自Button類,也等同於是繼承View。
在RelativeLayoutA類中重寫上面提到的三個方法,分別打印出他們的方法名:

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("lc_miao","RelativeLayoutA : dispatchTouchEvent");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("lc_miao","RelativeLayoutA : onTouchEvent");
        return super.onTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("lc_miao","RelativeLayoutA : onInterceptTouchEvent");
        return super.onInterceptTouchEvent(ev);
    }

  同理,在RelativeLayoutB類中也重寫這三個方法。

  在MyView中,重寫兩個方法(注意View是沒有onInterceptTouchEvent方法的):

   @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i("lc_miao","MyView : dispatchTouchEvent");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("lc_miao","MyView :  onTouchEvent");
        return super.onTouchEvent(event);
    }

  隨後,我們建立一個Activity,還有一個佈局檔案如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.android7.EventsActivity">
<com.example.android7.RelativeLayoutA
    android:id="@+id/RelativeLayoutA"
    android:layout_width="300dp"
    android:layout_height="300dp"
    android:layout_centerInParent="true"
    android:background="#ffff00"

    >
    <com.example.android7.RelativeLayoutB
        android:id="@+id/RelativeLayoutB"
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:layout_centerInParent="true"
        android:background="#00ff00"
        >
            <com.example.android7.MyView
                android:id="@+id/Button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="按鈕"
                android:layout_centerInParent="true"
                />

    </com.example.android7.RelativeLayoutB>

</com.example.android7.RelativeLayoutA>
</RelativeLayout>

  執行,介面截圖如下:

執行截圖

  可以看到最外層黃色的就是RelativeLayoutA,而中間那層青色的就是RelativeLayoutB,最中間的則是按鈕。
我們點選一下按鈕,檢視log:

BUTTON攔截

  通過log的列印,其實分為兩個部分,我用紅線分割開來,當我們按下螢幕的時候,列印順序則是這三個控制元件的事件攔截所被呼叫的順序

  因為Button預設是可以點選的(即使我們並沒有設定點選監聽事件),所以MyView打印出了onTouchEvent,隨後返回了true,這個ACTION.DOWN事件就被MyView消耗掉了。

  如果MyView自己不處理任務呢?我們把MyView的onTouchEvent事件返回false,編譯執行後點擊中間的按鈕,再看下列印:

不處理

  從Log中我們發現,只要MyView的的onTouchEvent事件返回false,那麼RelativeLayoutB的onTouchEvent就會被呼叫,由於預設是返回false,最終再給到最上級RelativeLayoutA去。
在這裡有個疑問,log只是打印出了ACTION.DOWN的列印,並沒有像上面的log一樣打印出ACTION.UP,這個問題要留到下一個博文我們深入原始碼分析後才可以給出答案,在這裡我們暫時只需要知道,也是必須重要的一個點就是:

  如果在同一個事件序列裡面,如果ACTION.DOWN事件不被這個View做出消耗,則後面陸續的事件序列則不會傳遞到這個View來。

  在這個Log裡面由於MyView不處理事件,而RelativeLayoutB和RelativeLayoutA其實也是不處理自己事件的,最後交由了更高級別的ViewGroup(Activity)去響應了,所以後面的ACTION.UP不會再傳遞到這幾個控制元件上來了。

  假如我們在RelativeLayoutB中讓onInterceptTouchEvent返回true,表示RelativeLayoutB會攔截事件自己處理,不分發給下一級View樹處理,編譯執行後點擊中間的按鈕,我們再來看看Log:

攔截

  從這個Log中可以看出,MyView並沒有打印出來,說明他沒有接收到事件,因為RelativeLayoutB已經把事件給攔截了,就不再分發給MyView,而RelativeLayoutB把事件攔截了後自己呼叫onTouchEvent,預設是沒有消耗事件的,所以才會再呼叫RelativeLayoutB的onTouchEvent方法。

  注意事件攔截和事件消費是兩回事,事件攔截說的是不把事件發給下一級View,而事件消費說的是處理完這個事件還要不要讓上一級也處理,如果消費了事件那麼就不會再讓上一級處理這個事件。

  關於事件攔截處理機制的基本分析,我們就講到這裡,在下一文章中我們再來深入Android原始碼解剖事件處理機制的實現過程。