Android 圖解 View 事件分發(fā)機(jī)制,看這一篇就夠了


本文首發(fā)我的微信公眾號(hào):徐公,想成為一名優(yōu)秀的 Android 開(kāi)發(fā)者,需要一份完備的?知識(shí)體系,在這里,讓我們一起成長(zhǎng),變得更好~。
在 Android 開(kāi)發(fā)當(dāng)中,View 的事件分發(fā)機(jī)制是一塊很重要的知識(shí)。不僅在開(kāi)發(fā)當(dāng)中經(jīng)常需要用到,面試的時(shí)候也經(jīng)常被問(wèn)到。
如果你在面試的時(shí)候,能把這塊講清楚,對(duì)于校招生或者實(shí)習(xí)生來(lái)說(shuō),算是一塊不錯(cuò)的加分項(xiàng)。對(duì)于工作幾年的我們來(lái)說(shuō),這是必須掌握的,講不明白,那你回去等通知吧,哈哈。

目錄大概如下:
View 事件分發(fā)機(jī)制簡(jiǎn)介
View 常見(jiàn)滑動(dòng)沖突解決
View 雙擊,多擊事件是怎么實(shí)現(xiàn)的
手勢(shì)識(shí)別
小結(jié)
View 事件分發(fā)機(jī)制簡(jiǎn)介
View 觸摸事件
對(duì)于屏幕的點(diǎn)擊,滑動(dòng),抬起等一系的動(dòng)作,其實(shí)都是由一個(gè)一個(gè)MotionEvent對(duì)象組成的。根據(jù)不同動(dòng)作,主要有以下三種事件類型:
1.ACTION_DOWN:手指剛接觸屏幕,按下去的那一瞬間產(chǎn)生該事件 2.ACTION_MOVE:手指在屏幕上移動(dòng)時(shí)候產(chǎn)生該事件 3.ACTION_UP:手指從屏幕上松開(kāi)的瞬間產(chǎn)生該事件 4.ACTION_CANCEL 當(dāng)前 View 的手勢(shì)被打斷,后續(xù)不會(huì)再收到任何事件
從 ACTION_DOWN 開(kāi)始到 ACTION_UP/ACTION_CANCEL 結(jié)束我們稱為一個(gè)事件序列
正常情況下,無(wú)論你手指在屏幕上有多么騷的操作,最終呈現(xiàn)在 MotionEvent 上來(lái)講無(wú)外乎下面 3 種 case。
點(diǎn)擊后抬起,也就是單擊操作:ACTION_DOWN -> ACTION_UP
點(diǎn)擊后再風(fēng)騷的滑動(dòng)一段距離,再抬起:ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_MOVE -> ACTION_UP
某些情況下,我們可能會(huì)沒(méi)有收到 ACTION_UP 事件,是收到 ACTION_CANCEL 事件。
ACTION_CANCEL 一般是指 ChildView 原先擁有事件處理權(quán),后面由于某些原因,該處理權(quán)需要交回給上層去處理,ChildView便會(huì)收到 ACTION_CANCEL 事件。對(duì)于一些復(fù)位或者重置操作,我們應(yīng)該在 ACTION_UP 和 ACTION_CANCEL 里面同時(shí)進(jìn)行處理。
代碼邏輯上是:上層判斷之前交給ChildView的事件處理權(quán)需要收回來(lái)了,便會(huì)做事件的攔截處理,攔截時(shí)給ChildView發(fā)一個(gè)ACTION_CANCEL事件
幾個(gè)主要方法
我們知道,View 的事件分發(fā)機(jī)制主要涉及到以下幾個(gè)方法
dispatchTouchEvent ,這個(gè)方法主要是用來(lái)分發(fā)事件的
onInterceptTouchEvent,這個(gè)方法主要是用來(lái)攔截事件的(需要注意的是 ViewGroup 才有這個(gè)方法,View 沒(méi)有 onInterceptTouchEvent 這個(gè)方法)
onTouchEvent 這個(gè)方法主要是用來(lái)處理事件的
requestDisallowInterceptTouchEvent(true),這個(gè)方法能夠影響父View是否攔截事件,true 表示父 View 不攔截事件,false 表示父 View 攔截事件
我們先來(lái)看一張圖。
以下內(nèi)容參考圖解 Android 事件分發(fā)機(jī)制這一篇博客

仔細(xì)看的話,圖分為3層,從上往下依次是Activity、ViewGroup、View
事件從左上角那個(gè)白色箭頭開(kāi)始,由 Activity 的 dispatchTouchEvent 進(jìn)行分發(fā)
箭頭的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super 的意思是調(diào)用父類實(shí)現(xiàn)。)
dispatchTouchEvent和 onTouchEvent的框里有個(gè)【true---->消費(fèi)】的字,表示的意思是如果方法返回true,那么代表事件就此消費(fèi),不會(huì)繼續(xù)往別的地方傳了,事件終止。
目前所有的圖的事件是針對(duì)ACTION_DOWN的,對(duì)于ACTION_MOVE和ACTION_UP我們最后做分析。
當(dāng)觸摸事件發(fā)生時(shí),首先 Activity 將 TouchEvent 傳遞給最頂層的 View,TouchEvent最先到達(dá)最頂層 view 的 dispatchTouchEvent ,然后由 dispatchTouchEvent 方法進(jìn)行分發(fā),
如果dispatchTouchEvent返回true 消費(fèi)事件,事件終結(jié)。
如果dispatchTouchEvent返回 false ,則回傳給父View的onTouchEvent事件處理;
如果dispatchTouchEvent返回super的話,默認(rèn)會(huì)調(diào)用自己的onInterceptTouchEvent方法。
默認(rèn)的情況下onInterceptTouchEvent回調(diào)用super方法,super方法默認(rèn)返回false,所以會(huì)交給子View的onDispatchTouchEvent方法處理
如果 interceptTouchEvent 返回 true ,也就是攔截掉了,則交給它的 onTouchEvent 來(lái)處理,
如果 interceptTouchEvent 返回 false ,那么就傳遞給子 view ,由子 view 的 dispatchTouchEvent 再來(lái)開(kāi)始這個(gè)事件的分發(fā)。
關(guān)于更多詳細(xì)分析,請(qǐng)查看原博客圖解 Android 事件分發(fā)機(jī)制,真心推薦,寫得很好。
View 滑動(dòng)事件沖突
在開(kāi)發(fā)當(dāng)中,View 的滑動(dòng)沖突時(shí)經(jīng)常遇到的,比如 ViewPager 嵌套 ViewPager,ScrollView 嵌套 ViewPager。下面讓我們一起來(lái)看看怎么解決。
常見(jiàn)的三種情況
第一種情況,滑動(dòng)方向不同

第二種情況,滑動(dòng)方向相同

第三種情況,上述兩種情況的嵌套

解決思路
看了上面三種情況,我們知道他們的共同特點(diǎn)是父View 和子View都想爭(zhēng)著響應(yīng)我們的觸摸事件,但遺憾的是我們的觸摸事件 同一時(shí)刻只能被某一個(gè)View或者ViewGroup攔截消費(fèi),所以就產(chǎn)生了滑動(dòng)沖突。
那既然同一時(shí)刻只能由某一個(gè) View 或者 ViewGroup 消費(fèi)攔截,那我們就只需要 決定在某個(gè)時(shí)刻由這個(gè) View 或者 ViewGroup 攔截事件,另外的 某個(gè)時(shí)刻由 另外一個(gè) View 或者 ViewGroup 攔截事件,不就 OK了嗎?
綜上,正如 在?《Android開(kāi)發(fā)藝術(shù)》?一書提出的,總共 有兩種解決方案
以下解決思路來(lái)自于?《Android開(kāi)發(fā)藝術(shù)》?書籍
下面的兩種方法針對(duì)第一種情況(滑動(dòng)方向不同),父View是上下滑動(dòng),子View是左右滑動(dòng)的情況。
外部解決法
從父View著手,重寫onInterceptTouchEvent方法,在父View需要攔截的時(shí)候攔截,不要的時(shí)候返回false,為代碼大概 如下
public?boolean?onInterceptTouchEvent(MotionEvent?ev)?{
????final?float?x?=?ev.getX();
????final?float?y?=?ev.getY();
????final?int?action?=?ev.getAction();
????switch?(action)?{
????????case?MotionEvent.ACTION_DOWN:
????????????mDownPosX?=?x;
????????????mDownPosY?=?y;
????????????break;
????????case?MotionEvent.ACTION_MOVE:
????????????final?float?deltaX?=?Math.abs(x?-?mDownPosX);
????????????final?float?deltaY?=?Math.abs(y?-?mDownPosY);
????????????//?這里是夠攔截的判斷依據(jù)是左右滑動(dòng),讀者可根據(jù)自己的邏輯進(jìn)行是否攔截
????????????if?(deltaX?>?deltaY)?{
????????????????return?false;
????????????}
????}
????return?super.onInterceptTouchEvent(ev);
}
內(nèi)部解決法
從子View著手,父View先不要攔截任何事件,所有的事件傳遞給 子View,如果子View需要此事件就消費(fèi)掉,不需要此事件的話就交給 父View處理。
實(shí)現(xiàn)思路 如下,重寫子 View的dispatchTouchEvent方法,在Action_down 動(dòng)作中通過(guò)方法 requestDisallowInterceptTouchEvent(true) 先請(qǐng)求 父 View不要攔截事件,這樣保證子 View 能夠接受到 Action_move 事件,再在 Action_move 動(dòng)作中根據(jù)自己的邏輯是否要攔截事件,不需要攔截事件的話再交給 父 View 處理。
public?boolean?dispatchTouchEvent(MotionEvent?ev)?{
????int?x?=?(int)?ev.getRawX();
????int?y?=?(int)?ev.getRawY();
????int?dealtX?=?0;
????int?dealtY?=?0;
????switch?(ev.getAction())?{
????????case?MotionEvent.ACTION_DOWN:
????????????dealtX?=?0;
????????????dealtY?=?0;
????????????//?保證子View能夠接收到Action_move事件
????????????getParent().requestDisallowInterceptTouchEvent(true);
????????????break;
????????case?MotionEvent.ACTION_MOVE:
????????????dealtX?+=?Math.abs(x?-?lastX);
????????????dealtY?+=?Math.abs(y?-?lastY);
????????????Log.i(TAG,?"dealtX:="?+?dealtX);
????????????Log.i(TAG,?"dealtY:="?+?dealtY);
????????????//?這里是夠攔截的判斷依據(jù)是左右滑動(dòng),讀者可根據(jù)自己的邏輯進(jìn)行是否攔截
????????????if?(dealtX?>=?dealtY)?{
????????????????getParent().requestDisallowInterceptTouchEvent(true);
????????????}?else?{
????????????????getParent().requestDisallowInterceptTouchEvent(false);
????????????}
????????????lastX?=?x;
????????????lastY?=?y;
????????????break;
????????case?MotionEvent.ACTION_CANCEL:
????????????break;
????????case?MotionEvent.ACTION_UP:
????????????break;
????}
????return?super.dispatchTouchEvent(ev);
}
更多細(xì)節(jié),可以查看我的這篇文章,里面有詳細(xì)介紹哦?ViewPager,ScrollView 嵌套ViewPager滑動(dòng)沖突解決
View 雙擊,多擊事件是怎么實(shí)現(xiàn)的
實(shí)現(xiàn)之前,我們首先來(lái)闡述一下思路,怎樣實(shí)現(xiàn)雙擊事件,正所謂,授人以魚不如授人以漁。
單擊:用戶點(diǎn)擊一次之后,一段時(shí)間之內(nèi)不再點(diǎn)擊
雙擊;用戶點(diǎn)擊一次之后,一段時(shí)間之內(nèi)再次點(diǎn)擊
實(shí)現(xiàn)思路
我們監(jiān)聽(tīng) onTouch 事件,在 ACTION_DOWN 的時(shí)候,點(diǎn)擊次數(shù) clickCount +1;
同時(shí),在 ACTION_DOWN 的時(shí)候,延時(shí)一段時(shí)間,執(zhí)行相應(yīng)的 Runnable 任務(wù),這里我們用 handler 的 postDelayed 實(shí)現(xiàn)
在延時(shí)任務(wù)執(zhí)行的時(shí)候,我們根據(jù)點(diǎn)擊的次數(shù),進(jìn)行單擊或者多級(jí)的回調(diào),最后,記得重置點(diǎn)擊次數(shù),以及移除延時(shí)任務(wù)
open?class?MyDoubleTouchListener(private?val?myClickCallBack:?MyClickCallBack)?:?OnTouchListener?{
????private?var?clickCount?=?0?//記錄連續(xù)點(diǎn)擊次數(shù)
????private?val?handler:?Handler?=?Handler()
????interface?MyClickCallBack?{
????????fun?oneClick()?//點(diǎn)擊一次的回調(diào)
????????fun?doubleClick()?//連續(xù)點(diǎn)擊兩次的回調(diào)
????}
????override?fun?onTouch(v:?View,?event:?MotionEvent):?Boolean?{
????????if?(event.action?==?MotionEvent.ACTION_DOWN)?{
????????????clickCount++
????????????handler.postDelayed({
????????????????if?(clickCount?==?1)?{
????????????????????myClickCallBack.oneClick()
????????????????}?else?if?(clickCount?==?2)?{
????????????????????myClickCallBack.doubleClick()
????????????????}
????????????????handler.removeCallbacksAndMessages(null)
????????????????//清空handler延時(shí),并防內(nèi)存泄漏
????????????????clickCount?=?0?//計(jì)數(shù)清零
????????????},?timeout.toLong())?//延時(shí)timeout后執(zhí)行run方法中的代碼
????????}
????????return?false?//讓點(diǎn)擊事件繼續(xù)傳播,方便再給View添加其他事件監(jiān)聽(tīng)
????}
????companion?object?{
????????private?const?val?TAG?=?"MyClickListener"
????????private?val?timeout?=?ViewConfiguration.getDoubleTapTimeout()?//雙擊間四百毫秒延時(shí)
????????init?{
????????????Log.i(TAG,?"timeout?is?$timeout?")
????????}
????}
}
三擊事件
三級(jí)事件呢,其實(shí)也很簡(jiǎn)單,我們直接判斷在指定時(shí)間間隔內(nèi)點(diǎn)擊的次數(shù)即可
open?class?MyMultiTouchListener(private?val?myClickCallBack:?MyClickCallBack)?:?OnTouchListener?{
????private?var?clickCount?=?0?//記錄連續(xù)點(diǎn)擊次數(shù)
????private?val?handler:?Handler?=?Handler()
????interface?MyClickCallBack?{
????????fun?oneClick()?//點(diǎn)擊一次的回調(diào)
????????fun?doubleClick()?//連續(xù)點(diǎn)擊兩次的回調(diào)
????????fun?threeClick()?//?連續(xù)點(diǎn)擊三次的回調(diào)
????}
????override?fun?onTouch(v:?View,?event:?MotionEvent):?Boolean?{
????????if?(event.action?==?MotionEvent.ACTION_DOWN)?{
????????????clickCount++
????????????handler.postDelayed({
????????????????if?(clickCount?==?1)?{
????????????????????myClickCallBack.oneClick()
????????????????}?else?if?(clickCount?==?2)?{
????????????????????myClickCallBack.doubleClick()
????????????????}?else?if?(clickCount?==?3)?{
????????????????????myClickCallBack.threeClick()
????????????????}
????????????????handler.removeCallbacksAndMessages(null)
????????????????//清空handler延時(shí),并防內(nèi)存泄漏
????????????????clickCount?=?0?//計(jì)數(shù)清零
????????????},?timeout.toLong())?//延時(shí)timeout后執(zhí)行run方法中的代碼
????????}
????????return?false?//讓點(diǎn)擊事件繼續(xù)傳播,方便再給View添加其他事件監(jiān)聽(tīng)
????}
????companion?object?{
????????private?const?val?TAG?=?"MyClickListener"
????????private?val?timeout?=?600?//雙擊間四百毫秒延時(shí)
????????init?{
????????????Log.i(TAG,?"timeout?is?$timeout?")
????????}
????}
}
手勢(shì)識(shí)別
在 Android 開(kāi)發(fā)當(dāng)中,幾乎所有的事件都會(huì)與用戶進(jìn)行交互,而我們用得的最多的就是手勢(shì)了。
我們知道當(dāng)我們觸摸屏幕的時(shí)候,會(huì)產(chǎn)生很多事件,比如 down,move,up, fling 事件等等。一些簡(jiǎn)單的處理,我們可以直接重寫 View 的 onTouchEvent 方法,根據(jù) View 的 MotionEvent 事件進(jìn)行處理。
而 Google 為了方便開(kāi)發(fā)者方便接入,提供了幾個(gè)默認(rèn)處理類,那就是 GestureDetector 和 ScaleGestureDetector。
GestureDetector這個(gè)類對(duì)外提供了兩個(gè)接口和一個(gè)外部類 。 接口:OnGestureListener,OnDoubleTapListener
內(nèi)部類:SimpleOnGestureListener,同時(shí)實(shí)現(xiàn)了 OnGestureListener,OnDoubleTapListener 接口,如果只想使用接口里面的某個(gè)方法,可以直接使用它,方便快捷。
講解之前,我們向來(lái)看一下怎么使用
GestureDetector(Context context, GestureDetector.OnGestureListener listener)
GestureDetector 基本使用
第一步,初始化?GestureDetector
?對(duì)象
?mDetector?=?GestureDetectorCompat(this,?MyGestureListener())
可以看到有兩個(gè)參數(shù),第一個(gè)參數(shù) context,第二個(gè)參數(shù) OnGestureListener,我們可以直接實(shí)現(xiàn) OnGestureListener 接口,也可以直接使用?GestureDetector.SimpleOnGestureListener
????private?class?MyGestureListener?:?GestureDetector.OnGestureListener?{
????????private?val?TAG?=?"GestureDemoActivity"
????????override?fun?onShowPress(e:?MotionEvent?)?{
????????????Log.d(TAG,?"onShowPress:?e?is?$e")
????????}
????????override?fun?onSingleTapUp(e:?MotionEvent?):?Boolean?{
????????????Log.d(TAG,?"onSingleTapUp:?e?is?$e")
????????????return?false
????????}
????????override?fun?onDown(event:?MotionEvent):?Boolean?{
????????????Log.d(TAG,?"onDown:?$event")
????????????return?true
????????}
????????override?fun?onFling(
????????????????event1:?MotionEvent,
????????????????event2:?MotionEvent,
????????????????velocityX:?Float,
????????????????velocityY:?Float
????????):?Boolean?{
????????????Log.d(TAG,?"onFling:?$event1?$event2")
????????????return?false
????????}
????????override?fun?onScroll(e1:?MotionEvent?,?e2:?MotionEvent?,?distanceX:?Float,?distanceY:?Float):?Boolean?{
????????????Log.d(TAG,?"onScroll:?distanceX?is?$distanceX,distanceY?is?$distanceY?")
????????????return?false
????????}
????????override?fun?onLongPress(e:?MotionEvent?)?{
????????????Log.d(TAG,?"onLongPress:?e?is?$e")
????????}
????}
第二步:設(shè)置雙擊監(jiān)聽(tīng)
//?設(shè)置雙擊監(jiān)聽(tīng)
mDetector.setOnDoubleTapListener(object?:?GestureDetector.OnDoubleTapListener?{
????????????override?fun?onDoubleTap(e:?MotionEvent?):?Boolean?{
????????????????Log.d(TAG,?"onDoubleTap:?e?is?e")
????????????????return?false
????????????}
????????????override?fun?onDoubleTapEvent(e:?MotionEvent?):?Boolean?{
????????????????Log.d(TAG,?"onDoubleTapEvent:?e?is?e")
????????????????return?false
????????????}
????????????override?fun?onSingleTapConfirmed(e:?MotionEvent?):?Boolean?{
????????????????Log.d(TAG,?"onSingleTapConfirmed:?e?is?e")
????????????????return?false
????????????}
????????})
最后,重寫 Activity 或者 View 的 onTouchEvent ,將事件交給 mDetector 處理。
通常會(huì)有兩種寫法,第一種是如果手勢(shì)處理器處理了,直接返回 true,進(jìn)行消費(fèi)。否則,進(jìn)行默認(rèn)處理
override?fun?onTouchEvent(event:?MotionEvent):?Boolean?{
????????return?if?(mDetector.onTouchEvent(event))?{
????????????true
????????}?else?{
????????????super.onTouchEvent(event)
????????}
????}
第二種寫法是直接在 onTouchEvent 方法中,直接調(diào)用?mDetector.onTouchEvent(event)
?方法
override?fun?onTouchEvent(event:?MotionEvent):?Boolean?{
????????mDetector.onTouchEvent(event)
????????return?super.onTouchEvent(event)
????}
第二種寫法,一般不會(huì)影響當(dāng)前 View 或者 Activity 事件的傳遞,在開(kāi)發(fā)當(dāng)中,有時(shí)候?yàn)榱藴p少一些觸摸事件的沖突,經(jīng)常這樣寫。
ScaleGestureDetector 這里暫時(shí)不展開(kāi)描述了了,寫著寫著,發(fā)現(xiàn)好多呀,一個(gè)周末就這樣過(guò)去,賊快,覺(jué)得對(duì)你有幫助的,請(qǐng)來(lái)個(gè)三連,點(diǎn)贊,收藏,轉(zhuǎn)發(fā)??。
小結(jié)
這篇文章,其實(shí)不難。主要是將 View 的事件分發(fā)機(jī)制,滑動(dòng)沖突,以及開(kāi)發(fā)當(dāng)中經(jīng)常用到的一些知識(shí)點(diǎn),總結(jié)一下。