我們在開發過程中,難免會用到ScrollView嵌套ViewPager的情況,比如淘寶商品詳情頁面。但當我們用普通的ScrollView嵌套ViewPager是,會出現滑動衝突的情況,原因很簡單:當我們左右滑動ViewPager時,我們的手指會有一點上下滑動的浮動,而ScrollView監聽了上下滑動事件,這就造成滑動衝突。
解決辦法也很容易,我們只需要重寫ScrollView,在ScrollView判斷手指滑動的y方向的距離,如果滑動距離的y方向距離大於x方向距離,則說明用戶是進行上下滑動ScrollView操作,響應ScrollView滑動事件;否則傳遞給子控件處理。
直接看代碼:
<span style="font-size:14px;">public class ViewPagerScroller extends ScrollView {
// 滑動距離及座標
private float xDistance, yDistance, xLast, yLast;
public ViewPagerScroller(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public ViewPagerScroller(Context context) {
super(context);
}
public ViewPagerScroller(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
xDistance = yDistance = 0f;
xLast = ev.getX();
yLast = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
final float curX = ev.getX();
final float curY = ev.getY();
xDistance += Math.abs(curX - xLast);
yDistance += Math.abs(curY - yLast);
xLast = curX;
yLast = curY;
if(xDistance > yDistance){
return false;
}
}
return super.onInterceptTouchEvent(ev);
}
}</span>
在重寫ScrollView時,我們主要重寫了onInterceptTouchEvent()方法,有些人對該方法不是很瞭解,下面對該方法簡要說明。
onInterceptTouchEvent():用於處理事件(類似於預處理,當然也可以不處理)並改變事件的傳遞方向,也就是決定是否允許Touch事件繼續向下(子控件)傳遞,一但返回True(代表事件在當前的viewGroup中會被處理),則向下傳遞之路被截斷(所有子控件將沒有機會參與Touch事件),同時把事件傳遞給當前的控件的onTouchEvent()處理;返回false,則把事件交給子控件的onInterceptTouchEvent()
瞭解瞭如何解決衝突後,我們要深入一下爲什麼這樣就解決衝突了,ScrollView源碼有沒有做過響應的處理。我們大致看一下ScrollView監聽事件得源碼:
<span style="font-size:14px;">public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
if (getScrollY() == 0 && !canScrollVertically(1)) {
return false;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
// If we don't have a valid id, the touch down wasn't on content.
break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId
+ " in onInterceptTouchEvent");
break;
}
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop) { // <<=================注意看這裏
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
final int y = (int) ev.getY();
if (!inChild((int) ev.getX(), (int) y)) {
mIsBeingDragged = false;
recycleVelocityTracker();
break;
}
mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
mIsBeingDragged = !mScroller.isFinished();
if (mIsBeingDragged && mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
postInvalidateOnAnimation();
}
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
return mIsBeingDragged;
}</span>
由於ScrollView是ViewPager的父容器,由他來決定事件是自己處理還是傳遞給ViewPager來處理,當onInterceptTouchEvent()返回true是,說明時間有自己攔截並處理,當返回false說明事件傳遞給子容器(ViewPager)來處理。
我們看一下源碼標記的地方,ScrollView在監聽ACTION_MOVE事件時,爲y方向滑動距離做了一次比較,如果滑動的距離大於一個值(mTouchSlop),則說明是垂直滑動,返回true而響應事件,如果不大於,就交給子View響應。看完源碼後可能會有這樣疑問,ScrollView已經做了判斷,也就是說做了滑動衝突的處理了,那麼問題來了,滑動ViewPager時可爲什麼還會有衝突的存在呢?
其實細心的朋友會注意到y是和mTouchSlop進行大小比較的,二我們重寫是使用y和x的活動距離進行比較的。說道這裏我們很容易就明白了,原來都是mTouchSlop的過,可他是什麼值呢?我們在源碼中可以看到:mTouchSlop
= configuration.getScaledTouchSlop();
原來他是系統的一個常量,可他是多少呢?其實他就是系統所能識別出來的被認爲是滑動的最小距離,他是一個常量,不同的設備,他的值可能不同。
很多人看到這裏就明白了爲什麼源碼不能解決我們的衝突了,源碼用Y方向滑動的距離與mTouchSlop 作比較,只要達到系統認爲能夠滑動的最小距離,就攔截該事件,所以我們滑動ViewPager時,如果手指平行滑動,就可以滑動ViewPager,可如果稍微有點傾斜,就會差生衝突,因爲此時,時而ViewPager處理事件,時而ScrollView處理事件,所以我們就會看到兩個View都在微微的滑動。
我們解決衝突時就是讓橫向和縱向來比較大小,這樣比較區分度很明顯,ScrollView也就知道什麼時候事件該他處理,什麼時候事件該交給子VIew處理。
這裏只是分享了衝突的一個例子,後面會詳細講解View的時間分發機制,以便於深入理解事件的分發和衝突的解決!!