解析 Android 事件傳遞機制

分類:編程 時間:2017-02-12

原文地址:https://gold.xitu.io/entry/58522e1d128fe1006d9936ad


深入淺出解析android事件傳遞機制

  關於Android中的事件傳遞,在Android系統源代碼層級的實現上非常復雜,而對於應用程序的開發而言,我們不必深究太多細節,只需要掌握事件傳遞機制所帶來的一些結論即可。本文通過父控件→子控件的事件模型來聊聊關於事件傳遞機制的一些知識點。


  在開始分析之前,大家可以先記幾個結論,這樣便於理解。
  結論1:事件一定是先到達父控件上。
  結論2:事件簡單來說可以分為三種:Down事件、Move事件、Up事件,結合結論1可以得到,Down事件、Move事件Up事件均是最先到達父控件上。
  結論3:父控件和父類不是一回事,這兩個概念初學者很容易混淆。


  事件模型:父控件→子控件
  關於這種事件模型主要涉及到3個概念:事件的分發、事件的攔截、事件的響應。
我們先回顧下基礎知識,事件分發是dispatchTouchEvent,事件攔截是onInterceptTouchEvent,事件的響應是onTouchEvent,對於ViewGroup類型的控件來說,它擁有這三種方法,而對於單個View控件來說,它只有dispatchTouchEvent和onTouchEvent,因為View不能包含其他的View,所以不需要判斷是否要攔截事件。
  一個事件如果到達某一個View或者ViewGroup,那麽一定會最先調用到這個控件的dispatchTouchEvent。dispatchTouchEvent這個方法的含義就是把一個事件給分發下去,那麽它具體分發的邏輯是怎樣的呢?

圖1

   A.它首先會先調用自身的onInterceptTouchEvent方法,調用此方法的目的是為了,先讓自己這個控件判斷下是否需要把此事件攔截下來,如果攔截下來,那麽就代表自己這個控件需要處理這個事件,所以此時會調用自身onTouchEvent來對這個事件進行響應。

 

  圖2 

  B.如果不攔截下來,那麽才會有後續的事件向下傳遞的流程。將這個事件傳遞給子控件。現在子控件接收到了這個事件,上文提過,一個事件到達了一個View或者ViewGroup,就會最先調用這個控件的dispatchTouchEvent,所以此時,事件到達了子控件的dispatchTouchEvent方法,如果這個控件仍然是一個ViewGroup的類型,那麽事件繼續分發的邏輯依然遵循A流程的邏輯。

圖3

  C.如果這個子控件只是一個View,而不是ViewGroup,那麽此時,事件分發的邏輯略有不同。由於View沒有onInterceptTouchEvent的方法,所以當一個事件到達這個View的dispatchTouchEvent的時候,dispatchTouchEvent就調用不到onInterceptTouchEvent,它會直接調用onTouchEvent的方法,直接讓這個View來響應此事件。

 

圖4

  通過ABC三個流程,我們就能夠非常清楚地了解傳遞的邏輯。父控件傳遞給子控件,當沒有控件攔截事件的話,將會一層層地向下傳遞,直到最後一個子控件。
接下來我們分析事件響應的方向。我們知道onTouchEvent是有返回值的,要麽是true,要麽是false,不同的返回值會有不同的表現。

                                   圖5


  如上圖所示,如果ViewGroupB攔截了事件,那麽此時事件就會由ViewGroupB來響應,調用ViewGroupB中的onTouchEvent,此時ViewGroupB中的onTouchEvent的返回值有兩種可能,一種是true,一種是false,如果返回true,則代表ViewGroupB消費了此事件,事件此時終止。如果返回的值是false,那麽此時這個事件會回傳給父控件,調用到父控件的onTouchEvent方法,由父控件來進行響應,那父控件的onTouchEvent也是同樣的邏輯。要麽消費事件,要麽回傳給父控件的父控件。
  此時,就可以得出我們通常所說的兩個方向:
  1.事件傳遞的方向:父控件→子控件
  2.事件響應的方向:子控件→父控件


  當然,僅僅是這個結論是無法滿足我們實際開發的需要,我們需要更細致的分析。這裏有一個細節上的問題需要註意,就是事件分為Down事件、Move事件、Up事件,任何一種事件都遵循事件傳遞和響應的邏輯原則,很多開發者常常會認為Down-Move-Up連在一起才是一個事件的產生,這種想法是不對的。
事件的起點是由Down事件開始的,然後產生一系列的Move事件,最後通常以Up事件結束。當Down事件產生的時候,會由父控件傳遞給子控件,Move事件也由父控件傳遞給子控件,Up事件也由父控件傳遞給子控件。它們都遵循同樣的傳遞事件的邏輯流程。不過Down事件最終響應的結果,會影響到後續事件的執行。這句話是什麽意思呢?
我們看圖6,如果Down事件傳遞到了子View上,但是子View的onTouchEvent對於這個Down事件的處理是return了一個false,這樣的結果就是會造成父View的onTouchEvent的調用,同時還有另外一個後果,那就是後續的Move事件、Up事件就都傳遞不到子View上。所以,如果一個View要處理滑動事件,也就是Move事件的話,那麽它一定不能在onTouchEvent中,對Down事件return false。

  如果Down事件到了父View上,父View需要調用自身的onInterceptTouchEvent判斷是否對這個Down事件進行攔截,如果攔截,return了true,那麽這個事件就會到父View的onTouchEvent中進行響應。如果此時父View的onTouchEvent也返回了true,那麽代表這個父View響應了Down事件。不過這裏有一點不太一樣的地方是,事件傳遞到父View的onTouchEvent方法是因為自身的onInterceptTouchEvent方法判斷攔截導致的,而不是由子View回傳回來的,在這種情況下,當Move事件、Up事件傳遞到父View的時候,它當然不會傳遞給子View,並且,也不再調用自身的onInterceptTouchEvent方法。

  理解事件傳遞的基本邏輯,對於工作過程中解決滑動事件沖突非常有幫助。比如我們此時有一個父控件ViewPager,這個ViewPager其中一個Item是ScrollView,此時會發生什麽問題呢?當ViewPager滑動到ScrollView這個條目的時候,再左右滑動,發現ViewPager再也左右滑動不了了。這是為什麽呢?我們結合圖6一起來分析一下。


  1.我們都知道ViewPager是能夠橫向滑動的控件,而ScrollView是縱向滑動的控件,當Down事件產生的時候,此時會由ViewPager傳遞給ScrollView,ViewPager沒有對Down事件攔截,ScrollView也不會對這個Down事件進行攔截,所以事件就會傳遞給ScrollView的孩子,也就是類似於圖6中的子View,子View如果沒有對Down事件響應,那麽最後會到ScrollView中的onTouchEvent,而ScrollView的onTouchEvent對於這個Down事件返回了true,代表ScrollView消費了這個Down事件。


  2.接下來開始滑動手指,產生一系列的Move事件。Move事件也是由ViewPager傳遞給ScrollView。由於Down事件是被ScrollView的onTouchEvent中消費的,所以Move事件就不會傳遞給ScrollView的子控件了。一系列的Move事件也是在ScrollView的onTouchEvent中被執行。


  3.最後的Up事件也是由ScrollView中的onTouchEvent消費。


  從上述1至3的步驟中,我們看出來無論是Down事件、Move事件還是Up事件,最後全部都是被ScrollView所消費。從頭到尾ViewPager的onTouchEvent都沒有得到執行。而ViewPager之所以能夠左右滑動,正是因為ViewPager的onTouchEvent裏面的代碼邏輯產生的效果。ViewPager的onTouchEvent沒有執行,這個ViewPager當然就不能夠左右滑動了。所以解決上述問題,就是在於如何讓ViewPager中的onTouchEvent方法執行。
我們可以自定義一個MyViewPager繼承ViewPager,重寫onInterceptTouchEvent方法,如果我們在onInterceptTouchEvent方法中直接野蠻地return一個true,此時就代表無論是Down事件、Move事件,還是Up事件,全部都攔截下來了,攔截在MyViewPager中,我們可以認為是圖6中的ViewGroupB,既然攔截下來了所有事件,那麽所有事件就會傳遞到MyViewPager的onTouchEvent,所以此時,這個MyViewPager一定可以左右滑動。

  但是,由此會引發另外一個問題,就是這個ScrollView不能上下滑動了。這又是為什麽呢?因為ScrollView能夠上下滑動的代碼邏輯在ScrollView中的onTouchEvent方法內,而此時事件又全部被MyViewPager攔截了下來,ScrollView完全得不到事件,onTouchEvent方法得不到執行,自然不能上下滑動。所以我們需要修改MyViewPager中的onInterceptTouchEvent的邏輯。


  ViewPager只對左右滑動感興趣,而ScrollView對上下滑動這個動作感興趣,所以我們只需要在MyViewPager的onInterceptTouchEvent中,根據多個Move事件,判斷是左右滑動還是上下滑動,如果是左右滑動,return true將事件攔截下來,如果是上下滑動,return false將事件傳遞給ScrollView,這樣就能解決問題了。
所以,對於Down事件,我們一般都不進行攔截,判斷是否攔截得根據一些列的Move事件才能得出具體的條件是否成立。
Cancel事件的產生:

  剛才我們說了事件一般有三個,Down、Move、Up,這三個事件比較好理解。其實還有一種事件就是Cancel事件。它代表什麽含義呢?
還是回到圖6,如果一個Down事件產生了,這個Down事件從ViewGroupA傳遞到ViewGroupB,最終到達子View,被子View的onTouchEvent消費,return了true,那麽此時Down事件就終止了。接下來後續的Move事件也會從ViewGroupA傳遞給ViewGroupB,也就是說ViewGroupA和ViewGroupB會比子View更先拿到Move事件,那既然ViewGroupA和ViewGroupB比子View更先拿到Move事件,那麽他們當中的任何一個都有可能在某一個Move事件中,把這個Move事件給攔截下來,一旦Move事件被攔截下來了,子View肯定就拿不到這個Move事件了,不過,此時子View會產生一個新的事件,就是Cancel事件。


  所以一個正常的事件序列是 Down→Move→Up,這樣才被認為是一個正常的事件序列。如果一個View響應的Down事件,可是卻被沒有正常結尾,Move事件或者Up事件被攔截了,此時非正常結尾的情況就會給子View產生一個新的事件Cancel。


  子控件可以影響父控件是否攔截的行為
  子控件是可以幹預父控件是否攔截事件的結果。通過在子View中dispatchTouchEvent中增加一行代碼即可。getParent().requestDisallowInterceptTouchEvent(true);這行代碼就可以請求父控件不要攔截事件。


  很多人可能不太明白這句話的意思,既然事件一定是先到達父控件,然後才到達子View,那也就是getParent().requestDisallowInterceptTouchEvent(true);這句話是在父控件是否攔截判斷結束之後才調用,怎麽能改變父控件是否攔截的結果呢,這裏存在一個執行先後順序的疑惑。
  

  其實是這樣的,getParent().requestDisallowInterceptTouchEvent(true);達到的效果不是修改父控件對本次事件是否攔截的結果,而影響的是後續事件。比如子View在Down事件中調用了getParent().requestDisallowInterceptTouchEvent(true);這行代碼,那麽在後續Move事件、Up事件產生到達父控件的時候,父控件就不會再攔截了。所以getParent().requestDisallowInterceptTouchEvent(true);只會影響Move事件和Up事件,影響不到Down事件。


http://www.jianshu.com/p/e99b5e8bd67b



Tags: Android 應用程序 源代碼 基礎知識 初學者

文章來源:


ads
ads

相關文章
ads

相關文章

ad