Android的事件分發與消費機制
一、Touch的三個重要方法
在Android中,與觸控事件也就是 Touch 相關的有三個重要方法,這三個方法共同完成觸控事件的分發。
- public boolean dispatchTouchEvent(MotionEvent ev) :事件分發
- public boolean onInterceptTouchEvent(MotionEvent ev):事件攔截
- public boolean onTouchEvent(MotionEvent ev):事件響應
下面就依次來分析這三個方法。
1、事件分發
public boolean dispatchTouchEvent(MotionEvent ev)
顧名思義,事件的分發就是當一個觸控事件發生的時候,會按照Activity -> Window -> View的順序依次往下傳遞。也就是說系統會把這個事件傳遞給一個具體的View,從而來執行或者說響應這個事件。我們來看部落格上是如何說明的:
Touch 事件發生時 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法會以隧道方式(從根元素依次往下傳遞直到最內層子元素或在中間某一元素中由於某一條件停止傳遞)將事件傳遞給最外層 View 的 dispatchTouchEvent(MotionEvent ev) 方法,並由該 View 的 dispatchTouchEvent(MotionEvent ev) 方法對事件進行分發。
這裡要注意幾個地方:
第一觸控事件傳遞的開始一定是Activity;
第二傳遞方式是通過隧道方式傳遞;
第三一直傳遞到一個最外層的View,也就是頂級View,由該View的這個方法來進行分發。
那麼我們不禁有個疑問,那Activity能不能直接分發呢,換句話說,傳遞過程什麼時候終止呢?答案就是通過判斷這個方法的返回值來處理分發的邏輯。我們看到,分發方法的返回值是 boolean ,所以返回值有 true 和 false ,再加上一個繼承超類的方法 super ,所以一共有三種返回值。
依次來看:
-
如果 return true,事件會分發給當前 View 並由 dispatchTouchEvent 方法進行消費,同時事件會停止向下傳遞;
-
如果 return false,事件會分發給事件來源Activity或者父級View的 onTouchEvent 進行消費;
-
如果return super.dispatchTouchEvent(ev),事件會自動的分發給當前 View 的 onInterceptTouchEvent 方法。
部落格上的這個結論單獨拿來看可能有點抽象,我當時看的時候也是,先不急著說清楚,稍後看案例就恍然大悟了。現在只要明白一個概念,事件的分發是按照依次往下的順序,並根據返回結果,決定由誰進行消費。
2、事件攔截
public boolean onInterceptTouchEvent(MotionEvent ev)
與事件分發不同的是,該方法是在事件分發的 dispatchTouchEvent 方法內部進行呼叫。是用來判斷在觸控事件傳遞過程中,是否攔截某個事件。部落格的解釋是這樣的:
在外層 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系統預設的 super.dispatchTouchEvent(ev) 情況下,事件會自動的分發給當前 View 的 onInterceptTouchEvent 方法。
同樣的是,該方法仍然通過返回值來判斷是否攔截當前事件。
-
如果return true,則表示將事件進行攔截,並將攔截到的事件交由當前 View 的 onTouchEvent 進行處理;
-
如果return false,則表示將事件放行,當前 View 上的事件會被傳遞到子 View 上,再由子 View 的 dispatchTouchEvent 來開始這個事件的分發;
-
如果return super.onInterceptTouchEvent(ev),事件預設會被攔截,並將攔截到的事件交由當前 View 的 onTouchEvent 進行處理。
當然現在我們也不需要深究是什麼意思,只需注意,如果當前的View已經攔截了某一個事件,那麼在觸控事件的一整個事件序列中,也就是down -> move -> ... -> up一整個事件中,此方法不會被在呼叫。
3、事件響應
public boolean onTouchEvent(MotionEvent ev)
這個方法就是用來處理具體點選事件的,它是在dispatchTouchEvent方法中呼叫,部落格是這樣說的:
在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 並且 onInterceptTouchEvent 返回 true 或返回 super.onInterceptTouchEvent(ev) 的情況下 onTouchEvent 會被呼叫。
它的返回值表示是否消費當前事件,也就是是否響應當前事件,具體邏輯如下:
-
如果return true 則會接收並消費該事件。
-
如果return false,那麼這個事件會向上傳遞,並由上層 View 的 onTouchEvent 來接收,如果傳遞到上面的 onTouchEvent 還是返回 false,這個事件無效,且接收不到下一次事件。
-
如果return super.onTouchEvent(ev) 預設處理事件的邏輯和返回 false 時相同。
值得注意的是,返回結果表示是否消費當前事件,如果不消費的話,那麼當前View就無法再次接受到事件。
那麼看到這裡的話,我相信大家還是沒有一個清晰的認識,下面我們就從三者的聯絡與區別上再次說明。
二、三種方法的區別與聯絡
1、區別
三者的區別在部落格開頭已經說明的非常清楚了,我也仿照文章中表格的形式,總結了一個表格。
Touch事件
也就是說,這三個觸控事件相關的方法,Activity、View、ViewGroup及其子類都能夠響應。但是Activity對事件攔截不響應。
值得注意的是,如果當前View能夠新增子View或者整個View中有多個子View,那麼方法都能響應。但是如果當前View本身已經是一個最小View,那就只能夠響應onTouchEvent。
原因簡單想一下就知道了,事件分發與事件攔截都是由上級往下級傳遞事件,如果一個View已經是最後一級了,它就無法進行事件分發或事件攔截的必要了。就相當於做汽車,中間站可能會停站進行讓乘客上車下車,但是到了終點站你只有下車,這是一樣的道理。
2、聯絡
上面已經說明了三者的區別,那麼三者的關係是怎麼樣的呢?在《Android開發藝術探索》一書中,有這樣一串虛擬碼,就跟書中說明的一樣,“已經將三者的關係表現得淋漓盡致”,我們來看這串虛擬碼:
/**
* @Title: dispatchTouchEvent
* @Description: 三者關係的虛擬碼
* @return: boolean
*
/
public boolean dispatchTouchEvent(MotionEvent ev){
//預設返回值
boolean consume = false;
//如果事件發生了攔截
if(onInterceptTouchEvent(ev)){
//消費事件
consume = onTouchEvent(ev);
}else{
//否則分發給子View
consume = child.dispatchTouchEvent(ev);
}
//返回值
return consume;
}
用語言來說就是,一旦發生觸控事件,根 View/ViewGroup 會呼叫 dispatchTouchEvent 方法,如果這個方法返回 false ,觸控事件不生效。但是如果它的 onInterceptTouchEvent 方法返回了true,代表事件被攔截,那麼事件就會交給當前View的 onTouchEvent 方法對事件進行消費。但是如果沒有攔截呢,那麼會繼續將事件分發給當前View的子View,子View繼續呼叫 dispatchTouchEvent 方法,如此迴圈,直到事件被消費。
但是需要注意的是,我們需要考慮另外一種情況,那就是最終的View的 onTouchEvent 方法仍然返回了false,那麼此時,它的父View的 onTouchEvent 方法將會被呼叫,如果父View仍然沒有消費該事件,那麼就繼續往上級傳遞,直到傳到最後的Activity呼叫 onTouchEvent 方法。
這就跟我們之前的返回值一一對應了起來,現在回頭看看,應該能對事件分發有了一個比較清晰的概念。好吧,如果還是沒有概念,我們只能上圖了。圖也是黑馬教程中的圖,我優化了一下,看的更加清晰。
事件消費
至此,我們理論上的東西基本已經講完了,現在我們就通過一個例子來說明具體的事件分發情況。
三、案例分析
1、案例說明
文章開頭已經說過了,沒有更好的例子能說明這個問題了。我將類的結構稍微更改了一下,內容不變。
先自定義兩個View繼承 LinearLayout ,其中為 TouchEventFather 為父View ,TouchEventChilds 為子View。
/**
* @ClassName: TouchEventFather
* @Description:父View
* @author: [email protected]
* @date: 2016年5月9日 下午9:54:14
*/
public class TouchEventFather extends LinearLayout {
public TouchEventFather(Context context) {
super(context);
}
public TouchEventFather(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("sunzn", "TouchEventFather | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.dispatchTouchEvent(ev);
// return false;
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("sunzn", "TouchEventFather | onInterceptTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.onInterceptTouchEvent(ev);
// return false;
}
public boolean onTouchEvent(MotionEvent ev) {
Log.d("sunzn", "TouchEventFather | onTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.onTouchEvent(ev);
}
}
/**
* @ClassName: TouchEventFather
* @Description:子View
* @author: [email protected]
* @date: 2016年5月9日 下午9:54:14
*/
public class TouchEventChilds extends LinearLayout {
public TouchEventChilds(Context context) {
super(context);
}
public TouchEventChilds(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e("sunzn", "TouchEventChilds | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.dispatchTouchEvent(ev);
// return false;
}
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("sunzn", "TouchEventChilds | onInterceptTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.onInterceptTouchEvent(ev);
// return false;
}
public boolean onTouchEvent(MotionEvent ev) {
Log.d("sunzn", "TouchEventChilds | onTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.onTouchEvent(ev);
}
}
定義好這兩個自定義佈局後,我們可以在佈局檔案中設定佈局,注意一定要寫自定義佈局的完整包名。
<xml version="1.0" encoding="utf-8"?>
<cn.sunzn.tevent.view.TouchEventFather xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#468AD7"
android:gravity="center"
android:orientation="vertical">
<cn.sunzn.tevent.view.TouchEventChilds
android:id="@+id/childs"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:background="#E1110D" />
</cn.sunzn.tevent.view.TouchEventFather>
接下來就是主Activity:
/**
* @ClassName: TouchEventActivity
* @Description:事件分發機制詳解
* @author: [email protected]
* @date: 2016年5月9日 下午9:53:27
*/
public class TouchEventActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.w("sunzn", "TouchEventActivity | dispatchTouchEvent --> " + TouchEventUtil.getTouchAction(ev.getAction()));
return super.dispatchTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
Log.w("sunzn", "TouchEventActivity | onTouchEvent --> " + TouchEventUtil.getTouchAction(event.getAction()));
return super.onTouchEvent(event);
}
}
最後是一個工具類,只是將各個點選狀態封裝到一個方法中:
/**
* @ClassName: TouchEventUtil
* @Description:點選事件工具類
* @author: [email protected]
* @date: 2016年5月9日 下午9:53:51
*/
public class TouchEventUtil {
public static String getTouchAction(int actionId) {
String actionName = "Unknow:id=" + actionId;
switch (actionId) {
case MotionEvent.ACTION_DOWN:
actionName = "ACTION_DOWN";
break;
case MotionEvent.ACTION_MOVE:
actionName = "ACTION_MOVE";
break;
case MotionEvent.ACTION_UP:
actionName = "ACTION_UP";
break;
case MotionEvent.ACTION_CANCEL:
actionName = "ACTION_CANCEL";
break;
case MotionEvent.ACTION_OUTSIDE:
actionName = "ACTION_OUTSIDE";
break;
}
return actionName;
}
}
好了,程式碼介紹完了,部署到手機上的時候,應該是這個樣子。
事件分發案例
我們現在就通過不同的返回值,來具體看事件分發的過程,注意我們將程式碼部署到手機上的時候,預設做的動作是點選中間紅色部分一下,這樣更能直觀的觀察日誌情況。另外由於原博主總結的非常好,這裡我就直接截圖過來,然後具體說明一下。
2、情況一
case1
過程及結果分析:
-
事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分發給 TouchEventFather 控制元件的dispatchTouchEvent;
-
而該TouchEventFather 控制元件的 dispatchTouchEvent 返回 false,表示對獲取到的事件停止向下傳遞,同時也不對該事件進行消費;
-
由於 TouchEventFather 獲取的事件直接來自 TouchEventActivity ,則會將事件返回給 TouchEventActivity 的 onTouchEvent 進行消費;
-
最後直接由 TouchEventActivity 來響應手指移動和擡起事件。
3、情況二
case2
過程及結果分析:
-
事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分發給 TouchEventFather 控制元件的 dispatchTouchEvent;
-
而該TouchEventFather 控制元件的 dispatchTouchEvent 返回 true,表示分發事件到 TouchEventFather 控制元件並由該控制元件的 dispatchTouchEvent 進行消費;
-
又因為TouchEventActivity 不斷的分發事件到 TouchEventFather 控制元件的 dispatchTouchEvent,而 TouchEventFather 控制元件的 dispatchTouchEvent 也不斷的將獲取到的事件進行消費。
4、情況三
case3
過程及結果分析:
-
事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分發給 TouchEventFather 控制元件的 dispatchTouchEvent;
-
而該TouchEventFather 控制元件的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),表示對事件進行分發並向下傳遞給 TouchEventFather 控制元件的 onInterceptTouchEvent 方法;
-
而該方法返回 true 表示對所獲取到的事件進行攔截並將事件傳遞給 TouchEventFather 控制元件的 onTouchEvent 進行處理,TouchEventFather 控制元件的 onTouchEvent 返回 super.onTouchEvent(ev) 表示對事件沒有做任何處理直接將事件返回給上級控制元件;
-
由於 TouchEventFather 獲取的事件直接來自 TouchEventActivity,所以 TouchEventFather 控制元件的 onTouchEvent 會將事件以冒泡方式直接返回給 TouchEventActivity 的 onTouchEvent 進行消費;
-
後續的事件則會跳過 TouchEventFather 直接由 TouchEventActivity 的 onTouchEvent 消費來自 TouchEventActivity 自身分發的事件。
5、情況四
case4
過程及結果分析:
-
事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分發給 TouchEventFather 控制元件的 dispatchTouchEvent;
-
而該控制元件的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),表示對事件進行分發並向下傳遞給 TouchEventFather 控制元件的 onInterceptTouchEvent 方法;
-
該方法返回 false 表示事件會被放行並傳遞到子控制元件 TouchEventChilds 的 dispatchTouchEvent 方法;
-
同樣 TouchEventChilds 的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),表示對事件進行分發並向下傳遞給 TouchEventChilds 控制元件的 onInterceptTouchEvent 方法;
-
而TouchEventChilds 的 onInterceptTouchEvent 方法返回 super.onInterceptTouchEvent(ev) ,預設會將事件傳遞給 TouchEventChilds 的 onTouchEvent 進行處理;
-
而TouchEventChilds 的 onTouchEvent 返回 super.onTouchEvent(ev) 表示對事件沒有做任何處理直接將事件返回給上級控制元件;
-
由於 TouchEventChilds 獲取的事件直接來自 TouchEventFather,所以 TouchEventChilds 控制元件的 onTouchEvent 會將事件以冒泡方式直接返回給 TouchEventFather 的 onTouchEvent 進行消費;
-
而 TouchEventFather 的 onTouchEvent 也返回了 super.onTouchEvent(ev),同樣 TouchEventFather 的 onTouchEvent 也會將事件返回給上級控制元件;
-
而 TouchEventFather 獲取的事件直接來自 TouchEventActivity,所以 TouchEventFather 控制元件的 onTouchEvent 會將事件以冒泡方式直接返回給 TouchEventActivity 的 onTouchEvent 進行消費;
-
後續的事件則會跳過 TouchEventFather 和 TouchEventChilds 直接由 TouchEventActivity 的 onTouchEvent 消費來自 TouchEventActivity 自身分發的事件。
6、情況五
case5
過程及結果分析:
-
事件首先由 TouchEventActivity 的 dispatchTouchEvent 方法分發給 TouchEventFather 控制元件的 dispatchTouchEvent;
-
該控制元件的 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev),事件會分發到 TouchEventFather 的 onInterceptTouchEvent,此方法返回 false 表示放行當先事件;
-
事件會被傳遞到子控制元件 TouchEventChilds 的 dispatchTouchEvent 方法,dispatchTouchEvent 返回 true 表示事件被分發到 TouchEventChilds ,並由 dispatchTouchEvent 方法消費;
-
後續的事件也會不斷的重複上面的邏輯最終被 TouchEventChilds 的 dispatchTouchEvent 消費。
四、總結與歸納
好了,看完理論與程式碼結果,我想應該已經夠直觀的說明事件分發機制了。雖然在實際開發過程還是會遇到各種各樣的問題,但是有了理論基礎,處理起來應該不會太難。接下來我就總結一下一些比較重要的注意事項和結論。有些是書上的重要結論。
如果在你不知道返回什麼的情況下,記住如果是完全自定義View就返回true,如果是繼承已有的控制元件或者View那就返回super;
正常情況下,一個事件序列只能被一個View攔截,這是肯定的,因為某個事件被攔截後,只能通過這個攔截View來處理。當然前提是正常情況下。
View沒有onInterceptTouchEvent方法,只要有觸控事件會直接呼叫onTouchEvent方法。
如果一個View設定了OnTouchListener方法,那麼會優先呼叫onTouch方法,這個時候還要看onTouch方法的返回值,如果為false那麼繼續呼叫onTouchEvent方法,如果為true則不呼叫。
如果onTouchEvent方法裡面設定了OnClickListener之類的方法,它會在onTouchEvent方法呼叫之後呼叫其中的onClick方法。
一個事件一旦交給了一個View進行處理,那麼它必須消費掉事件,否則剩下的事件序列將不再由其處理。