基礎(chǔ)知識概述:
首先來了解三個方法:
- dispatchTouchEvent(MotionEvent ev) 功能:事件的分發(fā)
- onInterceptTouchEvent(MotionEvent ev) 功能:事件的攔截
- onTouchEvent(MotionEvent ev) 功能:事件的消費
可以看到這三個方法里面都包含了同一個類:MotionEvent 類橡伞,這個類里面包含了很多的代表觸摸動作的常量制轰,比如:- MotionEvent.ACTION_DOWN: 代表手指按下的動作
- MotionEvent.ACTION_MOVE: 代表手指滑動的動作
- MotionEvent.ACTION_CANCEL: 代表手指動作取消的動作
- MotionEvent.ACTION_UP: 代表手指動作抬起的動作
這里我們也是圍繞著這四個動作進行解析缆八,因為:** 從手指觸摸屏幕潜秋,到手指離開屏 幕蛔琅,會經(jīng)歷一次 ACTION_DOWN、若干次ACTION_MOVE或ACTION_CANCEL峻呛、和一次ACTION_UP罗售,這也被稱為一個事件序列。**
之后我們在說一下能夠觸發(fā)這三個方法傳遞觸摸事件的容器:
- Activity 事件就是從它開始往下分發(fā)的钩述,這個容器里包含了dispatchTouchEvent()和onTouchEvent(),也就是擁有分發(fā)事件和消費事件的能力寨躁。
- ViewGroup 它接收Activity的事件分發(fā),并且可以選擇是否繼續(xù)往下傳遞還是自己直接進行消費牙勘,所以它包含了上面三個方法职恳,具有事件的分發(fā)、攔截方面、消費功能放钦。
- View 如果它的父容器不進行攔截的話,它接受父容器的事件分發(fā)恭金,并且判斷是否消費事件操禀,并且要把結(jié)果再通過dispatchTouchEvent()方法返回給父容器。
用表格的形式展現(xiàn)下容器和方法之間的關(guān)系:
事件相關(guān)方法 | 方法功能 | Activity | ViewGroup | View
---||||---|||---
public boolean dispatchTouchEvent | 事件分發(fā) | Yes | Yes | Yes
public boolean onInterceptTouchEvent | 事件攔截 | No | Yes | No
public boolean onTouchEvent | 事件消費 | Yes | Yes | Yes
- 分發(fā) : dispatchTouchEvent如果返回true横腿,則表示在當(dāng)前View或者其子View(子子...View)中颓屑,找到了處理事件的View斤寂;反之,則表示沒有尋找到揪惦。
- 攔截: onInterceptTouchEvent如果返回true遍搞,則表示這個事件由當(dāng)前View進行處理,不管處理結(jié)果如何丹擎,都不會再向子View傳遞這個事件尾抑;反之,則表示當(dāng)前View不主動處理這個事件蒂培,除非他的子View返回的事件分發(fā)結(jié)果為false再愈。
- 消費: onTouchEvent如果返回true,則表示當(dāng)前View消費了該事件护戳;反之翎冲,則表示當(dāng)前View沒有消費該事件,返回到父控件處理(如果有父控件的話)
代碼示例
需求:我想用DrawerLayou類來做滑動側(cè)邊看媳荒,但是有一個問題抗悍,就是當(dāng)側(cè)邊欄打開的時候,主屏幕會變暗并且不能觸發(fā)點擊事件钳枕,我現(xiàn)在想通過自定義的形式解決這一問題缴渊,并且當(dāng)側(cè)邊欄打開的時候點擊主界面的時候側(cè)邊欄不會自定收起。
先看下效果圖:
再看下布局的資源文件:
<?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>
簡要說明(請結(jié)合效果圖和布局文件):
- 為了能讓側(cè)邊欄在標(biāo)題欄的下面鱼炒,我在DrawerLayout布局放在了ToolBar布局的下面衔沼,而根布局我用的是線性布局。
- 可以看的我自定義了DrawerLayout類昔瞧,這樣做為了重寫上面三個觸摸事件分發(fā)指蚁,并且方便打印Log日志
- DrawerLayout容器里面的第一個布局代表的是主界面布局,這里我用的是自定義的FrameLayout這樣是也是為了重寫事件分發(fā)和打印日志(注意重寫方法為了實現(xiàn)上面說的功能自晰,和打印Log)
- 在主界面布局里面我放了一個自定義的按鈕這只是為了方便打印日志用凝化。
- DrawerLayout容器里面第二個布局代表的是側(cè)邊欄布局。
下面分別看下分別看些這幾個自定義布局的相關(guān)代碼:
-
MainActivity.class(Activity)
-
CustormFrameLayout(ViewGroup)
-
CustormButton(View)
首先默認沒有打開側(cè)邊欄酬荞,點擊按鈕查看日志信息:
從上面的日志信息可以看到搓劫,是一層層的傳遞的,這里用一張圖解釋下上面的傳遞流程:
因為Up事件和Down事件是一樣的過程混巧,所以就不多說了枪向。
從上面的Log和流程圖我們可以得出幾點結(jié)論:
- ACTION_DOWN事件從Activity#dispatchTouchEvent方法開始,然后一層層往下傳遞的牲剃,并且 ACTION_DOWN事件是否被消費的結(jié)果最終也是有dispatchTouchEvent傳回到Activity中遣疯。
- ACTION_DOWN事件傳遞至ViewGroup#dispatchTouchEvent方法,ViewGroup#onInterceptTouchEvent返回false,表示不攔截ACTION_DOWN缠犀,如果返回的是true虐急,表示攔截不會再傳遞給子View滔迈,這個時候就會調(diào)用ViewGroup#onTouchEvent方法燎悍,判斷是否消費事件俄删,并回傳給Activity的#dispatchTouchEvent方法
- ACTION_DOWN事件傳遞到View#dispatchTouchEvent方法鸽粉,在View#onTouchEvent進行執(zhí)行帚戳,返回true庐椒,表示事件已經(jīng)被消費笔宿,然后被回傳到View#dispatchTouchEvent,之后回傳到ACTION_DOWN事件的起點Activity#dispatchTouchEvent方法。如果這時候返回的是false的話重归,ViewGroup就是調(diào)用自己的onTouchEvent方法進行處理,如果依然返回false的話违柏,Activity就會調(diào)用自己的onTouchEvent方法處理(返回false的情況后面解釋)畜伐。
- 如果某個View消費了ACTION_DOWN事件,那么這個事件序列中的后續(xù)事件也將交由其進行處理(有一些特殊情況除外,比如在序列中的之后事件進行攔截)
這里使用工作中的情況來模擬一下吧:老板(Activity)、項目經(jīng)理(ViewGroup)、軟件工程師(View)
- 老板分配一個任務(wù)給項目經(jīng)理(Activity#dispatchTouchEvent→ViewGroup#dispatchTouchEvent),項目經(jīng)理選擇自己不做這個任務(wù)(ViewGroup#dispatchTouchEvent返回false),交由軟件工程師處理這個任務(wù)(View#dispatchTouchEvent)(我們忽略總監(jiān)與組長的情況),軟件工程師完成了這個任務(wù)(View#onTouchEvent返回true)
- 把結(jié)果告訴項目經(jīng)理(返回結(jié)果true,View#dispatchTouchEvent→ViewGroup#dispatchTouchEvent),項目經(jīng)理把結(jié)果告訴老板(返回結(jié)果true,ViewGroup#dispatchTouchEvent→Activity#dispatchTouchEvent)
- 項目經(jīng)理完成的不錯,老板決定把這個項目的二期、三期等都交給項目經(jīng)理,同樣項目經(jīng)理也覺得這個軟件工程師完成的不錯晒夹,所以也把二期、三期等都交給這個工程師來做
默認情況下View是消費觸摸事件的姊氓,現(xiàn)在我們再來看看當(dāng)View不消費觸摸事件(例如:clickable為false)的時候整個流程是怎么樣的:
流程圖:
原則:
- 當(dāng)View沒有對Down動作進行事件消費的時候读跷,就交由父布局處理,如果父布局也沒有消費的時候就交由Activity處理
- 如果View沒有對Down動作進行事件消費烫映,那么后續(xù)的動作也不在交由它處理识补,而是直接交由處理的了Down動作的容器進行處理族淮。
這里使用工作中的情況來模擬:依舊是老板(Activity)贴妻、項目經(jīng)理(ViewGroup)、軟件工程師(View) 從老板交任務(wù)給項目經(jīng)理蝙斜,項目經(jīng)理交任務(wù)給工程師名惩,這一段流程和之前的例子相同。不同之處是軟件工程師沒有完成這個任務(wù)(View#onTouchEvent返回false)孕荠,告訴項目經(jīng)理我沒有完成娩鹉,然后項目經(jīng)理自己進行了嘗試,同樣沒有完成(ViewGroup#onTouchEvent返回false)稚伍,項目經(jīng)理告訴了老板弯予,我沒有完成,然后老板自己試了下也沒有完成這個任務(wù)(Activity#onTouchEvent返回false),但之后的也有項目的二期个曙、三期锈嫩,不過老板知道你們完成不了,所以都是他自己進行嘗試垦搬,不過很慘都沒完成呼寸。(這段有點與正常情況不同,不過只是打個比方)
攔截事件
這里就直接貼圖看下Log吧:
流程圖:
結(jié)論
- ViewGroup攔截事件之后猴贰,直接由自己的onTouchEvent()方法進行處理等舔,并最終將結(jié)果返回給Activity
- ViewGroup攔截事件之后,后續(xù)的動作事件序列將不再調(diào)用onInterceptTouchEvent()判斷是否攔截糟趾,而是直接由自己進行處理慌植。
使用工作中的情況來模擬:老板(Activity)、項目經(jīng)理(ViewGroup)义郑、軟件工程師(View) 老板吧任務(wù)交給項目經(jīng)理蝶柿,項目經(jīng)理認為這個項目比較難,所以決定自己處理(ViewGroup#onInterceptTouchEvent,return true)非驮,項目經(jīng)理比較厲害他把任務(wù)完成了(ViewGroup#onTouchEvent,return true)交汤,然后他告訴老板他完成了(return true,ViewGroup#dispatchTouchEvent→Activity#dispatchTouchEvent)。之后老板依舊會把任務(wù)交給項目經(jīng)理劫笙,項目經(jīng)理知道這個任務(wù)難度芙扎,所以不假思索(也就是這個事件序列中的其余事件沒有經(jīng)過ViewGroup#onInterceptTouchEvent)的自己來做。
通過上面攔截的敘述就知道為什么在側(cè)邊欄打開的時候填大,不能點擊主界面了戒洼,因為觸摸事件被攔截了,所以只要進行好判斷在適當(dāng)?shù)臅r候不進行攔截就可以了允华。
至于點擊主界面不讓側(cè)邊欄收回圈浇,就讓主界面在動作為Activity_Up的時候消費事件(onTouchEvent()返回true)就可以了(因為DrawerLayout類在onTouchEvent()方法里關(guān)閉了側(cè)邊欄)
最后在總結(jié)一下:
- 一個事件序列是指從手指觸摸屏幕開始寥掐,到手指離開屏幕結(jié)束,這個過程中產(chǎn)生的一系列事件磷蜀。一個事件序列以ACTION_DOWN事件開始召耘,中間可能經(jīng)過若干個MOVE,以ACTION_UP事件結(jié)束褐隆。
- 事件的傳遞過程是由外向內(nèi)的污它,即事件總是由父元素分發(fā)給子元素
- 如果某個View消費了ACTION_DOWN事件,那么通常情況下庶弃,這個事件序列中的后續(xù)事件也將交由其進行處理轨蛤,但可以通過調(diào)用其父View的onInterceptTouchEvent方法,對后續(xù)事件進行攔截
- 如果某個View它不消耗ACTION_DOWN事件虫埂,那么這個序列的后續(xù)事件也不會再交由它來處理
- 如果事件沒有View對其進行處理祥山,那么最后將有Activity進行處理
- 如果事件傳遞的結(jié)果為true,回傳的結(jié)果直接通過不斷調(diào)用父View#dispatchTouchEvent方法掉伏,傳遞給Activity缝呕;如果事件傳遞的結(jié)果為false,回傳的結(jié)果不斷調(diào)用父View#onTouchEvent方法斧散,獲取返回結(jié)果供常。
- View默認的onTouchEvent在View可點擊的情況下,將會消耗事件鸡捐,返回true栈暇;不可點擊的情況下,則不消耗事件箍镜,返回false(longClickable的情況源祈,讀者可以自行測試,結(jié)果與clickable相同)
- 如果某個ViewGroup的onInterceptTouchEvent返回為true色迂,那么這個事件序列中的后續(xù)事件香缺,不會在進行onInterceptTouchEvent的判斷,而是由它的dispatchTouchEvent方法直接傳遞給onTouchEvent方法進行處理
- 如果某個View接收了ACTION_DOWN之后歇僧,這個序列的后續(xù)事件中图张,在某一刻被父View攔截了,則這個字View會收到一個ACTION_CANCEL事件诈悍,并且也不會再收到這個事件序列中的后續(xù)事件
項目源碼
參考文章
有問題歡迎聯(lián)系我(1021423736@qq.com)