Android觸摸事件處理流程

Android中的事件分爲按鍵事件和觸摸事件,這裏對觸摸事件進行闡述。Touch事件是由一個ACTION_DOWN,n個ACTION_MOVE,一個ACTION_UP組成onClick,onLongClick,onScroll等事件。Android中的控件都是繼承View這個基類的,而控件分爲兩種:一種是繼承View不能包含其他控件的控件;一種是繼承ViewGroup可以包含其他控件的控件,暫且稱爲容器控件,比如ListView,GridView,LinearLayout等。 

這裏先對幾個函數講解下。

Ø  public boolean dispatchTouchEvent (MotionEventev)     這個方法分發TouchEvent

Ø  public booleanonInterceptTouchEvent(MotionEvent ev)  這個方法攔截TouchEvent

Ø  public boolean onTouchEvent(MotionEvent ev)          這個方法處理TouchEvent

其中view類中有dispatchTouchEvent和onTouchEvent兩個方法,ViewGroup繼承View,而且還新添了一個onInterceptTouchEvent方法。Activity中也無onInterceptTouchEvent方法,但有另外兩種方法。我們可以發現上面3個方法都是返回boolean,那各代表什麼意思呢?

 

     public boolean dispatchTouchEvent (MotionEvent ev) 

  Activity中解釋:

Called to process touch screen events.You can override this to intercept all touch screen events before they aredispatched to the window. Be sure to call this implementation for touch screenevents that should be handled normally.

Parameters

ev

The touch screen event.

Returns

·        boolean Return true if this event was consumed.

它會被調用處理觸摸屏事件,可以重寫覆蓋此方法來攔截所有觸摸屏事件在這些事件分發到窗口之前。通常應該處理觸摸屏事件,一定要調用這個實現。當返回值爲true時,表示這個事件已經被消費了。例如在TextActivity中dispatchTouchEvent在ACTION_MOVE返回true,運行結果如下:

 

也就是它並沒有把那ACTION_MOVE分發下去。

 

public boolean onInterceptTouchEvent (MotionEvent ev)

Implementthis method to intercept all touch screen motion events. This allows you towatch events as they are dispatched to your children, and take ownership of thecurrent gesture at any point.

Usingthis function takes some care, as it has a fairly complicated interaction with View.onTouchEvent(MotionEvent),and using it requires implementing that method as well as this one in thecorrect way. Events will be received in the following order:

1.    You will receive the down event here.

2.    The down event will be handled either by a child of this viewgroup, or given to your own onTouchEvent() method to handle; this means youshould implement onTouchEvent() to return true, so you will continue to see therest of the gesture (instead of looking for a parent view to handle it). Also,by returning true from onTouchEvent(), you will not receive any followingevents in onInterceptTouchEvent() and all touch processing must happen inonTouchEvent() like normal.

3.    For as long as you return false from this function, eachfollowing event (up to and including the final up) will be delivered first hereand then to the target's onTouchEvent().

4.    If you return true from here, you will not receive any followingevents: the target view will receive the same event but with the action ACTION_CANCEL, and all further events will be delivered to youronTouchEvent() method and no longer appear here.

Parameters

ev

The motion event being dispatched down the hierarchy.

Returns

·        Return true to steal motionevents from the children and have them dispatched to this ViewGroup throughonTouchEvent(). The current target will receive an ACTION_CANCEL event, and nofurther messages will be delivered here.

基本意思就是:

1. ACTION_DOWN首先會傳遞到onInterceptTouchEvent()方法

2.如果該ViewGrouponInterceptTouchEvent()在接收到down事件處理完成之後return false,那麼後續的move, up等事件將繼續會先傳遞給該ViewGroup,之後才和down事件一樣傳遞給最終的目標viewonTouchEvent()處理。

3.如果該ViewGrouponInterceptTouchEvent()在接收到down事件處理完成之後return true,那麼後續的move, up等事件將不再傳遞給onInterceptTouchEvent(),而是和down事件一樣傳遞給該ViewGrouponTouchEvent()處理,注意,目標view將接收不到任何事件。

4.如果最終需要處理事件的viewonTouchEvent()返回了false,那麼該事件將被傳遞至其上一層次的viewonTouchEvent()處理。

5.如果最終需要處理事件的viewonTouchEvent()返回了true,那麼後續事件將可以繼續傳遞給該viewonTouchEvent()處理。

Android touch事件傳遞機制:

我們可以看看android源代碼:

Activity.Java

 

暫且不管onUserInteraction方法因爲它只是一個空方法如果你沒實現的話。getWindow().superDispatchTouchEvent(ev)。其中getWindow()返回的是PhoneWindow。

PhoneWindow.java:

 

此函數調用super.dispatchTouchEvent(event),Activity的rootview是PhoneWindow.DecorView,它繼承FrameLayout。通過super.dispatchTouchEvent把touch事件派發給各個Activity的是子view。同時我可以看到,如果子view攔截了事件,則不會執行onTouchEvent函數。

 

 

 

ViewGroup.javadispatchTouchEvent方法:

由於代碼過長這裏就不貼出來了,但也知道它返回的是

return target.dispatchTouchEvent(ev);

這裏target指的是所分發的目標,可以是它本身,也可以是它的子View。

 

ViewGroup.java中的onInterceptTouchEvent方法:

 

 

默認情況下返回false。即不攔截touch事件。

 

 

View.java中的dispatchTouchEvent方法


 

這裏我們很清楚可以知道如果if條件不成立則dispatchTouchEvent的返回值是onTouchEvent的返回值

View.java中的onTouchEvent方法

 

 

所以很容易得到觸摸事件默認處理流程(以ACTION_DOWN事件爲例):

 

當觸摸事件ACTION_DOWN發生之後,先調用Activity中的dispatchTouchEvent函數進行處理,緊接着ACTION_DOWN事件傳遞給ViewGroup中的dispatchTouchEvent函數,接着viewGroup中的dispatchTouchEvent中的ACTION_DOWN事件傳遞到調用ViewGroup中的onInterceptTouchEvent函數,此函數負責攔截ACTION_DOWN事件。由於viewGroup下還包含子View,所以默認返回值爲false,即不攔截此ACTION_DOWN事件。如果返回false,則ACTION_DOWN事件繼續傳遞給其子view。由於子view不是viewGroup的控件,所以ACTION_DOWN事件接着傳遞到onTouchEvent進行處理事件。此時消息的傳遞基本上結束。從上可以分析,motionEvent事件的傳遞是採用隧道方式傳遞。隧道方式,即從根元素依次往下傳遞直到最內層子元素或在中間某一元素中由於某一條件停止傳遞。

接下來繼續分析,事件的處理。剛纔ACTION_DOWN事件傳遞到view的onTouchEvent函數中處理了,默認是返回true,接着view的dispatchTouchEvent返回true,再接着viewGroup的dispatchTouchEvent返回true,最後Activity的dispatchTouchEvent返回true。我們發現,motionEvent事件的處理採用冒泡方式。冒泡方式,從最內層子元素依次往外傳遞直到根元素或在中間某一元素中由於某一條件停止傳遞。

下圖爲程序調試結果:

ACTION_DOWN事件輸出:

 

ACTION_MOVE事件輸出:

 

現在我們來做一些改變,就接着以ACTION_DOWN爲例

情況一:

我們在View中onTouchEvent中ACTION_DOWN返回false,輸出結果如下:

 

可以發現ACTION_DOWN事件傳遞到上層的ViewGroup的onTouchEvent,同時返回true,說明事件被ViewGroup消費了。同時之後的touch事件(ACTION_MOVE等)不再傳遞給view,只傳遞到ViewGroup,由ViewGroup的onTouchEvent函數處理touch事件。同時onInterceptTouchEvent也不再調用。

情況二:

我們在View中onTouchEvent中ACTION_MOVE返回false,輸出結果如下:

 

由於view未消費此ACTION_MOVE事件,按照原理來說應該是將事件處理冒泡到ViewGroup去處理,但結果卻是Activity處理的。我們知道,觸摸事件首先發生的就是ACTION_DOWN事件,我們在onInterceptTouchEvent所解釋就可以發現ACTION_DOWN與ACTION_MOVE等事件有區別,ACTION_DOWN事件作爲起始事件,它的重要性是要超過ACTION_MOVE和ACTION_UP的,如果發生了ACTION_MOVE或者ACTION_UP,那麼一定曾經發生了ACTION_DOWN。也就是說ACTION_DOWN事件被view消費了,而ACTION_MOVE事件沒被消費,傳遞到ViewGroup,由於之前ViewGroup沒處理ACTION_DOWN事件,所以它也不處理ACTION_MOVE。但Activity卻不一樣,它可以接受所有事件。

情況三:

這次在ViewGroup中的onInterceptTouchEvent中ACTION_DOWN返回true

結果如下:

 

它直接把事件發送給ViewGroup的onTouchEvent處理,此後不再攔截事件直接到viewGroup中的onTouchEvent處理。

情況四:

在ViewGroup中的onInterceptTouchEvent中ACTION_MOVE返回true

結果如下:

 

ACTION_MOVE被ViewGroup攔截了,上次處理ACTION_DOWN的view則會收到ACTION_CANCEL事件,之後ViewGroup不再攔截後續事件,事件直接在ViewGroup中的onTouchEvent處理。

還有很多情況,這裏不一一列出了。

 

在寫這個demo一開始,我發現重寫了onTouchEvent函數就無法獲取onClick和onLongClick事件。接下來討論當重寫了onTouchEvent,android是如何區分onClick,onLongClick事件的。搞清楚此問題對於如何響應UI各種事件是很重要的,例如類似android桌面的應用程序圖標,可以點擊,然後長按拖動。

Android中onclick,onLongClick是都是由ACTION_DOWN,ACTION_UP組成。如果在同一個View中onTouchEvent、onclick、onLongClick都進行了重寫。onTouchEvent最先捕獲ACTION_DOWN、ACTION_UP等單元事件。接下來纔可能發生onClick、onLongClick事件。一個onclick事件是由ACTION_DOWN和ACTION_UP組成的。一個onLongClick事件至少有一個ACTION_DOWN。那android具體是怎麼實現的呢,可以看源代碼:

View.java中:

 

上面我已經展示了onTouchEvent方法,但由於過長我摺疊了一部分代碼,現在展開

 

這個if條件內執行就是click事件處理及longClick事件處理。先看ACTION_DOWN事件

 

我們看到有個postDelayed方法,此方法意思爲延時把線程插入到消息隊列。即ACTION_DOWN後觸發一個postDelayed方法。mPendingCheckForTap屬於CheckForTap的實例。

 

在裏面開啓一個線程當爲LONG_CLICKABLE,調用postCheckForLongClick方法。

 

再看mPendingCheckForLongPress這個線程。

 

 

當上面一系列條件全都符合的情況就調用performLongClick方法。

此方法就調用我們熟悉的onLongClick函數。

 

至此onLongClick事件已經分析完。再接着看ACTION_UP事件

 

 

直接關注performClick函數:

 

這裏我們同樣看到了我們熟悉的onClick方法。

所以android這種機制是保證了此onClick和onLongClick能與onTouchEvent並存。接下來考慮onclick與onLongClick是否並存,其實這個問題前面已經闡述了。只要此事件沒被消費,它還會接着傳遞下去。從上面知道onLongClick是在單獨的線程執行,發生在ACTION_UP之前。Onclick發生在ACTION_UP之後,也就是說,如果在onLongClick返回false,onclick就會發生,而onlongClick返回true,則代表此事件已經被消費。Onclick不再發生。

返回false


 

返回true


 

如果多次設置onclick事件,則最頂層的onclick覆蓋掉底層onclick事件;多次設置onLongClick事件,則只執行底層view的onLongClick方法。當ACTION_DOWN調用之後返回false。

 

 

可以看到ACTION_DOWN被消費了,所以不會讓上層處理了。

 




最近在工作中經常處理觸摸事件,有時候會出現一些奇怪的bug,比如有時候會檢測不到ACTION_MOVE和ACTION_UP,我決定下決心寫個測試的小程序,來研究一個觸摸事件從上往下是怎麼傳遞和處理的。

先說下大概的流程吧,這個應該在很多博客中都有講解:當一個事件來臨的時候,會先傳遞給最外層的ViewGroup(比如LinearLayout,FrameLayout),如果這個ViewGroup沒有去攔截這個事件的話,纔會給傳遞給下層的ViewGroup或者View。如果被攔截掉的話,它會自己去處理這個事件,這個ViewGroup內的View將無法得知上層發生了什麼。

ViewGroup的攔截事件的函數爲

1 public boolean onInterceptTouchEvent(MotionEvent ev)

onInterceptTouchEvent的參數ev就是一個觸摸事件,可以從ev獲取到事件的座標,類型,當前屏幕上點的個數等等。通常我們在繼承ViewGroup的時候都會重寫這個方法,判斷目前需不需要攔截,即返回true還是false。返回true的時候表明事件不再往下傳了,否則就往下傳。那返回true的時候怎麼處理呢?

這就需要onTouchEvent():

1 public boolean onTouchEvent(MotionEvent ev)

具體怎麼實現就根據實際的需要來了。我們發現他的返回值也是boolean,那返回true或者false的時候會有什麼影響呢?用一張圖來說明:

觸摸流程,全是FALSE

這個一個典型的流程,也就是所有的相關方法都返回false的時候,一個事件先到了LinearLayout,它不攔截,然後就往下面跑,到了FrameLayout上,他又不處理,又傳到了Button上,這個時候Button返回了false,然後這個事件往上傳,最後沒有人處理。當FrameLayout的兩個方法返回true的時候會怎樣呢?

FrameLayout返回true

FrameLayout的onInterceptTouchEvent返回true後,就攔截觸摸消息了,然後交給自己的onTouchEvent處理。這裏面的邏輯自己定義就好了,如果這個事件被消費掉了,返回true就可以了,這樣系統就不會接着傳了,事件處理到此爲止。

是不是按下,移動,鬆開的流程都是按照這樣處理的呢?答案是否定的。ACTION_DOWN事件的判斷和處理,直接影響到了後續的ACTION_MOVE和ACTION_UP,在上面的圖中,FrameLayout的onTouchEvent返回了true,那麼當ACTION_MOVE來到FrameLayout這一層的時候,就不再需要通過onInterceptTouchEvent攔截了,直接用onTouchEvent處理。如果說一個ACTION_DOWN從頭到尾都是返回false,那麼後續的ACTION_MOVE和ACTION_UP就沒法被感知到了。

下面說一下多點觸摸的情況:

多點觸摸的時候,會多兩個事件 ACTION_POINTER_UP和ACTION_POINTER_DOWN。當第一個手指按下的時候,會產生ACTION_DOWN,當第二個手指按下的時候,會產生ACTION_POINTER_DOWN,第三個或者更多手指按下的時候,也是ACTION_POINTER_DOWN,如果此時有一個手指離開屏幕,會產生ACTION_POINTER_UP,當最後一個手指離開屏幕的時候,纔會產生ACTION_UP。在整個操作過程中,一個觸點會始終保持一個固定的ID,方便記錄和處理,比如說在ACTION_MOVE的處理過程中,可以通過MotionEvent的getX(int pointerIndex)來獲取某個點的座標。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章