在Android
開發(fā)中,事件分發(fā)機(jī)制是一塊Android比較重要的知識(shí)體系,了解并熟悉整套的分發(fā)機(jī)制有助于更好的分析各種點(diǎn)擊滑動(dòng)失效以及滑動(dòng)沖突問題,更好去擴(kuò)展控件的事件功能和開發(fā)自定義控件,同時(shí)事件分發(fā)機(jī)制也是Android面試必問考點(diǎn)之一,總結(jié)一句:事件分發(fā)機(jī)制很重要。
Android事件分發(fā)流程
網(wǎng)上關(guān)于事件分發(fā)機(jī)制的的博客很多很多,但是很多都是寫個(gè)Demo然后貼一下輸出的Log或者拿源碼分析,然后一堆的注釋和說明,讀者可能很難讀懂,或者是讀懂之后,過不了多久便又忘記了。那么,今天我用一張圖來總結(jié)一下Android整個(gè)事件分發(fā)機(jī)制的流程,如果你能在腦海里留下這張圖, 記住分發(fā)機(jī)制的整個(gè)流程,再去閱讀那些源碼博客會(huì)不會(huì)更加的印象深刻呢!反正我是印象挺深刻的!好了,請(qǐng)看圖!(自從記住了這張圖,媽媽再也不用擔(dān)心我被面試官虐啦!)
注釋:
- 1.整個(gè)流程圖,分為三層:Activity,ViewGroup,View,即最簡(jiǎn)單的情況。
- 2.整個(gè)事件從
Activity
開始,由Activity的dispatchTouchEvent
做分發(fā)。 - 3.虛線上的字代表了這個(gè)方法的返回值,分為false,true,super。
- 4.目前圖中所有事件是針對(duì)
ACTION_DOWN
的,對(duì)于ACTION_MOVE
和ACTION_UP
我們另行分析。 - 5.View是沒有
onInterceptTouchEvent
方法的,這個(gè)很容易理解,因?yàn)椴粫?huì)向下傳遞了,因此就沒有是否攔截事件之說了。
結(jié)合整個(gè)圖來看,我們得出事件流走向的幾個(gè)結(jié)論(希望讀者專心的對(duì)比U型圖來記這些結(jié)論,多看幾遍,腦子有比較清晰的概念。)
結(jié)論1:返回值為super.xxx()的情況:事件的默認(rèn)實(shí)現(xiàn)都是返回值為super.xxx(), 即我們沒有對(duì)控件里面的方法進(jìn)行重寫或更改返回值,而是直接用super調(diào)用父類的默認(rèn)實(shí)現(xiàn),那么整個(gè)事件流向應(yīng)該是從Activity---->ViewGroup--->View 從上往下調(diào)用dispatchTouchEvent方法,一直到葉子節(jié)點(diǎn)(View)的時(shí)候,再由View--->ViewGroup--->Activity從下往上調(diào)用
onTouchEvent
方法。若是ViewGroup則向下傳遞的時(shí)候會(huì)傳給onInterceptTouchEvent再傳給下層的dispatchTouchEvent,整個(gè)事件的流向是一個(gè)類U型圖。結(jié)論2:返回值為false的情況:對(duì)于dispatchTouchEvent和onTouchEvent,除了Activity返回值為false代表自己消費(fèi)該事件,ViewGroup和View都會(huì)將該事件回傳給父控件的onTouchEvent來處理。而onInterceptTouchEvent返回值為false的時(shí)候代表不進(jìn)行攔截,事件默認(rèn)也是不攔截的,所以它和返回值為super.xxx()時(shí)是一樣的,繼續(xù)將事件傳遞給下層的dispatchTouchEvent來處理。
結(jié)論3:返回值為true的情況:對(duì)于dispatchTouchEvent和onTouchEvent來說無論是Activity,還是ViewGroup和View返回值為true都代表自身來消費(fèi)該事件,不再向下進(jìn)行傳遞了。對(duì)于onInterceptTouchEvent來說,返回值為true代表攔截該事件的傳遞,既然攔截了,就代表不會(huì)往下傳遞了,這時(shí)候它會(huì)將事件傳遞給自身的onTouchEvent來處理。
以上這三個(gè)結(jié)論就代表了ACTION_DOWN事件的所有事件傳遞可能性,不知道讀者對(duì)著U型流程圖,有沒有在頭腦里有一個(gè)清晰的認(rèn)識(shí)了呢。相信記住這三個(gè)結(jié)論之后,再去跟著源碼理解,能更加對(duì)事件分發(fā)有深入的了解呢!OK,我們繼續(xù)來看ACTION_MOVE和ACTION_UP是怎么傳遞的呢!
注意:上面講解的都是針對(duì)ACTION_DOWN的事件,ACTION_MOVE和ACTION_UP在傳遞的過程中并不是和ACTION_DOWN 一樣,你在執(zhí)行ACTION_DOWN的時(shí)候返回了false,(case :ACTION_DOWN的返回值false,不是dispatchTouchEvent的返回值為false)后面一系列其它的action就不會(huì)再得到執(zhí)行了。簡(jiǎn)單的說,就是當(dāng)dispatchTouchEvent在進(jìn)行事件分發(fā)的時(shí)候,只有前一個(gè)事件(如ACTION_DOWN)返回true,才會(huì)收到ACTION_MOVE和ACTION_UP的事件。
上面提到過了,事件如果不被打斷的話是會(huì)不斷往下傳到葉子層(View),然后不斷回傳到Activity,dispatchTouchEvent 和 onTouchEvent 可以通過return true 消費(fèi)事件,終結(jié)事件傳遞,而onInterceptTouchEvent 并不能消費(fèi)事件,它相當(dāng)于是一個(gè)分叉口起到分流導(dǎo)流的作用,ACTION_MOVE和ACTION_UP 會(huì)在哪些函數(shù)被調(diào)用,之前說了并不是哪個(gè)函數(shù)收到了ACTION_DOWN,就會(huì)收到 ACTION_MOVE 等后續(xù)的事件的。(因?yàn)橐M(fèi)事件,才有ACTION_DOWN和ACTION_MOVE 發(fā)生,因此只考慮返回值為true的情況)
對(duì)于ACTION_MOVE和ACTION_UP在不同函數(shù)中的傳遞,有以下結(jié)論:
結(jié)論1:對(duì)于dispatchTouchEvent :返回值為true時(shí),自己消費(fèi)事件。因?yàn)榉祷刂禐閠rue代表消費(fèi),事件不會(huì)往下面?zhèn)?因此ACTION_DOWN事件傳遞到此處停止傳遞,ACTION_MOVE和ACTION_UP也傳遞到此處停止向下傳遞,這個(gè)時(shí)候傳遞方向是一致的。
結(jié)論2:對(duì)于onTouchEvent :返回值為true時(shí),自己消費(fèi)事件。因?yàn)槭录鬟f到onTouchEvent有可能是下層View或ViewGroup回傳過來的,這時(shí)候ACTION_DOWN是經(jīng)過下層傳遞回來的,但是此時(shí)ACTION_MOVE和ACTION_UP并不會(huì)傳遞到下層;也有可能是自身的onInterceptTouchEvent 返回了true傳遞過來的,這個(gè)時(shí)候ACTION_MOVE和ACTION_UP和ACTION_DOWN事件的傳遞流程也是一樣的。
對(duì)于ACTION_MOVE、ACTION_UP終極總結(jié):
ACTION_DOWN事件在哪個(gè)控件消費(fèi)了(return true), 那么ACTION_MOVE和ACTION_UP就會(huì)從上往下(通過dispatchTouchEvent)做事件分發(fā)往下傳,就只會(huì)傳到這個(gè)控件,不會(huì)繼續(xù)往下傳,如果ACTION_DOWN事件是在dispatchTouchEvent消費(fèi),那么事件到此為止停止傳遞,如果ACTION_DOWN事件是在onTouchEvent消費(fèi)的,那么會(huì)把ACTION_MOVE或ACTION_UP事件傳給該控件的onTouchEvent處理并結(jié)束傳遞。
滑動(dòng)沖突解決方案
介紹完了事件分發(fā)機(jī)制的基本流程,我們來看看滑動(dòng)沖突。滑動(dòng)沖突的基本形式分為兩種,其他復(fù)雜的滑動(dòng)沖突都可以拆成這兩種基本形式:
- 1:外部滑動(dòng)方向與內(nèi)部方向不一致。
- 2:外部方向與內(nèi)部方向一致。
先來看第一種, 滑動(dòng)方向不一致的情況。舉個(gè)例子, 比如你用ViewPaper和Fragment搭配,而Fragment里往往是一個(gè)豎直滑動(dòng)的ListView這種情況是就會(huì)產(chǎn)生滑動(dòng)沖突,但是由于ViewPaper本身已經(jīng)處理好了滑動(dòng)沖突,所以我們無需考慮,不過若是換成ScrollView,我們就得自己處理滑動(dòng)沖突了。圖示如下:
再看看第二種,這種情況下,因?yàn)閮?nèi)部和外部滑動(dòng)方向一致,系統(tǒng)會(huì)分不清你要滑動(dòng)哪個(gè)部分,所以會(huì)要么只有一層能滑動(dòng),要么兩層一起滑動(dòng)得很卡頓。圖示如下:
對(duì)于這兩種情況,我們有不同的方法來處理它。
第一種:第一種的沖突主要是一個(gè)橫向的,一個(gè)豎向的,所以在開發(fā)中我們只要判斷滑動(dòng)方向是豎向還是橫向的,再讓對(duì)應(yīng)的View滑動(dòng)即可。判斷的方法有很多,比如豎直距離與橫向距離的大小比較,哪個(gè)距離大就判定為向哪個(gè)方向滑動(dòng)的;滑動(dòng)路徑與水平形成的夾角等等。
第二種:對(duì)于這種情況,比較特殊,我們沒有通用的規(guī)則,得根據(jù)業(yè)務(wù)邏輯來得出相應(yīng)的處理規(guī)則。舉個(gè)最常見的例子,ListView下拉刷新功能,需要ListView自身滑動(dòng)實(shí)現(xiàn)滑動(dòng),但是當(dāng)滑動(dòng)到頭部時(shí)需要ListView和Header一起滑動(dòng),也就是整個(gè)父容器的滑動(dòng),這就涉及到滑動(dòng)沖突問題了,如果不處理好滑動(dòng)沖突,就會(huì)出現(xiàn)各種意想不到情況。對(duì)于這種情況的解決,我們可以采用攔截法:
- 1.外部攔截法(由父容器決定事件的傳遞):讓事件都經(jīng)過父容器的攔截處理(onInterceptTouchEvent ),如果父容器需要?jiǎng)t攔截,如果不需要?jiǎng)t不攔截,稱為外部攔截法,其偽代碼如下:
代碼注釋:
a:首先down事件父容器必須返回false ,因?yàn)槿羰欠祷豻rue,也就是攔截了down事件,那么后續(xù)的move和up事件就都會(huì)傳遞給父容器(onTouchEvent),子元素就沒有機(jī)會(huì)處理事件了。
b:其次是up事件也返回了false,一是因?yàn)閡p事件對(duì)父容器沒什么意義,其次是因?yàn)槿羰录亲釉靥幚淼模瑓s沒有收到up事件會(huì)讓子元素的onClick事件無法觸發(fā)。
- 2:內(nèi)部攔截法(自己決定事件的傳遞):父容器不攔截任何事件,將所有事件傳遞給子元素,如果子元素需要?jiǎng)t消耗掉,如果不需要?jiǎng)t通過requestDisallowInterceptTouchEvent方法(請(qǐng)求父類不要攔截,返回值為true時(shí)不攔截,返回值為false時(shí)為攔截)交給父容器處理,稱為內(nèi)部攔截法,使用起來稍顯麻煩,偽代碼如下:
首先我們需要重寫子元素的dispatchTouchEvent方法:
然后修改父容器的onInterceptTouchEvent方法:
滑動(dòng)沖突解決實(shí)戰(zhàn)
- 滑動(dòng)方向不一致的情況:
看代碼看不出所以然,我們通過實(shí)例來看看滑動(dòng)沖突是怎么樣的。我們先模擬第一種場(chǎng)景,內(nèi)外滑動(dòng)方向不一致,我們先自定義一個(gè)父控件,讓其可以左右滑動(dòng),類似于ViewPaper:
然后在布局中添加listview
可以看到左右滑動(dòng)確實(shí)失效了,說明確實(shí)產(chǎn)生了滑動(dòng)沖突。那么我們就來解決一下吧!首先我們要明白滑動(dòng)規(guī)則是什么,這個(gè)例子中如果我們豎直滑動(dòng)就讓ListView消耗事件進(jìn)行滑動(dòng),水平滑動(dòng)就讓我們自定義的父容器滑動(dòng)。
首先用外部攔截法,我們需要重寫onInterceptTouchEvent方法,代碼如下:
這里我們判斷橫向滑動(dòng)的距離與豎直滑動(dòng)距離的長(zhǎng)短。若是豎直滑動(dòng)的長(zhǎng),則判斷為豎直滑動(dòng),那么就是ListView的滑動(dòng),就將intercepted置為false,讓父容器不攔截,交由子元素ListView處理。若是橫向,則intercepted置為true,交由父容器處理。OK,完美解決滑動(dòng)沖突問題,效果圖:
接下來看看內(nèi)部攔截法:重寫其dispatchTouchEvent方法:
再重寫外部父容器的oninterceptTouchEvent方法:
- 2.滑動(dòng)方向一致的情況:
接下來看看同方向的滑動(dòng)沖突,這里我們用一個(gè)豎直的ScrollView嵌套一個(gè)ListView做例子。首先看看沒有解決滑動(dòng)沖突的時(shí)候是咋樣的:
我們看到只要ScrollView可以滑動(dòng),內(nèi)部的ListView是不能滑動(dòng)的。那我們現(xiàn)在來解決這個(gè)問題,同向滑動(dòng)沖突和與不同向滑動(dòng)沖突不一樣,得根據(jù)實(shí)際的需求來確定攔截的規(guī)則。
這里我們的需求是當(dāng)ListView滑到頂部了,并且繼續(xù)向下滑就讓ScrollView攔截掉;當(dāng)ListView滑到底部了,并且繼續(xù)向下滑,就讓ScrollView攔截掉,其余時(shí)候都交給ListView自身處理事件。
首先用外部攔截法,我們需要重寫ScrollView的onInterceptTouchEvent方法,代碼如下:
這里我們看到Down事件里我們并沒有返回false而是返回super.onInterceptTouchEvent(event),這是因?yàn)镾crollView在Down方法時(shí)需要初始化一些參數(shù)如果我們直接返回false,會(huì)導(dǎo)致滑動(dòng)出現(xiàn)問題。并且前面說過ViewGroup的onInterceptTouchEvent方法是默認(rèn)返回false的,所以我們這里要返回super方法才可。OK,完美解決,效果圖就不貼出來了,你懂的。
接下來看看內(nèi)部攔截法:先重寫ScrollView的onInterceptTouchEvent方法,讓其攔截除了Down事件以外的其他方法:
在重寫ListView的dispatchTouchEvent方法,規(guī)則已經(jīng)說明過了:
效果圖:
最終實(shí)現(xiàn)了完美解決滑動(dòng)沖突。解決問題的感覺是不是特別爽呢! <{=....(嘎嘎嘎~)
好了,這篇文章到此結(jié)束,希望各位讀者看完之后能對(duì)事件分發(fā)機(jī)制有更深入的了解,在實(shí)際項(xiàng)目開發(fā)中,遇到滑動(dòng)沖突問題時(shí)能夠輕松解決問題,喜歡的話點(diǎn)個(gè)贊吧。(#.#)