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的时间分发机制,以便于深入理解事件的分发和冲突的解决!!


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