更簡單的學習Android事件分發

Idtk發表於2016-08-20

事件分發是Android中非常重要的機制,是使用者與介面互動的基礎。這篇文章將通過示例列印出的Log,繪製出事件分發的流程圖,讓大家更容易的去理解Android的事件分發機制。

一、必要的基礎知識

1、相關方法

Android中與事件分發相關的方法主要包括dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三個方法,而事件分發一般會經過三種容器,分別為Activity、ViewGroup、View。下表對這三種容器分別擁有的事件分發相關方法進行了整理。

事件相關方法 方法功能 Activity ViewGroup View
public boolean dispatchTouchEvent 事件分發 Yes Yes Yes
public boolean onInterceptTouchEvent 事件攔截 No Yes No
public boolean onTouchEvent 事件消費 Yes Yes Yes
  • 分發: dispatchTouchEvent如果返回true,則表示在當前View或者其子View(子子…View)中,找到了處理事件的View;反之,則表示沒有尋找到
  • 攔截: onInterceptTouchEvent如果返回true,則表示這個事件由當前View進行處理,不管處理結果如何,都不會再向子View傳遞這個事件;反之,則表示當前View不主動處理這個事件,除非他的子View返回的事件分發結果為false
  • 消費: onTouchEvent如果返回true,則表示當前View就是事件傳遞的終點;反之,則表示當前View不是事件傳遞的終點

2、相關事件

這篇文章中我們只考慮4種觸控事件: ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_CANAL。 事件序列:一個事件序列是指從手指觸控螢幕開始,到手指離開螢幕結束,這個過程中產生的一系列事件。一個事件序列以ACTION_DOWN事件開始,中間可能經過若干個MOVE,以ACTION_UP事件結束。 接下來我們將使用之前的文章自定義View——彈性滑動中例子來作為本文的示例,簡單增加一些程式碼即可,修改之後的程式碼請點選檢視

二、示例的預設情況

我們可以從示例程式碼的xml中看出,圖片都是可點選的。

<?xml version="1.0" encoding="utf-8"?>
<com.idtk.customscroll.ParentView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp"
    tools:context="com.idtk.customscroll.MainActivity"
    >

    <com.idtk.customscroll.ChildView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/zhiqinchun"
        android:clickable="true"/>

    <com.idtk.customscroll.ChildView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/hanzhan"
        android:clickable="true"/>

    <com.idtk.customscroll.ChildView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/shengui"
        android:clickable="true"/>

    <com.idtk.customscroll.ChildView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/dayu"
        android:clickable="true"/>

</com.idtk.customscroll.ParentView>

我們現在來點選一下,檢視下列印出的日誌。

根據列印出的log來繪製一張事件傳遞的流程圖

現在來理一下事件序列的流程:

  • ACTION_DOWN事件從Activity#dispatchTouchEvent方法開始
  • ACTION_DOWN事件傳遞至ViewGroup#dispatchTouchEvent方法,ViewGroup#onInterceptTouchEvent返回false,表示不攔截ACTION_DOWN
  • ACTION_DOWN事件傳遞到View#dispatchTouchEvent方法,在View#onTouchEvent進行執行,返回true,表示事件已經被消費
  • 返回的結果true,被回傳到View#dispatchTouchEvent,之後回傳到ACTION_DOWN事件的起點Activity#dispatchTouchEvent方法
  • ACTION_UP事件的傳遞過程與ACTION_DOWN相同,這裡不再複述

這裡使用工作中的情況來模擬一下:老闆(Activity)、專案經理(ViewGroup)、軟體工程師(View)

  • 老闆分配一個任務給專案經理(Activity#dispatchTouchEvent → ViewGroup#dispatchTouchEvent),專案經理選擇自己不做這個任務(ViewGroup#dispatchTouchEvent返回false),交由軟體工程師處理這個任務(<View#dispatchTouchEvent)(我們忽略總監與組長的情況),軟體工程師完成了這個任務(View#onTouchEvent返回true)
  • 把結果告訴專案經理(返回結果true,View#dispatchTouchEvent→ ViewGroup#dispatchTouchEvent),專案經理把結果告訴老闆(返回結果true,ViewGroup#dispatchTouchEvent→Activity#dispatchTouchEvent)
  • 專案經理完成的不錯,老闆決定把這個專案的二期、三期等都交給專案經理,同樣專案經理也覺得這個軟體工程師完成的不錯,所以也把二期、三期等都交給這個工程師來做

通過上面的傳遞過程,我們可以得出一些結論:

  • 事件總是由父元素分發給子元素
  • 某個ViewGroup如果onInterceptTouchEvent返回為false,則表示ViewGroup不攔截事件,而是將其傳遞給View#dispatchTouchEvent方法
  • 某個View如果onTouchEvent返回true,表示事件被消費,則其結果將直接通過dispatchTouchEvent方法傳遞迴Activity
  • 如果某個View消費了ACTION_DOWN事件,那麼這個事件序列中的後續事件也將交由其進行處理(有一些特殊情況除外,比如在序列中的之後事件進行攔截)

三、在View中不消費事件

我們現在修改示例程式碼的xml部分,android:clickable="true"全部修改為android:clickable="false"

<?xml version="1.0" encoding="utf-8"?>
<com.idtk.customscroll.ParentView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp"
    tools:context="com.idtk.customscroll.MainActivity"
    >

    <com.idtk.customscroll.ChildView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/zhiqinchun"
        android:clickable="false"/>

    <com.idtk.customscroll.ChildView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/hanzhan"
        android:clickable="false"/>

    <com.idtk.customscroll.ChildView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/shengui"
        android:clickable="false"/>

    <com.idtk.customscroll.ChildView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/dayu"
        android:clickable="false"/>

</com.idtk.customscroll.ParentView>

這時再點選一下,檢視新列印出的日誌

現在根據log中顯示的邏輯,分別繪製ACTION_DOWN事件與ACTION_UP事件傳遞的流程圖

我們來整理下這個事件序列的流程:

  • ACTION_DOWN事件的傳遞與之前相同,不同的地方在於,返回值的傳遞
  • 因為不可點選,View#onTouchEvent返回值為false,將其傳遞給自己的dispatchTouchEvent方法,之後傳遞到ViewGroup#dispatchTouchEvent方法,再傳遞到ViewGroup#onTouchEvent方法
  • ViewGroup返回false之後,ACTION_DOWN事件交由Activity#onTouchEvent方法進行處理,然而依舊返回false,最後ACTION_DOWN事件的返回結果即為false
  • ACTION_UP事件在發現View、ViewGroup並不處理ACTION_DOWN事件後,直接將其傳遞給了Activity#onTouch方法處理,處理返回false,ACTION_UP事件的返回結果即為false

這裡使用工作中的情況來模擬:依舊是老闆(Activity)、專案經理(ViewGroup)、軟體工程師(View) 從老闆交任務給專案經理,專案經理交任務給工程師,這一段流程和之前的例子相同。不同之處是軟體工程師沒有完成這個任務(View#onTouchEvent返回false),告訴專案經理我沒有完成,然後專案經理自己進行了嘗試,同樣沒有完成(ViewGroup#onTouchEvent返回false),專案經理告訴了老闆,我沒有完成,然後老闆自己試了下也沒有完成這個任務(Activity#onTouchEvent返回false),但之後的也有專案的二期、三期,不過老闆知道你們完成不了,所以都是他自己進行嘗試,不過很慘都沒完成。(這段有點與正常情況不同,不過只是打個比方)

通過結合上面兩個例子,可以得出一些結論:

  • 某個View如果onTouchEvent返回false,表示事件沒有被消費,則事件將傳遞給其父View的onTouchEvent進行處理
  • 某個View如果它不消耗ACTION_DOWN事件,那麼這個序列的後續事件也不會再交由它來處理
  • 如果事件沒有View對其進行處理,那麼最後將有Activity進行處理
  • View預設的onTouchEvent在View可點選的情況下,將會消耗事件,返回true;不可點選的情況下,則不消耗事件,返回false(longClickable的情況,讀者可以自行測試,結果與clickable相同)

四、在ViewGroup中攔截事件

事件分發中攔截的情況,這裡我把它分為2種,一種是在ACTION_DOWN事件時,就進行攔截的;另一種是在ACTION_DOWN之後的事件序列中,對事件進行了攔截。

1、在事件開始時攔截

為了達到在ViewGroup中,一開始就攔截觸控事件的效果,我們需要進行修改,在ParentView#onInterceptTouchEvent方法的最後部分,我註釋掉的intercept=true;進行恢復,然後為activity_main.xml中的ParentView增加android:clickable="true"屬性。

<?xml version="1.0" encoding="utf-8"?>
<com.idtk.customscroll.ParentView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp"
    tools:context="com.idtk.customscroll.MainActivity"
    >

    <com.idtk.customscroll.ChildView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/zhiqinchun"
        android:clickable="true"/>

    <com.idtk.customscroll.ChildView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/hanzhan"
        android:clickable="true"/>

    <com.idtk.customscroll.ChildView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/shengui"
        android:clickable="true"/>

    <com.idtk.customscroll.ChildView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/dayu"
        android:clickable="true"/>

</com.idtk.customscroll.ParentView>

我們現在來看下攔截情況下的事件流程圖

這裡大部分和之前的例子相同,主要的區別是在於ViewGroup#onInterceptTouchEvent方法中,對傳遞的事件進行了攔截,返回true,ACTION_DOWN事件就傳遞到了ViewGroup#onTouchEvent中進行處理,ACTION_DOWN事件之後的傳遞就與之前的例子相同了。另一點重要的區別是,在ViewGroup攔截下事件之後,此事件序列的其餘事件,在進入ViewGroup#dispatchTouchEvent方法之後,不在需要進行是否攔截事件的判斷,而是直接進入了onTouchEvent方法之中。

使用工作中的情況來模擬:老闆(Activity)、專案經理(ViewGroup)、軟體工程師(View) 老闆吧任務交給專案經理,專案經理認為這個專案比較難,所以決定自己處理(ViewGroup#onInterceptTouchEvent,return true),專案經理比較厲害他把任務完成了(ViewGroup#onTouchEvent,return true),然後他告訴老闆他完成了(return true,ViewGroup#dispatchTouchEvent→Activity#dispatchTouchEvent)。之後老闆依舊會把任務交給專案經理,專案經理知道這個任務難度,所以不假思索(也就是這個事件序列中的其餘事件沒有經過ViewGroup#onInterceptTouchEvent)的自己來做。

通過上面的例子,可以得出一些結論:

  • 某個ViewGroup如果onInterceptTouchEvent返回為true,則ViewGroup攔截事件,將事件傳遞給其onTouchEvent方法進行處理
  • 某個ViewGroup如果它的onInterceptTouchEvent返回為true,那麼這個事件序列中的後續事件,不會在進行onInterceptTouchEvent的判斷,而是由它的dispatchTouchEvent方法直接傳遞給onTouchEvent方法進行處理

2、在事件序列中攔截

這裡把使用的示例恢復到初始狀態,然後把我在ParentView#onInterceptTouchEvent方法,switch內的兩個註釋掉的intercept = true;程式碼進行恢復,最後部分intercept = true;再次註釋掉。

<?xml version="1.0" encoding="utf-8"?>
<com.idtk.customscroll.ParentView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp"
    tools:context="com.idtk.customscroll.MainActivity"
    >

    <com.idtk.customscroll.ChildView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/zhiqinchun"
        android:clickable="true"/>

    <com.idtk.customscroll.ChildView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/hanzhan"
        android:clickable="true"/>

    <com.idtk.customscroll.ChildView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/shengui"
        android:clickable="true"/>

    <com.idtk.customscroll.ChildView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/dayu"
        android:clickable="true"/>

</com.idtk.customscroll.ParentView>

重新執行之後,滑動一個圖片,來看看Log

這裡分成兩張圖片,是因為中間有很多ACTION_MOVE,為了方便觀察,所以只擷取了Log的首尾部分。 這裡的關鍵部分,就是紅框中的ACTION_CANCEL,可以看到ACTION_DOWN事件的傳遞時onInterceptTouchEvent並沒有攔截,返回false,在其後的事件ACTION_MOVE再次進入onInterceptTouchEvent時,ViewGroup對事件進行了攔截,這樣將會對View傳遞一個ACTION_CANCEL事件,之後的ACTION_MOVE事件就不再傳遞給View了。

使用工作中的情況來模擬:老闆(Activity)、專案經理(ViewGroup)、軟體工程師(View) 這裡的情況就是,一期的任務和第一個例子一樣的情況一樣,由軟體工程師完成,不過忽然專案經理覺得二期的任務有點難,然後決定自己完成。這時就給工程師說,這個專案的後續任務,不要你來完成了(ACTION_CANCEL)。

從這裡也可以得出一個結論:

  • 某個View接收了ACTION_DOWN之後,這個序列的後續事件中,如果在某一刻被父View攔截了,則這個字View會收到一個ACTION_CANCEL事件,並且也不會再收到這個事件序列中的後續事件。

五、小結

本文通過示例列印出的各種Log對Android的事件分發機制進行,得出如下結論。

  • 一個事件序列是指從手指觸控螢幕開始,到手指離開螢幕結束,這個過程中產生的一系列事件。一個事件序列以ACTION_DOWN事件開始,中間可能經過若干個MOVE,以ACTION_UP事件結束。
  • 事件的傳遞過程是由外向內的,即事件總是由父元素分發給子元素
  • 如果某個View消費了ACTION_DOWN事件,那麼通常情況下,這個事件序列中的後續事件也將交由其進行處理,但可以通過呼叫其父View的onInterceptTouchEvent方法,對後續事件進行攔截
  • 如果某個View它不消耗ACTION_DOWN事件,那麼這個序列的後續事件也不會再交由它來處理
  • 如果事件沒有View對其進行處理,那麼最後將有Activity進行處理
  • 如果事件傳遞的結果為true,回傳的結果直接通過不斷呼叫父View#dispatchTouchEvent方法,傳遞給Activity;如果事件傳遞的結果為false,回傳的結果不斷呼叫父View#onTouchEvent方法,獲取返回結果。
  • View預設的onTouchEvent在View可點選的情況下,將會消耗事件,返回true;不可點選的情況下,則不消耗事件,返回false(longClickable的情況,讀者可以自行測試,結果與clickable相同)
  • 如果某個ViewGroup的onInterceptTouchEvent返回為true,那麼這個事件序列中的後續事件,不會在進行onInterceptTouchEvent的判斷,而是由它的dispatchTouchEvent方法直接傳遞給onTouchEvent方法進行處理
  • 如果某個View接收了ACTION_DOWN之後,這個序列的後續事件中,在某一刻被父View攔截了,則這個字View會收到一個ACTION_CANCEL事件,並且也不會再收到這個事件序列中的後續事件
事件相關方法 方法功能 Activity ViewGroup View
public boolean dispatchTouchEvent 事件分發 Yes Yes Yes
public boolean onInterceptTouchEvent 事件攔截 No Yes No
public boolean onTouchEvent 事件消費 Yes Yes Yes

相關文章