用ViewDragHelper自定义侧滑菜单——浅析源码解决与ScrollView的滑动冲突

上篇博文带大家分析学习了关于ViewDragHelper的简单基本用法,本篇将带大家用ViewDragHelper做一个简单侧滑菜单的Demo,并通过对ViewDragHelper源码的简单分析,找到其与ScrollView滑动事件冲突的原因与解决方法。闲话少说,直入正题,首先来看一下运行效果。

用ViewDragHelper自定义侧滑菜单——浅析源码解决与ScrollView的滑动冲突_第1张图片

首先,我们先看一下布局文件,以便整体了解界面布局结构:




    

        

        

            

                
通过看布局,我们可以发现com.example.caifengyao.drawerlayout.DragViewGroup就是我们的自定义侧滑菜单布局,它实际上是继承自RelativeLayout,它里面又放置三个内容,第一个就是标题了,第二个ScrollView就是页面的Content,第三个ScrollView就是我们的侧滑内容,并且它的marginLeft属性设置为了-300dp,使其可以偏离出屏幕左面。

下面就是我们的DragViewGroup代码了,我们来看下代码实现:

public class DragViewGroup extends RelativeLayout {
    private ViewDragHelper mViewDragHelper;
    private View mMainView;
    private int mWidth;

    public DragViewGroup(Context context) {
        super(context);
        initView();
    }

    public DragViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }


    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMainView = getChildAt(2);
    }

    private void initView() {
        mViewDragHelper = ViewDragHelper.create(this, callback);
        mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = mMainView.getMeasuredWidth();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        mViewDragHelper.processTouchEvent(ev);
        return true;
    }


    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return mMainView == child;
        }


        @Override
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
            super.onEdgeDragStarted(edgeFlags, pointerId);
            mViewDragHelper.captureChildView(mMainView, pointerId);
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return 0;
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return Math.min(left, 0);//使展开的最大程度不超过菜单的整体宽度
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            if (mMainView.getRight() < mWidth / 2) {//当拖动距离小于侧划菜单宽度的一半时,菜单收回
                mViewDragHelper.smoothSlideViewTo(mMainView, -mWidth, 0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            } else {//当拖动距离大于等于侧划菜单宽度的一半时,菜单完全展开
                mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }
        }
    };

    @Override
    public void computeScroll() {
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }
}
可以看到,代码与上篇博文的基本没什么差别,各方法作用在上篇博文也已经做出来讲解,这里就不在赘述,不清楚的朋友可以查看我的上篇博文: ViewDragHelper用法介绍,快捷的拖动效果设计。

这里,主要说两个地方,首先我在clampViewPositionHorizontal方法中返回的是Math.min(left, 0),而不再是left,原因是我不希望侧滑菜单被过分的拖动。因为他是隐藏在左屏幕外,所以他的X轴坐标是负的,在拖动过程中,left一直是负数且逐渐增大,当left为0的时候,其完全展开,菜单左边界与屏幕左边界相交,这时我如果返回left,就会导致菜单可以继续被拖动,从而出现菜单左边界与屏幕左边界分离的情况,这显然不是我们要实现的效果,因此,我才会在left与0之间去一个最小值最为返回值。

其次就是onViewReleased方法,代码注释应该比较清楚,这么做的原因呢,也是为了体验更好一点而已。

好了,下面我们看下运行效果如何

用ViewDragHelper自定义侧滑菜单——浅析源码解决与ScrollView的滑动冲突_第2张图片

大家仔细看,首先,content中的ScrollView可以正常的上下滑动;其次侧滑菜单也是可以正常的拖拽出来,并且菜单内部的ScrollView也是可以正常的上下滑动。但是,认真观察,当菜单滑动出来之后,我想收起菜单时,发现向左拖动时,居然没有了反应,这是怎么回事?因为ViewDragHelper与ScrollView发生了事件冲突。下面我们来分析解决这个问题。

1、先分析侧滑菜单拖出来时,事件的逻辑

首先,我们来看下ViewDragHelper关于事件拦截方法shouldInterceptTouchEvent(ev)的源码:

 public boolean shouldInterceptTouchEvent(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);
        final int actionIndex = MotionEventCompat.getActionIndex(ev);

//省略关于记录触摸点等操作的代码
......

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                final float x = ev.getX();
                final float y = ev.getY();
                final int pointerId = ev.getPointerId(0);
                saveInitialMotion(x, y, pointerId); //步骤1

                final View toCapture = findTopChildUnder((int) x, (int) y); //步骤2

                // Catch a settling view if possible.
                if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { //步骤3
                    tryCaptureViewForDrag(toCapture, pointerId);
                }

                final int edgesTouched = mInitialEdgesTouched[pointerId];

                if ((edgesTouched & mTrackingEdges) != 0) { //步骤4
                    mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
                }
                break;
            }

           
            //省略除ACTION_DOWN外的其他代码
           ......

            
        }
        
        return mDragState == STATE_DRAGGING;//步骤5
    }

我们先看ViewDragHelper事件拦截中关于ACTION_DOWN部分的源码。当我们按下手指的时候,我们的DragViewGroup获得事件,并将拦截逻辑交由ViewDragHelper处理。其中有5个比较关键的步骤。首先是步骤1,存储一些触摸点与指针的初始化信息。然后是步骤2,根据触摸点找到最顶部的子View。步骤3,判断是否尝试进行捕获拖动的View,因为ACTION_DOWN,mCapturedView==null,所以判断不成立。步骤4,因为我们设置了边界拖动,所以判断成立,走回调。到此ACTION_DOWN执行完毕,break出分支,到达步骤5,因为mDragState的初始状态为STATE_IDLE,在ACTION_DOWN中没有执行关于给mDragState改变赋值的操作,因此返回值为false,这也就意味着不进行事件拦截,事件向下层组件传递。

这时,事件传递到了ScrollView,通过对ScrollView代码的检测,我们发现其对ACTION_DOWN进行了消费。有兴趣的朋友可以查看下ScrollView的源码,这里只研究ViewDragHelper,就不在多说,我们只需知道结果就好。

那么,这就用到事件分发机制中知识了,还记得我再Android的事件分发(dispatchTouchEvent),拦截(onInterceptTouchEvent)与处理(onTouchEvent)中介绍过的。当事件传递到最底层时,消费事件将被触发,并且一层一层向上移交,直到有对事件进行消费为止,之后的事件,也将从上向下分发,当分发到消费了前一个事件的View或ViewGroup时,直接交由它消费接下来的事件。因为ScrollView消费了事件,所以,事件不会再向上层移交,所以我们的DragViewGroup的onTouchEvent接收不到消费请求,也就无法将消费的具体方式交由ViewDragHelper处理。

接下来就是拖动时候的事件逻辑,也就是ACTION_MOVE分支,还是ViewDragHelper源码:

            case MotionEvent.ACTION_MOVE: {
                if (mInitialMotionX == null || mInitialMotionY == null) break;

                // First to cross a touch slop over a draggable view wins. Also report edge drags.
                final int pointerCount = ev.getPointerCount();
                for (int i = 0; i < pointerCount; i++) {
                    final int pointerId = ev.getPointerId(i);

                    // If pointer is invalid then skip the ACTION_MOVE.
                    if (!isValidPointerForActionMove(pointerId)) continue;

                    final float x = ev.getX(i);
                    final float y = ev.getY(i);
                    final float dx = x - mInitialMotionX[pointerId];
                    final float dy = y - mInitialMotionY[pointerId];

                    final View toCapture = findTopChildUnder((int) x, (int) y);
                    
                    //步骤1
                    final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
                    if (pastSlop) {
                        // check the callback's
                        // getView[Horizontal|Vertical]DragRange methods to know
                        // if you can move at all along an axis, then see if it
                        // would clamp to the same value. If you can't move at
                        // all in every dimension with a nonzero range, bail.
                        final int oldLeft = toCapture.getLeft();
                        final int targetLeft = oldLeft + (int) dx;
                        final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
                                targetLeft, (int) dx);
                        final int oldTop = toCapture.getTop();
                        final int targetTop = oldTop + (int) dy;
                        final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
                                (int) dy);
                        final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
                                toCapture);
                        final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
                        if ((horizontalDragRange == 0 || horizontalDragRange > 0
                                && newLeft == oldLeft) && (verticalDragRange == 0
                                || verticalDragRange > 0 && newTop == oldTop)) {
                            break;
                        }
                    }
                    //步骤2
                    reportNewEdgeDrags(dx, dy, pointerId);
                    //步骤3
                    if (mDragState == STATE_DRAGGING) {
                        // Callback might have started an edge drag
                        break;
                    }
                    
                    if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
                        break;
                    }
                }
                saveLastMotion(ev);
                break;
            }
当我们出发ACTION_MOVE的时候,这是也是一个持续触发的过程,因为我们拖动是一个持续的状态。这个时候,关键代码有3处地方。步骤1我们会获得一个boolean值pastSlop,我们知道toCapture肯定不为null,所以前面是成立的,那么后面的checkTouchSlop又是什么呢,我们看看:

    private boolean checkTouchSlop(View child, float dx, float dy) {
        if (child == null) {//步骤1
            return false;
        }
        final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;//步骤2
        final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0; //步骤3
//步骤4
        if (checkHorizontal && checkVertical) {
            return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
        } else if (checkHorizontal) {
            return Math.abs(dx) > mTouchSlop;
        } else if (checkVertical) {
            return Math.abs(dy) > mTouchSlop;
        }
        return false;
    }
这就是checkTouchSlop的源码,因为我们知道toCapture肯定不为null,所以传到这里的child就一定不为null,所以步骤1是不成立的,略过。之后执行到步骤2、3,这两个方法是不是很熟悉,我们在上篇博文介绍过他们,用于解决消费了事件的View对被拖动View的事件冲突,是被拖动View可以成功实现拖动,其实我们解决现在的问题,最终也会落实到这个方法上面。

我们继续分析,因为步骤2、3的方法,默认返回的是0.所以两个都不成立,都是false,这就导致我们步骤4的三个判断分支都不成立,这个方法之行结束,最终返回值是false。

好,我们再回到ViewDragHelper的ACTION_MOVE中的步骤1,因为前面判断成立,后面的checkTouchSlop返回值是false,所以&&运算结果是false,这也就使pastSlop的值最终为false。所以步骤1下面的一大段代码直接不执行,略过。

此时执行到步骤2的reportNewEdgeDrags(dx, dy, pointerId)方法,这个又是干什么用的呢?

    private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
        int dragsStarted = 0;
        //判断4个方向的拖动
        if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
            dragsStarted |= EDGE_LEFT;
        }
        if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
            dragsStarted |= EDGE_TOP;
        }
        if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) {
            dragsStarted |= EDGE_RIGHT;
        }
        if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {
            dragsStarted |= EDGE_BOTTOM;
        }
    
        if (dragsStarted != 0) {
            mEdgeDragsInProgress[pointerId] |= dragsStarted;
            mCallback.onEdgeDragStarted(dragsStarted, pointerId);//最关键
        }
    }
英语好的朋友通过方法名就能看出大概,这个方法就是用于获得最新的边界拖动值的。我说过ACTION_MOVE是一个持续的过程,所以ACTION_MOVE就会使一个持续执行的过程,那么这个方法也会持续的被调用。当我们在某一个方向的边界拖动值满足上面四个判断中的某一个,就会使dragsStarted不为0,那么最下面的判断将成立,这时,最关键的那段回调代码mCallback.onEdgeDragStarted(dragsStarted, pointerId)将被执行。那这个代码的实现在哪里呢?

        @Override
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
            super.onEdgeDragStarted(edgeFlags, pointerId);
            mViewDragHelper.captureChildView(mMainView, pointerId);//关键代码
        }
看,这不就是在我们自定义的DragViewGroup中,Callback中的回调么。那么我们自己写的实现mViewDragHelper.captureChildView(mMainView, pointerId)就发挥了作用,我们去看看,它里面都干了些什么。

    public void captureChildView(View childView, int activePointerId) {
        if (childView.getParent() != mParentView) {
            throw new IllegalArgumentException("captureChildView: parameter must be a descendant "
                    + "of the ViewDragHelper's tracked parent view (" + mParentView + ")");
        }

        mCapturedView = childView;
        mActivePointerId = activePointerId;
        mCallback.onViewCaptured(childView, activePointerId);
        setDragState(STATE_DRAGGING);//关键
    }
这是captureChildView的源码。其实最主要的就是赋值,将childView赋值给mCapturedView,activePointerId赋值给mActivePointerId,触发Callback的另外一个回调。最关键的就是setDragState这个设置。

    void setDragState(int state) {
        mParentView.removeCallbacks(mSetIdleRunnable);
        if (mDragState != state) {
            mDragState = state;
            mCallback.onViewDragStateChanged(state);
            if (mDragState == STATE_IDLE) {
                mCapturedView = null;//拖动结束,mCapturedView制空
            }
        }
    }
其实,setDragState源码中,最关键的逻辑点就是mDragState = state,它把mDragState的状态有初始值的STATE_IDLE改变为STATE_DRAGGING。到这里,ACTION_MVOE中的步骤2,reportNewEdgeDrags方法就执行完成了,跳出这个方法,回到ACTION_MOVE,看步骤3,一直不成立的mDragState == STATE_DRAGGING,也因为触发了setDragState,使得状态变为了STATE_DRAGGING,使得步骤3的判断成立了,因此,直接break掉,跳出来ACTION_MOVE,这是,返回值return mDragState == STATE_DRAGGING也就势成立,这说明ACTION_MOVE开始进行事件拦截,而我们在DragViewGroup的拦截事件也因此返回true,事件不在向下传递,直接交给DragViewGroup的onTOuchEvent出来。也就是说,ScrollView接受不到ACTION_MOVE的事件消息了。

看看我们怎么对事件进行消费的:

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        mViewDragHelper.processTouchEvent(ev);
        return true;
    }
我们将事件的消费交由了ViewDragHelper处理,并直接返回了true。那么关键代码还是在ViewDragHelper源码中。

	    case MotionEvent.ACTION_MOVE: {
                if (mDragState == STATE_DRAGGING) {
                    // If pointer is invalid then skip the ACTION_MOVE.
                    if (!isValidPointerForActionMove(mActivePointerId)) break;

                    final int index = ev.findPointerIndex(mActivePointerId);
                    final float x = ev.getX(index);
                    final float y = ev.getY(index);
                    final int idx = (int) (x - mLastMotionX[mActivePointerId]);
                    final int idy = (int) (y - mLastMotionY[mActivePointerId]);

                    dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);//关键步骤
                    saveLastMotion(ev);
                } else {

//省略了出STATE_DRAGGING状态外的处理
......
                      
                }
                break;
            }
这就是ViewDragHelper的processTouchEvent方法中,关于ACTION_MOVE的源码。因为此时状态已经是STATE_DRAGGING,所以我们只看满足条件的分支。其中最关键的就是dragTo这个方法调用,他就是将我们拖动得到的位置信息进行效果展示,这个方法内部主要就是通过offsetLeftAndRight和offsetTopAndBottom进行位置改变,以及将最新的位置信息传递给Callback中的clampViewPositionHorizontal和clampViewPositionVertical两个回调方法。

源码如下:

    private void dragTo(int left, int top, int dx, int dy) {
        int clampedX = left;
        int clampedY = top;
        final int oldLeft = mCapturedView.getLeft();
        final int oldTop = mCapturedView.getTop();
        if (dx != 0) {
            clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
            ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
        }
        if (dy != 0) {
            clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
            ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
        }

        if (dx != 0 || dy != 0) {
            final int clampedDx = clampedX - oldLeft;
            final int clampedDy = clampedY - oldTop;
            mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
                    clampedDx, clampedDy);
        }
    }
至此,侧滑菜单就已经画出来了。

2、解决滑出的菜单无法拖回的问题

当按下手指,触发ACTION_DOWN的时候,逻辑与上面的是相同的,这里就不在重复,我们直接进行ACTION_MOVE的分析。

            case MotionEvent.ACTION_MOVE: {
                if (mInitialMotionX == null || mInitialMotionY == null) break;

                // First to cross a touch slop over a draggable view wins. Also report edge drags.
                final int pointerCount = ev.getPointerCount();
                for (int i = 0; i < pointerCount; i++) {
                    final int pointerId = ev.getPointerId(i);

                    // If pointer is invalid then skip the ACTION_MOVE.
                    if (!isValidPointerForActionMove(pointerId)) continue;

                    final float x = ev.getX(i);
                    final float y = ev.getY(i);
                    final float dx = x - mInitialMotionX[pointerId];
                    final float dy = y - mInitialMotionY[pointerId];

                    final View toCapture = findTopChildUnder((int) x, (int) y);
                    
                    //步骤1
                    final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
                    if (pastSlop) {
                        // check the callback's
                        // getView[Horizontal|Vertical]DragRange methods to know
                        // if you can move at all along an axis, then see if it
                        // would clamp to the same value. If you can't move at
                        // all in every dimension with a nonzero range, bail.
                        final int oldLeft = toCapture.getLeft();
                        final int targetLeft = oldLeft + (int) dx;
                        final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
                                targetLeft, (int) dx);
                        final int oldTop = toCapture.getTop();
                        final int targetTop = oldTop + (int) dy;
                        final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
                                (int) dy);
                        final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
                                toCapture);
                        final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
                        if ((horizontalDragRange == 0 || horizontalDragRange > 0
                                && newLeft == oldLeft) && (verticalDragRange == 0
                                || verticalDragRange > 0 && newTop == oldTop)) {
                            break;
                        }
                    }
                    //步骤2
                    reportNewEdgeDrags(dx, dy, pointerId);
                    //步骤3
                    if (mDragState == STATE_DRAGGING) {
                        // Callback might have started an edge drag
                        break;
                    }
                    //步骤4,解决靠这个判断
                    if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
                        break;
                    }
                }
                saveLastMotion(ev);
                break;
            }
因为拖动时,步骤1的pastSlop最终值依然是false,步骤2因为不是边界拖动,导致reportNewEdgeDrags中的最后的判断一致不成立,也就是说,Callback中的mCallback.onEdgeDragStarted(dragsStarted, pointerId)回调不能执行,连锁反应就是captureChildView方法不会被调用,不能调用setDragState设置,mDragState状态一直是初始值STATE_IDLE。导致事件不被拦截,最终由ScrollView消费,而ViewDragHelper不能对事件进行消费,所以无法拖动。

那么我们要用什么办法呢?看上面源码的步骤4,其中有个tryCaptureViewForDrag(toCapture, pointerId)方法,它是尝试捕获拖动View的方法。我们看看它的实现:

    boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
        if (toCapture == mCapturedView && mActivePointerId == pointerId) {
            // Already done!
            return true;
        }
        if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
            mActivePointerId = pointerId;
            captureChildView(toCapture, pointerId);//关键
            return true;
        }
        return false;
    }
这个方法简单来说,就是我们捕获的view是指定的被拖动的view,就返回true。那么我们的侧滑菜单已经展开,并点击在其上,对其拖动,说明我们已经捕获成功,这个方法是返回true的。再仔细看,captureChildView这个方法不就是之前我们自己在回调onEdgeDragStarted中调用的么,他就是触发状态设置,实现拦击的关键点啊。好,回来看ACTION_MOVE,既然tryCaptureViewForDrag是true,并且其中有触发状态设置,实现拦截的关键,那么我们就需要它可以知道,但是由于pastSlop为false,因为&&运算,导致tryCaptureViewForDrag不被执行,那么我们就要想办法是pastSlop最终为true,那个关键源码就是在步骤1的后半部分判断中。
    private boolean checkTouchSlop(View child, float dx, float dy) {
        if (child == null) {//步骤1
            return false;
        }
        final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;//步骤2
        final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0; //步骤3
        if (checkHorizontal && checkVertical) {//步骤4
            return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
        } else if (checkHorizontal) {
            return Math.abs(dx) > mTouchSlop;
        } else if (checkVertical) {
            return Math.abs(dy) > mTouchSlop;
        }
        return false;
    }
在checkTouchSlop方法中,由于步骤2、3默认值为0,不成立,所以导致步骤4不成立,整体返回值为false,最终导致pastSlop为false。因为我们的侧滑菜单是水平滑动,所以我们使步骤2成立,那么,步骤4的水平判断分支在滑动超过最小判断距离后将会成立,那么pastSlop就会为true,我们的目的就实现了。

在DragViewGroup的Callback中,重写getViewHorizontalDragRange:

    @Override
        public int getViewHorizontalDragRange(View child) {
            return 1;
        }
看看效果吧:

用ViewDragHelper自定义侧滑菜单——浅析源码解决与ScrollView的滑动冲突_第3张图片

我擦擦。。。怎会回事,怎么连侧滑都拖不出来了,是不是这么改是不对的呢?其实不是,我们再看看看,还是ViewDragHelper拦截方法中的ACTION_MOVE:

            case MotionEvent.ACTION_MOVE: {
                if (mInitialMotionX == null || mInitialMotionY == null) break;

                // First to cross a touch slop over a draggable view wins. Also report edge drags.
                final int pointerCount = ev.getPointerCount();
                for (int i = 0; i < pointerCount; i++) {
                    final int pointerId = ev.getPointerId(i);

                    // If pointer is invalid then skip the ACTION_MOVE.
                    if (!isValidPointerForActionMove(pointerId)) continue;

                    final float x = ev.getX(i);
                    final float y = ev.getY(i);
                    final float dx = x - mInitialMotionX[pointerId];
                    final float dy = y - mInitialMotionY[pointerId];

                    final View toCapture = findTopChildUnder((int) x, (int) y);
                    
                    //步骤1
                    final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
                    if (pastSlop) {
                        // check the callback's
                        // getView[Horizontal|Vertical]DragRange methods to know
                        // if you can move at all along an axis, then see if it
                        // would clamp to the same value. If you can't move at
                        // all in every dimension with a nonzero range, bail.
                        final int oldLeft = toCapture.getLeft();
                        final int targetLeft = oldLeft + (int) dx;
                        final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
                                targetLeft, (int) dx);
                        final int oldTop = toCapture.getTop();
                        final int targetTop = oldTop + (int) dy;
                        final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
                                (int) dy);
                        final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
                                toCapture);
                        final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
			//关键判断,问题所在
                        if ((horizontalDragRange == 0 || horizontalDragRange > 0
                                && newLeft == oldLeft) && (verticalDragRange == 0
                                || verticalDragRange > 0 && newTop == oldTop)) {
                            break;
                        }
                    }
                    //步骤2
                    reportNewEdgeDrags(dx, dy, pointerId);
                    //步骤3
                    if (mDragState == STATE_DRAGGING) {
                        // Callback might have started an edge drag
                        break;
                    }
                    //步骤4,解决靠这个判断
                    if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
                        break;
                    }
                }
                saveLastMotion(ev);
                break;
            }

因为我们重写了getViewHorizontalDragRange方法,使之返回值>0,所以步骤1的pastSlop为true,那么他就进入了下面那一大段的分支判断,也就获取一些新、老位置信息等。其中有一段判断,注释已经标出,就是问题所在,因为horizontalDragRange已经被我们重写返回1,所以它是>0的,然后因为侧滑菜单还没有滑出,我们捕获不到需要被拖动的View,因此导致newLeft与oldLeft都为0。因为没有重新垂直方向上的方法getViewVerticalDragRange,所以verticalDragRange为0,判断整体成立,直接break出去了,导致下面我们希的步骤2、3、4,都没有执行,因为步骤2、3是拖出侧滑菜单的关键,步骤4是拖回侧滑菜单的关键,他们都不执行,所以......那怎么办,

动动脑筋,getViewHorizontalDragRange返会0,菜单可以拖出,返回1,菜单可以拖回。那么我们需要加一个标识,记录菜单是拖出状态,还是未拖出状态,未拖出,返回0;拖出,返回1,不就解决了么。

下面是DragViewGroup的最终完整代码:

public class DragViewGroup extends RelativeLayout {
    private ViewDragHelper mViewDragHelper;
    public View mMainView;
    private int mWidth;
    private boolean isShowMenu;

    public DragViewGroup(Context context) {
        super(context);
        initView();
    }

    public DragViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }


    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMainView = getChildAt(2);
    }

    private void initView() {
        mViewDragHelper = ViewDragHelper.create(this, callback);
        mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = mMainView.getMeasuredWidth();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        mViewDragHelper.processTouchEvent(ev);
        return true;
    }


    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return mMainView == child;
        }


        @Override
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
            super.onEdgeDragStarted(edgeFlags, pointerId);
            mViewDragHelper.captureChildView(mMainView, pointerId);
        }

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return 0;
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return Math.min(left, 0);
        }

        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            if (mMainView.getRight() < mWidth / 2) {//当拖动距离小于侧划菜单宽度的一半时,菜单收回
                isShowMenu = false;
                mViewDragHelper.smoothSlideViewTo(mMainView, -mWidth, 0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            } else {//当拖动距离大于等于侧划菜单宽度的一半时,菜单完全展开
                isShowMenu = true;
                mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }
        }

        @Override
        public int getViewHorizontalDragRange(View child) {
            if (isShowMenu) {
                return 1;
            } else {
                return 0;
            }
        }

    };

    @Override
    public void computeScroll() {
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }
}

好了,到这里,我们的例子就已经实现了。

为了方便读者研究,demo中将ViewDragHelper所有代码复制出来,形成自定义的ViewDragHelper,方便大家断点调试追踪代码。

Demo下载地址:点击打开链接








你可能感兴趣的:(用ViewDragHelper自定义侧滑菜单——浅析源码解决与ScrollView的滑动冲突)