Android 滑動衝突的解決方法
一、常見的滑動衝突場景
場景1——外部滑動方向和內部滑動方向不一致,如:ViewPager中有多個fragment,而fragment中有ListView,這時ViewPager可以左右滑動,而ListView可以上下滑動,這就造成了滑動衝突。注意:這只是舉個例子說明一下場景1,事實上ViewPager內部已經處理了這種滑動衝突,在採用ViewPager時,我們無需關注這個問題。
場景2——外部滑動方向和內部滑動方向一致。
場景3——上述兩種場景的巢狀,即共有三層,外層與中層的滑動方向一致,而中層與內層的滑動方向不一致。
二、滑動衝突的處理規則
場景1的處理規則: 1、當用戶左右滑動時,讓外部的View攔截點選事件,當用戶上下滑動時,讓內部的View攔截點選事件; 2、判斷使用者的滑動方向(左右、上下):如果使用者手指滑動的水平距離大於垂直距離,則左右滑動,反之,上下滑動;還可以根據角度、速度差來做判斷;
場景2的處理規則: 無法根據滑動的角度、距離差、速度差來判斷,因為場景2內部、外部的滑動方向一致;這時候一般都能在業務上找到突破點,如業務上規定:當處於某種狀態需要外部View響應使用者的滑動,而處於另一種狀態時則需要內部View響應使用者的滑動,所以我們可以根據業務的需求得出相應的處理規則。
場景3的處理規則:場景1的處理規則和場景2的處理規則一起用。
三、滑動衝突的解決方法
以下兩種解決方法都是通用的方法:1、外部攔截法 我們都知道點選事件是先經過父容器的攔截處理的,所以我們就可以在父容器的攔截方法中寫下自己的邏輯程式碼即可,相當於我們要重寫一個佈局Layout,使其繼承ViewGroup或其它的佈局(LinearLayout等),然後重寫其onInterceptTouchEvent方法。外部攔截法的虛擬碼如下:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; int x = (int)ev.getX(); int y = (int)ev.getY(); switch (ev.getAction()){ //父容器不需要攔截 case MotionEvent.ACTION_DOWN: intercepted = false; break; //在此判斷父容器是否需要攔截 case MotionEvent.ACTION_MOVE: if (父容器需要當前點選事件){ intercepted = true; } //父容器不需要當前的點選事件 else { intercepted = false; } break; //父容器不需要攔截 case MotionEvent.ACTION_UP: intercepted = false; break; default: break; } //重置手指的起始位置 mLastXIntercept = x; mLastYIntercept = y; return intercepted; }
解釋上述程式碼: 1、父容器不攔截ACTION_DOWN事件:那是因為一旦父容器攔截了ACTION_DOWN事件,後續的ACTION_MOVE、ACTION_UP事件都會直接交給父容器處理,這個時候事件無法傳遞給子元素了。
2、父容器不攔截ACTION_UP事件:首先我們要知道onClick 事件是在ACTION_UP事件之後執行的,那當子元素有一個onClick事件,而這時候父容器攔截了ACTION_UP事件,那子元素的onClick事件就無法執行了。
3、ACTION_MOVE事件:在這裡可以根據我們的需求,來判斷父容器是否需要攔截事件,需要則返回true,否則返回false。
總結,外部攔截法比較簡單,實現起來較容易。
2、內部攔截法 首先我們瞭解下ViewGroup.requestDisallowInterceptTouchEvent(boolean)方法,此方法就是在子View中通知父容器攔截或不攔截點選事件,false ——攔截,true ——不攔截;
內部攔截法的思想是父容器先不攔截任何事件,即所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交由父容器進行處理。這時我們就用到requestDisallowInterceptTouchEvent 方法了。
具體做法:我們重寫子View的dispatchTouchEvent方法,即我們新建一個類繼承子View,然後重寫它的dispatchTouchEvent方法,虛擬碼如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//獲得當前的位置座標
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
//通知父容器不要攔截事件
horizontalScrollLayout.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要此事件){
//通知父容器攔截此事件
horizontalScrollLayout.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
//重置手指的初始位置
mLastY = y;
mLastX = x;
return super.dispatchTouchEvent(ev);
}
- 程式碼: 1、horizontalScrollLayout是此子View的父容器的物件,我們可以在子 View中定義一個方法:
public void setHorizontalScrollLayout(HorizontalScrollLayout horizontalScrollLayout) {
this.horizontalScrollLayout = horizontalScrollLayout;
}
此時,父容器攔截了除ACTION_DOWN以外的其它事件,那為什麼這樣做? 原因:只有這樣,才能使的當子元素呼叫了requestDisallowInterceptTouchEvent(false)方法後,父容器才能繼續攔截所需的事件。
3、那父容器為什麼不能攔截ACTION_DOWN事件呢? 原因:因為ACTION_DOWN事件不受FLAG_DISALLOW_INTERCEPT這個標記位的控制,所以一旦父容器攔截了ACTION_DOWN事件,那麼所有的事件都無法傳遞給子元素了,這樣內部攔截就起不到作用了。
總結,內部攔截法比較複雜,在實際應用中,建議還是用外部攔截法。
四、舉例說明
在舉例的時候,我自定義了幾個view,大家主要看其中的dispatchTouchEvent方法和onInterceptTouchEvent方法,至於自定View的方法會在之後的部落格中詳細描述。
場景1的解決方案:外部攔截和內部攔截的方法都有 。程式碼下載請點我
--------------------- 作者:struggle323 來源:CSDN 原文:https://blog.csdn.net/struggle323/article/details/50788136