天天看点

读《Android开发艺术探索》---View滑动冲突

//今天来学习一下View的滑动冲突,主要从滑动冲突的产生原因和滑动冲突的解决方案出发。

View滑动冲突产生的三个冲突场景。

场景1、外部View左右滑动,内部View上下滑动

场景2、外部View上下滑动,内部View也上下滑动

场景3、场景1和场景2之间的混合滑动。

如图从左向右场景1-3

读《Android开发艺术探索》---View滑动冲突
读《Android开发艺术探索》---View滑动冲突
读《Android开发艺术探索》---View滑动冲突

场景1和场景2都是比较常见的页面布局。因此解决这种滑动冲突十分必要

据我现在的了解,场景1现在可以使用ViewPager+Fragment来避免这种滑动的冲突。

场景2这种布局,我现在在写的项目中使用了NestedScrollView+RecyclerView这种布局方式,

也没有遇到这种冲突的情况。

所以,我想说的是,如果可以避免这些冲突那就尽量的去绕开这些冲突;如果避免不了滑动冲突,

那么我们可以借鉴以下的解决滑动冲突的思路,来解决我们的问题。

-----------------------------------------------------------------场景1---------------------------------------------------

首先来构造一个场景1的滑动冲突,并给出解决思路(这些例子来自于Android开发艺术探索)。

1 场景1的滑动冲突处理规则

    场景1是一个简单的滑动冲突,外部View是左右滑动,内部View上下滑动。

    当用户上下滑动时,我们希望内部View来处理事件;当用户左右滑动时,我们希望外部View来处理事件。

    因此,我们可以通过判断用户的手势滑动方向,来决定是哪个View来处理事件。

1.1外部拦截法 

1.事件拦截与处理

/**
     * 事件拦截
     *
     * 通过手势的滑动,判断是上下滑动还是左右滑动。
     * 如果是左右滑动,则父View进行事件的处理。
     * 反之子View进行处理。
     * @param ev
     * @return
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                    intercept = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if(Math.abs(deltaX) > deltaY){ //左右滑动,父容器拦截事件
                    intercept = true;
                }else{
                    intercept = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
            default:break;
        }
        Log.i(TAG,"intercept:"+intercept);
        mLastYIntercept = y;
        mLastXIntercept = x;

        Log.i(TAG,"x="+x+" y="+y+" mLastXIntercept="+mLastXIntercept+" mLastYIntercept="+mLastYIntercept);
        return intercept;
    }

    /**
     * 事件处理
     *
     * * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                if(!mScroller.isFinished())
                    mScroller.abortAnimation();
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                scrollBy(-deltaX,0);
                break;
            case MotionEvent.ACTION_UP:
                int scrollX = getScrollX();
                mVelocityTracker.computeCurrentVelocity(1000);
                //计算滑动速率
                float xVelocity = mVelocityTracker.getXVelocity();
                if(Math.abs(xVelocity) >=50){//滑动速率大于50.
                    mChildIndex = xVelocity > 0 ? mChildIndex-1 : mChildIndex+1;
                }else{
                    mChildIndex = (scrollX + mChildWidth /2) / mChildWidth;
                }
                mChildIndex = Math.max(0,Math.min(mChildIndex,mChildSize - 1));
                int dx = mChildIndex * mChildWidth - scrollX;
                smoothScrollBy(dx,0);
                mVelocityTracker.clear();
                break;
            default:break;
        }
            mLastY = y;
            mLastX = x;
        return true;
    }
           

1.2 内部拦截法:

    重写子View的方法,并重写dispatchTouchEvent()方法,使用requestdisallowInterceptTouchEvent()方法通知父类是否拦截点击事件。

具体代码实现如下:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG," ----------------------mLastX="+mLastY+"---------------mLastY="+mLastY);
                //告诉父View,不直接调用父View的onInterceptTouchEvent()方法,直接将方法分发给子View
                horizontalScrollViewEX2.requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                Log.i(TAG," ----------------------mLastX="+mLastY+"---------------mLastY="+mLastY);
                if(Math.abs(deltaX) > Math.abs(deltaY)){  //左右滑动的话,就转发给父View来处理
                    horizontalScrollViewEX2.requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG," ----------------------MotionEvent...ACTION_UP");
                break;
            default:break;
        }
        mLastX = x;
        mLastY = y;
        Log.i(TAG," mLastX="+mLastY+"mLastY="+mLastY);
        return super.dispatchTouchEvent(ev);
    }
           

这里遇到过一个坑,就是requestDisallowInterceptTouchEvent(ture)失效的问题。查阅资料说,传入true时,父View不会调用onInterceptTouchEvent()方法去拦截事件。但是

我在子View中的ACTION_DOWN中设置了requestDisallowInterceptTouchEvent(ture),父View还是调用了onInterceptTouchEvent()方法,导致事件没有分发给子View。

我的解决方案是,在子View创建完成之后,再去创建父View对象。这样使得子View的dispatchTouchEvent()中的逻辑判断在父View之前执行。

该Demo的源码在我的github