Android事件分發(fā)機(jī)制及滑動(dòng)沖突解決方案

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)心我被面試官虐啦!)

事件分發(fā)機(jī)制U形圖

注釋:

  • 1.整個(gè)流程圖,分為三層:Activity,ViewGroup,View,即最簡(jiǎn)單的情況。
  • 2.整個(gè)事件從Activity開始,由Activity的dispatchTouchEvent做分發(fā)。
  • 3.虛線上的字代表了這個(gè)方法的返回值,分為false,true,super。
  • 4.目前圖中所有事件是針對(duì)ACTION_DOWN的,對(duì)于ACTION_MOVEACTION_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)沖突了。圖示如下:

滑動(dòng)方向不一致的情況

再看看第二種,這種情況下,因?yàn)閮?nèi)部和外部滑動(dòng)方向一致,系統(tǒng)會(huì)分不清你要滑動(dòng)哪個(gè)部分,所以會(huì)要么只有一層能滑動(dòng),要么兩層一起滑動(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方法:

dispatchTouchEvent方法

然后修改父容器的onInterceptTouchEvent方法:

父容器的onInterceptTouchEvent方法

滑動(dòng)沖突解決實(shí)戰(zhàn)

    1. 滑動(dòng)方向不一致的情況:

看代碼看不出所以然,我們通過實(shí)例來看看滑動(dòng)沖突是怎么樣的。我們先模擬第一種場(chǎng)景,內(nèi)外滑動(dòng)方向不一致,我們先自定義一個(gè)父控件,讓其可以左右滑動(dòng),類似于ViewPaper:

內(nèi)外滑動(dòng)方向不一致

然后在布局中添加listview

添加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方法:

dispatchTouchEvent方法

再重寫外部父容器的oninterceptTouchEvent方法:

父容器的oninterceptTouchEvent方法
  • 2.滑動(dòng)方向一致的情況:

接下來看看同方向的滑動(dòng)沖突,這里我們用一個(gè)豎直的ScrollView嵌套一個(gè)ListView做例子。首先看看沒有解決滑動(dòng)沖突的時(shí)候是咋樣的:

滑動(dòng)方向一致的情況

我們看到只要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事件以外的其他方法:

父容器的onInterceptTouchEvent方法

在重寫ListView的dispatchTouchEvent方法,規(guī)則已經(jīng)說明過了:

listview的dispatchTouchEvent方法

效果圖:

最終效果圖

最終實(shí)現(xiàn)了完美解決滑動(dòng)沖突。解決問題的感覺是不是特別爽呢! <{=....(嘎嘎~)

好了,這篇文章到此結(jié)束,希望各位讀者看完之后能對(duì)事件分發(fā)機(jī)制有更深入的了解,在實(shí)際項(xiàng)目開發(fā)中,遇到滑動(dòng)沖突問題時(shí)能夠輕松解決問題,喜歡的話點(diǎn)個(gè)贊吧。(#.#)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,185評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,446評(píng)論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,951評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,189評(píng)論 0 287
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,718評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,602評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,800評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,045評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評(píng)論 1 281
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,420評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,755評(píng)論 2 371

推薦閱讀更多精彩內(nèi)容