Android觸摸事件解析(自定義DrawerLayout)

基礎知識概述:

首先來了解三個方法:

  • dispatchTouchEvent(MotionEvent ev) 功能:事件的分發
  • onInterceptTouchEvent(MotionEvent ev) 功能:事件的攔截
  • onTouchEvent(MotionEvent ev) 功能:事件的消費
    可以看到這三個方法里面都包含了同一個類:MotionEvent 類,這個類里面包含了很多的代表觸摸動作的常量,比如:
    • MotionEvent.ACTION_DOWN: 代表手指按下的動作
    • MotionEvent.ACTION_MOVE: 代表手指滑動的動作
    • MotionEvent.ACTION_CANCEL: 代表手指動作取消的動作
    • MotionEvent.ACTION_UP: 代表手指動作抬起的動作
      這里我們也是圍繞著這四個動作進行解析,因為:** 從手指觸摸屏幕,到手指離開屏 幕,會經歷一次 ACTION_DOWN、若干次ACTION_MOVE或ACTION_CANCEL、和一次ACTION_UP,這也被稱為一個事件序列。**

之后我們在說一下能夠觸發這三個方法傳遞觸摸事件的容器:

  • Activity 事件就是從它開始往下分發的,這個容器里包含了dispatchTouchEvent()和onTouchEvent(),也就是擁有分發事件和消費事件的能力。
  • ViewGroup 它接收Activity的事件分發,并且可以選擇是否繼續往下傳遞還是自己直接進行消費,所以它包含了上面三個方法,具有事件的分發、攔截、消費功能。
  • View 如果它的父容器不進行攔截的話,它接受父容器的事件分發,并且判斷是否消費事件,并且要把結果再通過dispatchTouchEvent()方法返回給父容器。
    用表格的形式展現下容器和方法之間的關系:

事件相關方法 | 方法功能 | 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沒有消費該事件,返回到父控件處理(如果有父控件的話)

代碼示例

需求:我想用DrawerLayou類來做滑動側邊看,但是有一個問題,就是當側邊欄打開的時候,主屏幕會變暗并且不能觸發點擊事件,我現在想通過自定義的形式解決這一問題,并且當側邊欄打開的時候點擊主界面的時候側邊欄不會自定收起。

先看下效果圖:


圖片.png

再看下布局的資源文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:titleTextColor="@color/colorAccent" />
    
    <com.mllwf.slidesidebar.TEDrawerLayout
        android:id="@+id/drawer_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.mllwf.slidesidebar.CustormFrameLayout
            android:id="@+id/container"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <com.mllwf.slidesidebar.CustormButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:gravity="center"
                android:onClick="toCanSlieDrawer"
                android:padding="16dp"
                android:text="是否能滑動" />
        </com.mllwf.slidesidebar.CustormFrameLayout>

        <RelativeLayout
            android:id="@+id/nav_view"
            android:layout_width="150dp"
            android:layout_height="match_parent"
            android:layout_gravity="start"
            android:background="@android:color/darker_gray">

            <Button
                android:id="@+id/btn_one"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/colorPrimary"
                android:text="這是頂部按鈕"
                android:textColor="@color/colorAccent" />

            <Button
                android:id="@+id/btn_two"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:background="@color/colorPrimary"
                android:text="這是中間的按鈕"
                android:textColor="@color/colorAccent" />

            <Button
                android:id="@+id/btn_three"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_alignParentBottom="true"
                android:background="@color/colorPrimary"
                android:text="這是底部按鈕"
                android:textColor="@color/colorAccent" />
        </RelativeLayout>
    </com.mllwf.slidesidebar.TEDrawerLayout>
</LinearLayout>

簡要說明(請結合效果圖和布局文件):

  • 為了能讓側邊欄在標題欄的下面,我在DrawerLayout布局放在了ToolBar布局的下面,而根布局我用的是線性布局。
  • 可以看的我自定義了DrawerLayout類,這樣做為了重寫上面三個觸摸事件分發,并且方便打印Log日志
  • DrawerLayout容器里面的第一個布局代表的是主界面布局,這里我用的是自定義的FrameLayout這樣是也是為了重寫事件分發和打印日志(注意重寫方法為了實現上面說的功能,和打印Log)
  • 在主界面布局里面我放了一個自定義的按鈕這只是為了方便打印日志用。
  • DrawerLayout容器里面第二個布局代表的是側邊欄布局。

下面分別看下分別看些這幾個自定義布局的相關代碼:

  • MainActivity.class(Activity)


    Activity觸摸事件方法.png
  • CustormFrameLayout(ViewGroup)


    ViewGroup觸摸事件.png
  • CustormButton(View)


    View事件觸摸方法.png

首先默認沒有打開側邊欄,點擊按鈕查看日志信息:


圖片.png

從上面的日志信息可以看到,是一層層的傳遞的,這里用一張圖解釋下上面的傳遞流程:


只考慮了Down和Up動作.png
只考慮了Down和Up動作.png

因為Up事件和Down事件是一樣的過程,所以就不多說了。
從上面的Log和流程圖我們可以得出幾點結論:
  • ACTION_DOWN事件從Activity#dispatchTouchEvent方法開始,然后一層層往下傳遞的,并且 ACTION_DOWN事件是否被消費的結果最終也是有dispatchTouchEvent傳回到Activity中。
  • ACTION_DOWN事件傳遞至ViewGroup#dispatchTouchEvent方法,ViewGroup#onInterceptTouchEvent返回false,表示不攔截ACTION_DOWN,如果返回的是true,表示攔截不會再傳遞給子View,這個時候就會調用ViewGroup#onTouchEvent方法,判斷是否消費事件,并回傳給Activity的#dispatchTouchEvent方法
  • ACTION_DOWN事件傳遞到View#dispatchTouchEvent方法,在View#onTouchEvent進行執行,返回true,表示事件已經被消費,然后被回傳到View#dispatchTouchEvent,之后回傳到ACTION_DOWN事件的起點Activity#dispatchTouchEvent方法。如果這時候返回的是false的話,ViewGroup就是調用自己的onTouchEvent方法進行處理,如果依然返回false的話,Activity就會調用自己的onTouchEvent方法處理(返回false的情況后面解釋)。
  • 如果某個View消費了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)
  • 項目經理完成的不錯,老板決定把這個項目的二期、三期等都交給項目經理,同樣項目經理也覺得這個軟件工程師完成的不錯,所以也把二期、三期等都交給這個工程師來做

默認情況下View是消費觸摸事件的,現在我們再來看看當View不消費觸摸事件(例如:clickable為false)的時候整個流程是怎么樣的:

View不消費觸摸事件.png
View不消費觸摸事件.png

流程圖:

ActivityDown.png
ActivityDown.png
ActivityUp.png
ActivityUp.png

原則:

  • 當View沒有對Down動作進行事件消費的時候,就交由父布局處理,如果父布局也沒有消費的時候就交由Activity處理
  • 如果View沒有對Down動作進行事件消費,那么后續的動作也不在交由它處理,而是直接交由處理的了Down動作的容器進行處理。

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

攔截事件

這里就直接貼圖看下Log吧:


攔截事件
攔截事件

流程圖:


Down
Down

Up
Up
結論
  • ViewGroup攔截事件之后,直接由自己的onTouchEvent()方法進行處理,并最終將結果返回給Activity
  • ViewGroup攔截事件之后,后續的動作事件序列將不再調用onInterceptTouchEvent()判斷是否攔截,而是直接由自己進行處理。

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

通過上面攔截的敘述就知道為什么在側邊欄打開的時候,不能點擊主界面了,因為觸摸事件被攔截了,所以只要進行好判斷在適當的時候不進行攔截就可以了。


圖片.png

至于點擊主界面不讓側邊欄收回,就讓主界面在動作為Activity_Up的時候消費事件(onTouchEvent()返回true)就可以了(因為DrawerLayout類在onTouchEvent()方法里關閉了側邊欄)


ViewGroup#onTouchEvent().png
DrawerLayout#onTouchEvent().png

最后在總結一下:

  • 一個事件序列是指從手指觸摸屏幕開始,到手指離開屏幕結束,這個過程中產生的一系列事件。一個事件序列以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事件,并且也不會再收到這個事件序列中的后續事件

項目源碼
參考文章
有問題歡迎聯系我(1021423736@qq.com)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容