怕什麼真理無窮 進一寸有進一寸的歡喜
在實際項目的開發中,View的事件分發機制起到了很重要的作用,尤其是遇到不按常理出牌的產品經理的時候,沒事給你一個嵌套,沒事再給你加個嵌套,那麼事件分發就顯得尤爲重要。對於經常打交道的滑動衝突,瞭解了View事件分發的原理,也就可以很容易的解決。實踐出真知,我們從一個Demo出發,通過Log的形式分析View是如何將事件分發的,又是如何攔截並作出相應處理的。
一、創建Demo
首先,創建一個簡單的Demo,自定義最外層的ViewGroup,命名爲CustomViewGroup1,重寫onInterceptTouchEvent,onTouchEvent,dispatchTouchEvent三個方法,並將背景設爲紅色。
public class CustomViewGroup1 extends FrameLayout {
private static final String TAG = "CustomeViewGroup1";
public CustomViewGroup1(Context context) {
super(context);
}
public CustomViewGroup1(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "onInterceptTouchEvent:" );
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent:");
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG, "dispatchTouchEvent:");
return super.dispatchTouchEvent(ev);
}
}
和上述代碼一樣,自定義中間層的CustomViewGroup2,重寫攔截和處理的方法,將背景設爲藍色。
接着自定義View,重寫onTouchEvent和dispatchTouchEvent方法,將背景設置爲黃色。
public class CustomView extends Button {
private static final String TAG = "CustomView";
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent:");
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d(TAG, "dispatchTouchEvent:");
return super.dispatchTouchEvent(event);
}
}
最後運行的效果爲
紅色爲最外層CustomViewGroup1,藍色爲中間層CustomViewGroup2,黃色爲CustomView。
二、分析
2.1
從上面自定義ViewGroup的代碼可以看出,View的事件分發涉及到了幾個重要的方法,磨刀不誤砍柴工,下面就先來分析一下這幾個方法。
- boolean dispatchTouchEvent(MotionEvent event):事件分發
- boolean onInterceptTouchEvent(MotionEvent ev):事件攔截
- boolean onTouchEvent(MotionEvent event):事件處理
dispatchTouchEvent爲事件分發的第一步,將事件向下分發;onInterceptTouchEvent表示是否攔截當前事件,當返回true時,表示攔截,也就是下面的事件就不會觸發;onTouchEvent表示處理事件,返回結果表示是否消耗當前事件。這裏不清楚沒關係,接着看下面的分析。
上述三種方法默認返回false,不攔截也不消耗事件
2.2
接下來就通過Log一步一步的看事件分發的過程。當點擊黃色界面,也就是最底層的View時,Log如下:
從上面的Log可以看出,在默認的情況下,最先觸發CustomeViewGroup1(紅色界面)的dispatchTouchEvent和onInterceptTouchEvent方法,接着觸發CustomViewGroup2(藍色界面)的dispatchTouchEvent和onInterceptTouchEvent方法,最後才觸發CustomView(黃色界面)的dispatchTouchEvent和onTouchEvent,一級一級向下傳遞。
上述爲默認情況,不攔截也不消耗任何事件,事件就會一層一層的向下傳遞,那麼當我們在第一層CustomeViewGroup1的onInterceptTouchEvent中返回true,將點擊事件攔截下來,會出現什麼情況呢?
public class CustomViewGroup1 extends FrameLayout {
...
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "onInterceptTouchEvent:" );
return true;
}
}
同樣點擊黃色區域,再看Log:
這時我們發現,Log上只走了CustomeViewGroup1的方法,並且走了onTouchEvent,這就說明CustomeViewGroup1將事件攔截了下來,並沒有分發下去,自己就將事件進行了處理。那麼我們再試着將CustomeViewGroup2的onInterceptTouchEvent返回true,依據上面的分析我們猜測到CustomeViewGroup2就停止傳遞,接着會觸發onTouchEvent。同樣我們修改代碼並點擊VIew黃色區域。
public class CustomViewGroup2 extends FrameLayout {
...
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "onInterceptTouchEvent: ");
return true;
}
}
從上述Log可以看出我們的猜測是正確的,在CustomeViewGroup2進行攔截,事件在分發到CustomeViewGroup2就沒有繼續向下分發了。不過我們又發現Log的最後兩行,先觸發CustomViewGroup2的onTouchEvent接着觸發CustomViewGroup1的 onTouchEvent,這是屬於onTouchEvent返回false,CustomViewGroup2沒有消耗掉當前事件,導致將事件返回給了他的上一層處理。
2.3、小結
從上面的Log分析我們可以得出三種不同情況下的事件分發規則:
1、在正常情況下,三個方法的返回值都爲false,不攔截也不消耗事件,那麼事件就會從ViewGroup1傳遞到ViewGroup2再傳遞到最後的View。
2、當ViewGroup1的onInterceptTouchEvent方法返回true,那麼ViewGroup1就攔截下了這個事件,後面的ViewGroup2和View就接受不到任何事件的信號,並調用ViewGroup1的onTouchEvent方法對事件進行處理。
3、當ViewGroup2的onInterceptTouchEvent方法返回true,那麼ViewGroup2攔截下當前事件,並調用自己的onTouchEvent方法,若ViewGroup2的onTouchEvent返回false,表示不消耗這個事件,那麼就會返回他的上一層,將這個事件交給上一層處理,也就是ViewGroup1的onTouchEvent方法會被調用。相反,ViewGroup2的onTouchEvent返回true,就不會返回給上一層。
簡單的打個比方,ViewGroup1代表校長,ViewGroup2代表老師,View則代表學生
1、正常情況下是校長將一個任務首先下發給老師,接着老師下發給學生,學生收到任務,並執行該任務。
2、第二種情況,校長髮現該任務自己可以處理就不用下發給老師,及校長的onInterceptTouchEvent返回true,將這個任務攔截下來了。後面的老師和學生也就不知道該任務是什麼。
3、第三種情況,校長將任務下發給老師,老師卻將任務攔截了下來,並沒有告訴學生,即ViewGroup2的onInterceptTouchEvent方法返回true。當老師準備自己處理的時候發現這個任務太難,自己解決不了,就onTouchEvent方法返回false,將事情又返回給更高一級的校長處理。
三、onTouchEvent
接下來分析onTouchEvent中是如何處理動作的,我們在onTouchEvent的方法中加上三種最常見的動作的Case,ACTION_DOWN手指點擊,ACTION_MOVE手指滑動,ACTION_UP手指擡起。
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent:");
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onTouchEvent: ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "onTouchEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "onTouchEvent: ACTION_UP");
break;
}
return super.onTouchEvent(event);
}
點擊畫面並滑動,
我們可以看到日誌Log上只有ACTION_DOWN事件發生,並沒有觸發滑動MOVE和手指擡起的UP事件,這是因爲ACTION_DOWN事件默認是返回false,表示不消耗DOWN事件,也就是說DOWN動作發生後是不會向下傳遞,也不會觸發MOVE和UP的動作。所以我們需要觸發後面的動作時,需要將返回值設爲 true,表示消耗當前事件。
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent:");
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "onTouchEvent: ACTION_DOWN");
return true;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "onTouchEvent: ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "onTouchEvent: ACTION_UP");
break;
}
return super.onTouchEvent(event);
}
這時我們同樣去點擊和滑動畫面,
可以從上圖看到,MOVE和UP動作都相應觸發。
四、最後
至此,View的事件分發基本上從頭捋了一遍,更多的源碼的細節準備好好研究一下,在後面的文章中展示出來。其實將事件分發的文章網絡上比比皆是,本沒有想法去記錄這篇文章,只是因爲前幾天在做Android測試題的時候,有涉及到事件分發,當時將onInterceptTouchEvent和onTouchEvent的返回值弄反。原本以爲自己記得很清楚,到了用的時候卻弄混,才發現有些東西需要自己去好好的寫上一遍,纔會真正明白,也記得更牢。古人云:好記性不如爛筆頭,不是沒有道理的。
文章同步個人博客:https://fuusy.github.io
公衆號:小猿說,歡迎關注,分享Android知識。