Android事件分發機制及滑動沖突解決方案

Android開發中,事件分發機制是一塊Android比較重要的知識體系,了解并熟悉整套的分發機制有助于更好的分析各種點擊滑動失效以及滑動沖突問題,更好去擴展控件的事件功能和開發自定義控件,同時事件分發機制也是Android面試必問考點之一,總結一句:事件分發機制很重要

Android事件分發流程

網上關于事件分發機制的的博客很多很多,但是很多都是寫個Demo然后貼一下輸出的Log或者拿源碼分析,然后一堆的注釋和說明,讀者可能很難讀懂,或者是讀懂之后,過不了多久便又忘記了。那么,今天我用一張圖來總結一下Android整個事件分發機制的流程,如果你能在腦海里留下這張圖, 記住分發機制的整個流程,再去閱讀那些源碼博客會不會更加的印象深刻呢!反正我是印象挺深刻的!好了,請看圖!(自從記住了這張圖,媽媽再也不用擔心我被面試官虐啦!)

事件分發機制U形圖

注釋:

  • 1.整個流程圖,分為三層:Activity,ViewGroup,View,即最簡單的情況。
  • 2.整個事件從Activity開始,由Activity的dispatchTouchEvent做分發。
  • 3.虛線上的字代表了這個方法的返回值,分為false,true,super。
  • 4.目前圖中所有事件是針對ACTION_DOWN的,對于ACTION_MOVEACTION_UP我們另行分析。
  • 5.View是沒有onInterceptTouchEvent方法的,這個很容易理解,因為不會向下傳遞了,因此就沒有是否攔截事件之說了。

結合整個圖來看,我們得出事件流走向的幾個結論(希望讀者專心的對比U型圖來記這些結論,多看幾遍,腦子有比較清晰的概念。)

  • 結論1:返回值為super.xxx()的情況:事件的默認實現都是返回值為super.xxx(), 即我們沒有對控件里面的方法進行重寫或更改返回值,而是直接用super調用父類的默認實現,那么整個事件流向應該是從Activity---->ViewGroup--->View 從上往下調用dispatchTouchEvent方法,一直到葉子節點(View)的時候,再由View--->ViewGroup--->Activity從下往上調用onTouchEvent方法。若是ViewGroup則向下傳遞的時候會傳給onInterceptTouchEvent再傳給下層的dispatchTouchEvent,整個事件的流向是一個類U型圖。

  • 結論2:返回值為false的情況:對于dispatchTouchEvent和onTouchEvent,除了Activity返回值為false代表自己消費該事件,ViewGroup和View都會將該事件回傳給父控件的onTouchEvent來處理。而onInterceptTouchEvent返回值為false的時候代表不進行攔截,事件默認也是不攔截的,所以它和返回值為super.xxx()時是一樣的,繼續將事件傳遞給下層的dispatchTouchEvent來處理。

  • 結論3:返回值為true的情況:對于dispatchTouchEvent和onTouchEvent來說無論是Activity,還是ViewGroup和View返回值為true都代表自身來消費該事件,不再向下進行傳遞了。對于onInterceptTouchEvent來說,返回值為true代表攔截該事件的傳遞,既然攔截了,就代表不會往下傳遞了,這時候它會將事件傳遞給自身的onTouchEvent來處理。

以上這三個結論就代表了ACTION_DOWN事件的所有事件傳遞可能性,不知道讀者對著U型流程圖,有沒有在頭腦里有一個清晰的認識了呢。相信記住這三個結論之后,再去跟著源碼理解,能更加對事件分發有深入的了解呢!OK,我們繼續來看ACTION_MOVE和ACTION_UP是怎么傳遞的呢!

注意:上面講解的都是針對ACTION_DOWN的事件,ACTION_MOVE和ACTION_UP在傳遞的過程中并不是和ACTION_DOWN 一樣,你在執行ACTION_DOWN的時候返回了false,(case :ACTION_DOWN的返回值false,不是dispatchTouchEvent的返回值為false)后面一系列其它的action就不會再得到執行了。簡單的說,就是當dispatchTouchEvent在進行事件分發的時候,只有前一個事件(如ACTION_DOWN)返回true,才會收到ACTION_MOVE和ACTION_UP的事件。

上面提到過了,事件如果不被打斷的話是會不斷往下傳到葉子層(View),然后不斷回傳到Activity,dispatchTouchEvent 和 onTouchEvent 可以通過return true 消費事件,終結事件傳遞,而onInterceptTouchEvent 并不能消費事件,它相當于是一個分叉口起到分流導流的作用,ACTION_MOVE和ACTION_UP 會在哪些函數被調用,之前說了并不是哪個函數收到了ACTION_DOWN,就會收到 ACTION_MOVE 等后續的事件的。(因為要消費事件,才有ACTION_DOWN和ACTION_MOVE 發生,因此只考慮返回值為true的情況)

對于ACTION_MOVE和ACTION_UP在不同函數中的傳遞,有以下結論:

  • 結論1:對于dispatchTouchEvent :返回值為true時,自己消費事件。因為返回值為true代表消費,事件不會往下面傳,因此ACTION_DOWN事件傳遞到此處停止傳遞,ACTION_MOVE和ACTION_UP也傳遞到此處停止向下傳遞,這個時候傳遞方向是一致的。

  • 結論2:對于onTouchEvent :返回值為true時,自己消費事件。因為事件傳遞到onTouchEvent有可能是下層View或ViewGroup回傳過來的,這時候ACTION_DOWN是經過下層傳遞回來的,但是此時ACTION_MOVE和ACTION_UP并不會傳遞到下層;也有可能是自身的onInterceptTouchEvent 返回了true傳遞過來的,這個時候ACTION_MOVE和ACTION_UP和ACTION_DOWN事件的傳遞流程也是一樣的。

對于ACTION_MOVE、ACTION_UP終極總結:
ACTION_DOWN事件在哪個控件消費了(return true), 那么ACTION_MOVE和ACTION_UP就會從上往下(通過dispatchTouchEvent)做事件分發往下傳,就只會傳到這個控件,不會繼續往下傳,如果ACTION_DOWN事件是在dispatchTouchEvent消費,那么事件到此為止停止傳遞,如果ACTION_DOWN事件是在onTouchEvent消費的,那么會把ACTION_MOVE或ACTION_UP事件傳給該控件的onTouchEvent處理并結束傳遞。

滑動沖突解決方案

介紹完了事件分發機制的基本流程,我們來看看滑動沖突。滑動沖突的基本形式分為兩種,其他復雜的滑動沖突都可以拆成這兩種基本形式:

  • 1:外部滑動方向與內部方向不一致。
  • 2:外部方向與內部方向一致。

先來看第一種, 滑動方向不一致的情況。舉個例子, 比如你用ViewPaper和Fragment搭配,而Fragment里往往是一個豎直滑動的ListView這種情況是就會產生滑動沖突,但是由于ViewPaper本身已經處理好了滑動沖突,所以我們無需考慮,不過若是換成ScrollView,我們就得自己處理滑動沖突了。圖示如下:

滑動方向不一致的情況

再看看第二種,這種情況下,因為內部和外部滑動方向一致,系統會分不清你要滑動哪個部分,所以會要么只有一層能滑動,要么兩層一起滑動得很卡頓。圖示如下:

滑動方向一致的情況

對于這兩種情況,我們有不同的方法來處理它。

第一種:第一種的沖突主要是一個橫向的,一個豎向的,所以在開發中我們只要判斷滑動方向是豎向還是橫向的,再讓對應的View滑動即可。判斷的方法有很多,比如豎直距離與橫向距離的大小比較,哪個距離大就判定為向哪個方向滑動的;滑動路徑與水平形成的夾角等等。

第二種:對于這種情況,比較特殊,我們沒有通用的規則,得根據業務邏輯來得出相應的處理規則。舉個最常見的例子,ListView下拉刷新功能,需要ListView自身滑動實現滑動,但是當滑動到頭部時需要ListView和Header一起滑動,也就是整個父容器的滑動,這就涉及到滑動沖突問題了,如果不處理好滑動沖突,就會出現各種意想不到情況。對于這種情況的解決,我們可以采用攔截法:

  • 1.外部攔截法(由父容器決定事件的傳遞):讓事件都經過父容器的攔截處理(onInterceptTouchEvent ),如果父容器需要則攔截,如果不需要則不攔截,稱為外部攔截法,其偽代碼如下:
外部攔截法

代碼注釋:

a:首先down事件父容器必須返回false ,因為若是返回true,也就是攔截了down事件,那么后續的move和up事件就都會傳遞給父容器(onTouchEvent),子元素就沒有機會處理事件了。

b:其次是up事件也返回了false,一是因為up事件對父容器沒什么意義,其次是因為若事件是子元素處理的,卻沒有收到up事件會讓子元素的onClick事件無法觸發。

  • 2:內部攔截法(自己決定事件的傳遞):父容器不攔截任何事件,將所有事件傳遞給子元素,如果子元素需要則消耗掉,如果不需要則通過requestDisallowInterceptTouchEvent方法(請求父類不要攔截,返回值為true時不攔截,返回值為false時為攔截)交給父容器處理,稱為內部攔截法,使用起來稍顯麻煩,偽代碼如下:

首先我們需要重寫子元素的dispatchTouchEvent方法:

dispatchTouchEvent方法

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

父容器的onInterceptTouchEvent方法

滑動沖突解決實戰

    1. 滑動方向不一致的情況:

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

內外滑動方向不一致

然后在布局中添加listview

添加listview之后的界面

可以看到左右滑動確實失效了,說明確實產生了滑動沖突。那么我們就來解決一下吧!首先我們要明白滑動規則是什么,這個例子中如果我們豎直滑動就讓ListView消耗事件進行滑動,水平滑動就讓我們自定義的父容器滑動。

首先用外部攔截法,我們需要重寫onInterceptTouchEvent方法,代碼如下:

外部攔截法

這里我們判斷橫向滑動的距離與豎直滑動距離的長短。若是豎直滑動的長,則判斷為豎直滑動,那么就是ListView的滑動,就將intercepted置為false,讓父容器不攔截,交由子元素ListView處理。若是橫向,則intercepted置為true,交由父容器處理。OK,完美解決滑動沖突問題,效果圖:

最終效果圖

接下來看看內部攔截法:重寫其dispatchTouchEvent方法:

dispatchTouchEvent方法

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

父容器的oninterceptTouchEvent方法
  • 2.滑動方向一致的情況:

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

滑動方向一致的情況

我們看到只要ScrollView可以滑動,內部的ListView是不能滑動的。那我們現在來解決這個問題,同向滑動沖突和與不同向滑動沖突不一樣,得根據實際的需求來確定攔截的規則

這里我們的需求是當ListView滑到頂部了,并且繼續向下滑就讓ScrollView攔截掉;當ListView滑到底部了,并且繼續向下滑,就讓ScrollView攔截掉,其余時候都交給ListView自身處理事件。

首先用外部攔截法,我們需要重寫ScrollView的onInterceptTouchEvent方法,代碼如下:

外部攔截法

這里我們看到Down事件里我們并沒有返回false而是返回super.onInterceptTouchEvent(event),這是因為ScrollView在Down方法時需要初始化一些參數如果我們直接返回false,會導致滑動出現問題。并且前面說過ViewGroup的onInterceptTouchEvent方法是默認返回false的,所以我們這里要返回super方法才可。OK,完美解決,效果圖就不貼出來了,你懂的。

接下來看看內部攔截法:先重寫ScrollView的onInterceptTouchEvent方法,讓其攔截除了Down事件以外的其他方法:

父容器的onInterceptTouchEvent方法

在重寫ListView的dispatchTouchEvent方法,規則已經說明過了:

listview的dispatchTouchEvent方法

效果圖:

最終效果圖

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

好了,這篇文章到此結束,希望各位讀者看完之后能對事件分發機制有更深入的了解,在實際項目開發中,遇到滑動沖突問題時能夠輕松解決問題,喜歡的話點個贊吧。(#.#)

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

推薦閱讀更多精彩內容