說起Android滑動衝突,是個很常見的場景,比如SliddingMenu與ListView的嵌套,要解決滑動衝突,不得不提及到View的事件分發機制。
一、Touch事件傳遞規則分析
首先,我們要知道Touch事件是包裝在MotionEvent對象中的,在手指與屏幕接觸過程中產生一系列事件,典型的事件有以下三種:
ACTION_DOWN:手指剛接觸屏幕的瞬間
ACTION_UP:手指剛離開屏幕的瞬間
ACTION_MOVE:手指在屏幕上滑動
那麼,Android中Touch事件是一個怎樣的傳遞過程呢?
事件分發:public boolean dispatchTouchEvent(MotionEvent ev)
Touch事件發生時Activity的dispatchTouchEvent(MotionEvent ev)方法會將事件傳遞給最外層View的dispatchTouchEvent(MotionEvent ev)方法,該方法對事件進行分發。分發邏輯如下:如果return true,事件會由當前View的dispatchTouchEvent方法進行消費,同時事件會停止向下傳遞;
如果return false,事件分發分爲兩種情況:
如果當前 View 獲取的事件直接來自 Activity,則會將事件返回給Activity的onTouchEvent進行消費;
如果當前 View 獲取的事件來自外層父控件,則會將事件返回給父View的onTouchEvent進行消費。
如果return super.dispatchTouchEvent(ev),事件會自動的分發給當前View的onInterceptTouchEvent方法。
事件攔截:public boolean onInterceptTouchEvent(MotionEvent ev)
上面已經提到,如果在dispatchTouchEvent返回super.dispatchTouchEvent(ev),那麼事件將會傳遞到onInterceptTouchEvent方法,該方法對事件進行攔截。攔截邏輯如下:如果return true,則表示攔截該事件,並將事件傳遞給當前View的onTouchEvent方法;
如果return false,則表示不攔截該事件,並將該事件交由子View的dispatchTouchEvent方法進行事件分發,重複上述過程;
如果return super.onInterceptTouchEvent(ev),默認表示攔截該事件,並將事件傳遞給當前View的onTouchEvent方法,和return true一樣。
事件響應:public boolean onTouchEvent(MotionEvent ev)
上面已經提到,在dispatchTouchEvent(事件分發)返回super.dispatchTouchEvent(ev)並且onInterceptTouchEvent(事件攔截返回true或super.onInterceptTouchEvent(ev)的情況下,那麼事件會傳遞到onTouchEvent方法,該方法對事件進行響應。響應邏輯如下:如果return true,則表示響應並消費該事件;
如果return fasle,則表示不響應事件,那麼該事件將會不斷向上層View的onTouchEvent方法傳遞,直到某個View的onTouchEvent方法返回true,如果到了最頂層View還是返回false,那麼認爲該事件不消耗,則在同一個事件系列中,當前View無法再次接收到事件,該事件會交由Activity的onTouchEvent進行處理;
如果return super.dispatchTouchEvent(ev),則表示不響應事件,結果與return false一樣。
這裏也順便說一下,如果一個View同時監聽了onTouch事件和onClick事件,則在onTouchEvent裏面應該返回false,否則點擊事件就無法監聽到。後面會提到這一點。
上述三個方法到底有什麼區別與聯繫呢?我們通過一段僞代碼來表示:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){ // 如果onInterceptTouchEvent返回true
consume = onTouchEvent(ev); // 則交由該View的onTouchEvent方法
} else {
consume = child. dispatchTouchEvent(ev); // 否則交由子View的dispatchTouchEvent事件進行分發
}
return consume; // 如果成功消費該事件,則返回true,然後停止傳遞,否則返回false
}
那麼,接下來就總結一下事件的傳遞的規則。(1)當一個點擊事件產生後,它的傳遞過程遵循的規則如下:Activity->Window->View。頂級View接收到事件之後,就會按相應規則去分發事件。如果一個View的onTouchEvent方法返回false,那麼將會交給父容器的onTouchEvent方法進行處理,逐級往上,如果所有的View都不處理該事件,則交由Activity的onTouchEvent進行處理,就跟工作中遇到了難題,逐級找領導解決一個道理,領導解決不了,再找上一級領導。
(2)正常情況下,一個事件序列只能被一個View攔截且消耗,某個View一旦進行事件攔截,那麼這一個事件序列都只能交由他處理,並且onInterceptTouchEvent也不會被再次調用,因此,正常情況下一個事件是不能交給兩個View來處理的,當然,特殊做法就是在View的onTouchEvent,處理完之後再返回false,強行交給其他View處理。
(3)如果某一個View開始處理事件,如果他不消耗ACTION_DOWN事件(也就是onTouchEvent返回false),則同一事件序列比如接下來進行ACTION_MOVE,則不會再交給該View處理,就像工作中做一件事情,你要嘛做完,要嘛你就不要做這件事了。
(4)在Android中,ViewGroup默認返回false,即不攔截任何事件。
(5)諸如TextView、ImageView這些不作爲容器的View,一旦接受到事件,就調用onTouchEvent方法,它們本身沒有onInterceptTouchEvent方法。正常情況下,它們都會消耗事件(返回true),除非它們是不可點擊的(clickable和longClickable都爲false),那麼就會交由父容器的onTouchEvent處理。
(6)View的enable屬性不影響onTouchEvent的默認返回值,只要它clickable或者longClickable爲true,則onTouchEvent就會返回true。
(7)點擊事件分發過程如下 dispatchTouchEvent—->OnTouchListener的onTouch方法—->onTouchEvent-->OnClickListener的onClick方法。也就是說,我們平時調用的setOnClickListener,優先級是最低的,所以,onTouchEvent或OnTouchListener的onTouch方法如果返回true,則不響應onClick方法...
二、滑動衝突處理過程分析
滑動衝突的場景常見於滑動嵌套,就是一個頁面中可能有兩個或兩個以上的View同時可以滑動,那麼就可能會導致只有其中的一個View能滑動。一個最簡單的屏幕觸摸動作觸發了一系列Touch事件:ACTION_DOWN->ACTION_MOVE->ACTION_MOVE—>...->ACTION_MOVE->ACTION_UP。
滑動衝突場景主要有三種:
(1)一個頁面中同時存在左右滑動和上下滑動。
讓外部的View攔截滑動事件,判斷滑動的特徵,如果水平滑動距離>豎直滑動距離,則爲水平滑動,反之爲豎直滑動。假設內部View可以水平滑動,外部View可以豎直滑動,那麼在外部View的onInterceptTouchEvent方法判斷,如果觸摸事件爲水平滑動,則應該放行,也就是返回false,然後交給內部View來處理,那麼內部子View就可以實現水平滑動。當然,還有一種方法就是外部View不攔截,交給內部View處理,如果內部View有需要就自己消耗掉,否則交給上一層,但是這樣違反了事件分發機制,所以需配合requestDisallowInterceptTouchEvent(MotionEvent ev)進行處理,這裏就不細說了,有興趣的童鞋可以研究一下。
(2)同時存在兩個豎直或水平滑動
這個主要還得根據具體的需求分析。最簡單的加入是兩個ScrollView嵌套,一般可以判斷ACTION_DOWN在那個View上,就執行那個View的滑動事件。
(3)就是(1)和(2)同時存在的情況
實際上也得看具體業務需求找到突破點,但是處理方式本質上來說都是差不多的,就是要根據滑動策略,來干擾事件分發機制。
附上一段僞代碼來理清一下思路:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) { // 外部View攔截事件
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x; // 分別記錄上次滑動座標
mLastYIntercept = y;
return intercepted; // 看是否需要傳遞給內部View處理
}
一、Touch事件傳遞規則分析
首先,我們要知道Touch事件是包裝在MotionEvent對象中的,在手指與屏幕接觸過程中產生一系列事件,典型的事件有以下三種:
ACTION_DOWN:手指剛接觸屏幕的瞬間
ACTION_UP:手指剛離開屏幕的瞬間
ACTION_MOVE:手指在屏幕上滑動
那麼,Android中Touch事件是一個怎樣的傳遞過程呢?
事件分發:public boolean dispatchTouchEvent(MotionEvent ev)
Touch事件發生時Activity的dispatchTouchEvent(MotionEvent ev)方法會將事件傳遞給最外層View的dispatchTouchEvent(MotionEvent ev)方法,該方法對事件進行分發。分發邏輯如下:如果return true,事件會由當前View的dispatchTouchEvent方法進行消費,同時事件會停止向下傳遞;
如果return false,事件分發分爲兩種情況:
如果當前 View 獲取的事件直接來自 Activity,則會將事件返回給Activity的onTouchEvent進行消費;
如果當前 View 獲取的事件來自外層父控件,則會將事件返回給父View的onTouchEvent進行消費。
如果return super.dispatchTouchEvent(ev),事件會自動的分發給當前View的onInterceptTouchEvent方法。
事件攔截:public boolean onInterceptTouchEvent(MotionEvent ev)
上面已經提到,如果在dispatchTouchEvent返回super.dispatchTouchEvent(ev),那麼事件將會傳遞到onInterceptTouchEvent方法,該方法對事件進行攔截。攔截邏輯如下:如果return true,則表示攔截該事件,並將事件傳遞給當前View的onTouchEvent方法;
如果return false,則表示不攔截該事件,並將該事件交由子View的dispatchTouchEvent方法進行事件分發,重複上述過程;
如果return super.onInterceptTouchEvent(ev),默認表示攔截該事件,並將事件傳遞給當前View的onTouchEvent方法,和return true一樣。
事件響應:public boolean onTouchEvent(MotionEvent ev)
上面已經提到,在dispatchTouchEvent(事件分發)返回super.dispatchTouchEvent(ev)並且onInterceptTouchEvent(事件攔截返回true或super.onInterceptTouchEvent(ev)的情況下,那麼事件會傳遞到onTouchEvent方法,該方法對事件進行響應。響應邏輯如下:如果return true,則表示響應並消費該事件;
如果return fasle,則表示不響應事件,那麼該事件將會不斷向上層View的onTouchEvent方法傳遞,直到某個View的onTouchEvent方法返回true,如果到了最頂層View還是返回false,那麼認爲該事件不消耗,則在同一個事件系列中,當前View無法再次接收到事件,該事件會交由Activity的onTouchEvent進行處理;
如果return super.dispatchTouchEvent(ev),則表示不響應事件,結果與return false一樣。
這裏也順便說一下,如果一個View同時監聽了onTouch事件和onClick事件,則在onTouchEvent裏面應該返回false,否則點擊事件就無法監聽到。後面會提到這一點。
上述三個方法到底有什麼區別與聯繫呢?我們通過一段僞代碼來表示:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){ // 如果onInterceptTouchEvent返回true
consume = onTouchEvent(ev); // 則交由該View的onTouchEvent方法
} else {
consume = child. dispatchTouchEvent(ev); // 否則交由子View的dispatchTouchEvent事件進行分發
}
return consume; // 如果成功消費該事件,則返回true,然後停止傳遞,否則返回false
}
那麼,接下來就總結一下事件的傳遞的規則。(1)當一個點擊事件產生後,它的傳遞過程遵循的規則如下:Activity->Window->View。頂級View接收到事件之後,就會按相應規則去分發事件。如果一個View的onTouchEvent方法返回false,那麼將會交給父容器的onTouchEvent方法進行處理,逐級往上,如果所有的View都不處理該事件,則交由Activity的onTouchEvent進行處理,就跟工作中遇到了難題,逐級找領導解決一個道理,領導解決不了,再找上一級領導。
(2)正常情況下,一個事件序列只能被一個View攔截且消耗,某個View一旦進行事件攔截,那麼這一個事件序列都只能交由他處理,並且onInterceptTouchEvent也不會被再次調用,因此,正常情況下一個事件是不能交給兩個View來處理的,當然,特殊做法就是在View的onTouchEvent,處理完之後再返回false,強行交給其他View處理。
(3)如果某一個View開始處理事件,如果他不消耗ACTION_DOWN事件(也就是onTouchEvent返回false),則同一事件序列比如接下來進行ACTION_MOVE,則不會再交給該View處理,就像工作中做一件事情,你要嘛做完,要嘛你就不要做這件事了。
(4)在Android中,ViewGroup默認返回false,即不攔截任何事件。
(5)諸如TextView、ImageView這些不作爲容器的View,一旦接受到事件,就調用onTouchEvent方法,它們本身沒有onInterceptTouchEvent方法。正常情況下,它們都會消耗事件(返回true),除非它們是不可點擊的(clickable和longClickable都爲false),那麼就會交由父容器的onTouchEvent處理。
(6)View的enable屬性不影響onTouchEvent的默認返回值,只要它clickable或者longClickable爲true,則onTouchEvent就會返回true。
(7)點擊事件分發過程如下 dispatchTouchEvent—->OnTouchListener的onTouch方法—->onTouchEvent-->OnClickListener的onClick方法。也就是說,我們平時調用的setOnClickListener,優先級是最低的,所以,onTouchEvent或OnTouchListener的onTouch方法如果返回true,則不響應onClick方法...
二、滑動衝突處理過程分析
滑動衝突的場景常見於滑動嵌套,就是一個頁面中可能有兩個或兩個以上的View同時可以滑動,那麼就可能會導致只有其中的一個View能滑動。一個最簡單的屏幕觸摸動作觸發了一系列Touch事件:ACTION_DOWN->ACTION_MOVE->ACTION_MOVE—>...->ACTION_MOVE->ACTION_UP。
滑動衝突場景主要有三種:
(1)一個頁面中同時存在左右滑動和上下滑動。
讓外部的View攔截滑動事件,判斷滑動的特徵,如果水平滑動距離>豎直滑動距離,則爲水平滑動,反之爲豎直滑動。假設內部View可以水平滑動,外部View可以豎直滑動,那麼在外部View的onInterceptTouchEvent方法判斷,如果觸摸事件爲水平滑動,則應該放行,也就是返回false,然後交給內部View來處理,那麼內部子View就可以實現水平滑動。當然,還有一種方法就是外部View不攔截,交給內部View處理,如果內部View有需要就自己消耗掉,否則交給上一層,但是這樣違反了事件分發機制,所以需配合requestDisallowInterceptTouchEvent(MotionEvent ev)進行處理,這裏就不細說了,有興趣的童鞋可以研究一下。
(2)同時存在兩個豎直或水平滑動
這個主要還得根據具體的需求分析。最簡單的加入是兩個ScrollView嵌套,一般可以判斷ACTION_DOWN在那個View上,就執行那個View的滑動事件。
(3)就是(1)和(2)同時存在的情況
實際上也得看具體業務需求找到突破點,但是處理方式本質上來說都是差不多的,就是要根據滑動策略,來干擾事件分發機制。
附上一段僞代碼來理清一下思路:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) { // 外部View攔截事件
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x; // 分別記錄上次滑動座標
mLastYIntercept = y;
return intercepted; // 看是否需要傳遞給內部View處理
}