滑動衝突問題的簡單解決思路
其實之所以《從原始碼角度分析android事件分發處理機制》這篇部落格,是因為在此之前一個android群友遇到一個滑動衝突問題,然後幫助其解決過後才想起來要仔細分析研究,並完成了文章開頭索索的那篇部落格。。
該群友的應用問題場景是:一個FrameLayout,裡面巢狀一個ListView.通過手指左右的滑動來顯示和關閉FrameLayout。他滑動開啟/關閉FrameLayout的效果是實現了,但是點選ListView的某一個item的時候,onItemClick事件始終不會執行。
該群友當時的處理方法:重寫FrameLayout的onTouchEvent,使之返回true:
public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: .... break; case MotionEvent.ACTION_MOVE: if (向左滑動) { //關閉FrameLayout closeDraw(); } else if (向右滑動) { //開啟FrameLayout openDraw(); } break; case MotionEvent.ACTION_UP: break; } return true; } public boolean onInterceptTouchEvent(MotionEvent event) { return true; }
很明顯這樣做的錯誤很明顯,讓FrameLayout直接返回true(在解決問題之前,這個是我的失誤,我當初說你把事件攔截了,onInterceptTouchEvent返回true試試,剛開始貌似是解決關閉/開啟FrameLayout的問題),這樣的話事件都被FrameLayout這個父View來攔截處理了,這樣ListView這個childView是肯定獲取不到這個事件了,所以更無從談起處理onItemClick了。所以解決問題的核心思路很簡單:處理左右滑動的時候讓父類攔截和處理事件,當處理點選事件的時候讓ListView獲取事件即可。簡而言之就是父類FrameLayout什麼時候攔截什麼時候不攔截事件的邏輯。
在《
1)事件:從手指觸控手機螢幕開始到擡起會發起產生一系列列事件,每個事件單獨去分析看待,它的傳遞順序仍然是由parentView 通過呼叫dispatchTouchEvent進行分發給childView的順序進行。
2)parentView在處理down事件的時候會遍歷它的childrenView來尋找能處理事件的那個childView,即targetView.
3)找到targetView之後,down事件之後的事件序列都交給targetView進行處理。
接著上面的問題繼續說明,既然每個事件都會由parentView分發給childView這樣的順序執行,這就說明parentView有優先選擇是否攔截和處理事件的權利,所以按照上面的說明,解決該群友的問題我用到了如下方法:重寫FrameLayout的onInterceptTouchEvent,來處理什麼時候需要攔截什麼時候不需要攔截:
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean result = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//DOWN事件千萬不能攔截
result = false;
break;
case MotionEvent.ACTION_MOVE:
if (向左滑動手指||向右滑動手指) {
result = true;
}else{
result = false;
}
break;
case MotionEvent.ACTION_UP:
result = false;
break;
}
return result;
}
}
上面讓parentView決定是否攔截事件的思路也很簡單:當左右滑動手指的時候parentView對事件進行攔截和處理,否則就不攔截,將事件分發給childView,讓childView決定是否處理該事件。就需要注意的是在處理ACTION_DOWN的時候,不能攔截,因為DOWN事件是一系列事件的開始事件,如果攔截的ParentView的down事件,那麼後續move事件,up事件都會交給parentView處理。
上面的思路說白了就是:所有的事件都需要parentViewView進行攔截處理,由parentView決定是自己是否需要攔截該事件,不需要的話就分發給childrenView.這個思路是讓父類優先選擇對事件的攔截與否,其實通過《從原始碼角度分析android事件分發處理機制》對原始碼的分析發現有兩處程式碼可以提供我們解決問題的另一種思路:
在ViewGroup進行事件分發的時候(處理ACTION_DOWN的時候)會進行如下判斷:
if(disallowIntercept || !onInterceptTouchEvent(ev))這句很簡單,但是代表的內容卻很豐富,它說明了兩種情況可以進入if條件的程式碼裡面讓ViewGroup的childrenView來分發處理事件:
1)當disallowIntercept==true的時候,即不允許當前的ViewGroup對該事件進行攔截
2)允許當前ViewGroup對事件進行攔截,也就是disallowIntercept==false,但是ViewGroup並未ACTION_DOWN事件攔截成功,也就是ViewGroup的onInterceptTouchEvent 返回了false。
至於disallowIntercept這個屬性怎麼設定呢,查閱ViewGroup的原始碼可以發下下面一個方法requestDisallowInterceptTouchEvent,該方法是public的也就是說childView也可以呼叫這個方法如:childView.getParent().requestDisallowInterceptTouchEvent(true or false)來干擾parentView對事件的分發。
前面說過當parentView尋找到到target的時候,後續事件一直都會讓target來處理,但後續事件還會進入parentView的dispatchTouchEvent方法裡面去執行,也就是說說通過requestDisallowInterceptTouchEvent可以讓parentView繼續對後續事件進行攔截和處理,從原始碼上也可以說明:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//處理down事件,主要目的是尋找target
if (action == MotionEvent.ACTION_DOWN) {
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
//找到了target
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
}
}
}
}
}
}
//如果允許對後續事件進行攔截,並且攔截成功的話
//通過這段程式碼可以知道,在childView中可以呼叫
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
mMotionTarget = null;
return true;
}
//後續事件:ACTION_MOVE系列事件和up事件都交給target處理
return target.dispatchTouchEvent(ev);
}
根據if(!disallowIntercept&&onInterceptTouchEvent(ev))我們知道,可以再target這個childView中在合適的時機或者符合某個業務邏輯的情況下,執行如下呼叫:
childView.getParent().requestDisallowInterceptTouchEvent(false):允許父類對後續事件進行攔截,並且在某個合適的時機或者符合某個業務邏輯的時候
讓parentView的onInterceptEvent方法返回true,這樣後續的ACTION_MOVE事件就可以又交給parentView來進行處理了,通過這種childView干預parentView對時間進行攔截的方法也是解決滑動事件衝突的有一種思路,相比直接讓第一種方式,這種方式較為麻煩點。