Android訊息傳遞機制分析
1.事件響應機制的預備知識
在深入瞭解Android事件響應機制前,一些預備知識我們應該有所瞭解。
1.1 onTouch是優先於onClick執行,事件傳遞的順序是先經過onTouch,再傳遞到onClick。
1.2 Android中的事件onClick、onLongClick、onScroll等,都是由多個Touch事件(一個ACTION_DOWN,多個ACTION_MOVE,一個ACTION_UP)組成。
1.3 Android事件響應機制是“由外到內”分發、“由內到外”處理的形式實現的。
1.4 MotionEvent物件的四種狀態
MotionEvent.ACTION_DOWN:手指按下螢幕的瞬間。
MotionEvent.ACTION_MOVE:手指在螢幕上移動
MotionEvent.ACTION_UP:手指離開螢幕瞬間
MotionEvent.ACTION_CANCEL:取消手勢
1.5 訊息宿主
Activity只有dispatchTouchEvent和onTouchEvent,沒有onInterceptTouchEvent
ViewGroup三個函式都有
View只有dispatchTouchEvent和onTouchEvent,沒有onInterceptTouchEvent
2.Android事件處理的三個重要函式
Android事件分發機制主要由“事件分發”—>“事件攔截”—>“事件響應”這三步來進行邏輯控制的。本文也將從這三步對應的函式來分析。
2.1 事件分發:public boolean dispatchTouchEvent(MotionEvent ev) 當監聽到有觸發事件時,首先由Activity進行捕獲,然後事件就進入事件分發的流程。Activity本身沒有事件攔截,從而將事件傳遞給最外層的View的dispatchTouchEvent(MotionEvent ev)方法,該方法將對事件進行分發。
return true : View消費所有事件。 return false :停止分發,交由上層控制元件的onTouchEvent方法進行消費,如果本層控制元件是Activity,那麼事件將被系統消費、處理。 super.dispatchTouchEvent(ev): 將事件交由本層的事件攔截onInterceptTouchEvent方法處理。
2.2 事件攔截:public boolean onInterceptTouchEvent(MotionEvent ev) return true: 對事件攔截,交由本層的onTouchEvent進行處理。 return false: 不攔截,分發到子View,由子View的dispatchTouchEvent方法處理。 super.onInterceptTouchEvent(ev):預設表示事件不攔截,分發給子控制元件。
2.3 事件響應:public boolean onTouchEvent(MotionEvent ev) return true: 表示onTouchEvent處理完事件後消費了此次事件。 return false: 不響應事件,不斷的傳遞給上層的onTouchEvent方法處理,直到某個View的onTouchEvent返回true,則認為該事件被消費。如果到最頂層View還是返回false,那麼該事件不消費,將交由Activity的onTouchEvent進行處理。 return: super.onTouchEvent,不響應事件,結果與return返回false一樣。
此時,就可以得出我們通常所說的兩個方向: 1.事件傳遞的方向:父控制元件→子控制元件 2.事件響應的方向:子控制元件→父控制元件
結合上面的理解,我們再來看看Touch事件傳遞機制流程圖
3.例項分析
理解事件傳遞的基本邏輯,對於工作過程中解決滑動事件衝突非常有幫助。比如我們此時有一個父控制元件ViewPager,這個ViewPager其中一個Item是ScrollView,此時會發生什麼問題呢?當ViewPager滑動到ScrollView這個條目的時候,再左右滑動,發現ViewPager再也左右滑動不了了。這是為什麼呢? 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中,既然攔截下來了所有事件,那麼所有事件就會傳遞到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事件。它代表什麼含義呢? 如果一個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事件。
4. 總結
通過上面的敘述,相信大家對Android的分發機制有了初步的理解。為了加深大家的理解,下面做個簡單的總結。
ViewGroup預設不攔截任何事件。 點選事件的分發過程如下:dispatchTouchEvent—>onTouchListener的OnTouch方法—>onTouchEvent—>onClickListener的onClick方法。從而也可以看出onTouch優先於onClick執行。 子View可以通過使用getParent().requestDisallowInterceptTouchEvent(true),阻止ViewGroup對其MOVE或UP事件進行攔截。 一個點選事件產生後,傳遞過程是:Activity—>Window—>View。頂級View接受到事件後,就會按照上面的規則去分發事件。