Android View的事件分發機制和滑動沖突解決方案

passiontimIP屬地: 臺灣
0.108字數 3,245



本文作者


本文由陳添投稿。

陳添的博客地址:

http://www.lxweimin.com/u/f7fa41541bf9


這篇文章會先講Android中View的事件分發機制,然后再介紹Android滑動沖突的形成原因并給出解決方案。因水平有限,講的不會太過深入,只希望各位看了之后對事件分發機制的流程有個大概的概念,并且以后能自己解決有關滑動沖突的問題,用語淺薄,文筆生疏,見諒。



1
View的事件分發機制



View的事件分發機制說白了就是點擊事件的傳遞,也就是一個Down事件,若干個Move事件,一個Up事件構成的事件序列的傳遞。


當你手指按了屏幕,點擊事件就會遵循Activity->Window->View這一順序傳遞。


這一傳遞過程有三個重要的方法,分別是:

  • boolean dispatchTouchEcent(MotionEvent ev),

  • boolean onInterceptTouchEvent(MotionEvent event),

  • boolean onTouchEvent(MotionEvent event)


先一個一個簡單介紹下:


dispatchTouchEcent:


只要事件傳遞到了當前View,那么dispatchTouchEcent方法就一定會被調用。返回結果表示是否消耗當前事件。


onInterceptTouchEvent:

在dispatchTouchEcent方法內部調用此方法,用來判斷是否攔截某個事件。如果當前View攔截了某個事件,那么在這同一個事件序列中,此方法不會再次被調用。返回結果表示是否攔截當前事件。


onTouchEvent:


在dispatchTouchEcent方法內調用此方法,用來處理事件。返回結果表示是否處理當前事件,如果不處理,那么在同一個事件序列里面,當前View無法再收到后續的事件。


上面的解釋聽起來比較抽象,我們可以用一段偽代碼來表示上面三個方法的關系:



上面代碼很好的解釋了三個方法之間的關系,我們也可以從代碼中大致摸索到事件傳遞的順序規則:


當點擊事件傳遞到根ViewGroup里,會執行dispatchTouchEvent,在其內部會先調用onInterceptTouchEvent詢問是否攔截事件,若攔截,則執行onTouchEvent方法處理這個事件;


若不攔截,則執行子元素的dispatchTouchEvent,進入向下分發的傳遞,直到事件被處理。


在處理一個事件的時候,是有優先級的,如果設置了OnTouchListener,會先執行其內部的onTouch方法,這時若onTouch方法返回true,那么表示事件被處理了,不會向下傳遞了;


如果返回了false,那么事件會繼續傳遞給onTouchEvent方法處理,在onTouchEvent方法中如果當前設置了OnClickListener,那么就會調用其onClick方法。

所以其優先級為:OnTouchListen>onTouchEvent>OnClickListen。


這里有一種情況,如果一個View的onTouchEvent返回了false,那么它父容器的onTouchEvent方法將會被調用。


我們寫個例子來試一下:



先自定義一個Button,重寫其onTouchEvent方法返回false。在自定義一個MyTouchView作為父布局。效果如下:




大家可以自己試試。


既然如此,在開頭我們說過事件的傳遞順序是Activity->Window->View,所以如果所有的元素都返回了false,那么最后事件就會再次傳遞到Activity里,由Activity的onTouchEvent方法來處理。


《Android開發藝術探索》這本書里總結了11條關于事件傳遞的結論:


  1. 同一個事件序列是指手機接觸屏幕那一刻起,到離開屏幕那一刻結束,有一個down事件,若干個move事件,一個up事件構成。

  2. 某個View一旦決定攔截事件,那么這個事件序列之后的事件都會由它來處理,并且不會再調用onInterceptTouchEvent。

  3. 正常情況下,一個事件序列只能被一個View攔截并消耗。這個原因可以參考第2條,因為一旦攔截了某個事件,那么這個事件序列里的其他事件都會交給這個View來處理,所以同一事件序列中的事件不能分別由兩個View同時處理,但是我們可以通過特殊手段做到,比如一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理。

  4. 一個View如果開始處理事件,如果它不處理down事件(onTouchEvent里面返回了false),那么這個事件序列的其他事件就不會交給它來繼續處理了,而是會交給它的父元素去處理。

  5. 如果一個View處理了down事件,卻沒有處理其他事件,那么這些事件不會交給父元素處理,并且這個View還能繼續受到后續的事件。而這些未處理的事件,最終會交給Activity來處理。

  6. ViewGroup的onInterceptToucheEvent默認返回false,也就是默認不攔截事件。

  7. View沒有InterceptTouchEvent方法,如果有事件傳過來,就會直接調用onTouchEvent方法。

  8. View的onTouchEvent方法默認都會消耗事件,也就是默認返回true,除非他是不可點擊的(longClickable和clickable同時為false)。

  9. View的enable屬性不會影響onTouchEvent的默認返回值。就算一個View是不可見的,只要他是可點擊的(clickable或者longClickable有一個為true),它的onTouchEvent默認返回值也是true。

  10. onClick方法會執行的前提是當前View是可點擊的,并且它收到了down和up事件。

  11. 事件傳遞過程是由外向內的,也就是事件會先傳給父元素在向下傳遞給子元素。但是子元素可以通過requestDisallowInterceptTouchEvent來干預父元素的分發過程,但是down事件除外(因為down事件方法里,會清除所有的標志位)。



2

滑動沖突



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


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

2:外部方向與內部方向一致。


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




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




對于這兩種情況,我們處理的方法也很簡單,并且都有相應的套路。


第一種:第一種的沖突主要是一個橫向一個豎向的,所以我們只要判斷滑動方向是豎向還是橫向的,再讓對應的View滑動即可。


判斷的方法有很多,比如豎直距離與橫向距離的大小比較;滑動路徑與水平形成的夾角等等。


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



3

滑動沖突的處理方法



滑動沖突的攔截方法有兩種:


一種是讓事件都經過父容器的攔截處理,如果父容器需要則攔截,如果不需要則不攔截,成為外部攔截法,其偽代碼如下:



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


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


另一種是父容器不攔截任何事件,將所有事件傳遞給子元素,如果子元素需要則消耗掉,如果不需要則通過requestDisallowInterceptTouchEvent方法交給父容器處理,稱為內部攔截法,使用起來稍顯麻煩。


偽代碼如下:
首先我們需要重寫子元素的dispatchTouchEvent方法:



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



這里父容器也不能攔截down事件。



4
實例說明



(1)內外滑動方向不一致


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




然后將里面換成三個ListView:




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


知道了這個我們只需要將其替換到之前偽代碼里的攔截條件里即可。


先用外部攔截法



這里我們判斷橫向滑動的距離與豎直滑動距離的長短。若是豎直滑動的長,則判斷為豎直滑動,那么就是ListView的滑動,就將intercepted置為false,讓父容器不攔截,交由子元素ListView處理。若是橫向,則intercepted置為true,交由父容器處理。


效果如下:




接下來看看內部攔截法


先自定義一個MyListView繼承ListView,重寫其dispatchTouchEvent方法:



再重寫外部父容器的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方法即可。

處理了滑動沖突后效果如下:





接下來看看內部攔截法


先重寫ScrollView的onInterceptTouchEvent方法,讓其攔截除了Down事件以外的其他方法:



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



最終效果和外部攔截法一樣。


好了,這篇文章到此結束,希望各位看了能對事件分發機制有個大致的了解,并且遇到了滑動沖突的問題能夠迎刃而解。


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

推薦閱讀更多精彩內容