ScrollView嵌套ViewPager滑動衝突的解決

我們在開發過程中,難免會用到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的時間分發機制,以便於深入理解事件的分發和衝突的解決!!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章