为何说Android ViewDragHelper是神器 (二)

来源:互联网 发布:python json模块 编辑:程序博客网 时间:2024/04/29 05:46

前言: 通过上一篇的为何说Android ViewDragHelper是神器 (一)中我们简单了解了ViewDragHelper的用法,然后实现了一个“view随手指滑动而滑动”的效果,代码很简单,但是VDH中处理的逻辑却很多很多,不得不说VDH真的是神器,要我们自己写的话得写一段时间了,接下来我们继续往下研究研究VDH,加油吧!骚年(^__^) !!!
以下demo内容大致参考鸿阳博客中的Android ViewDragHelper解析 一文,阳神一直是我崇拜的一个偶像(^__^) 。

ViewDragHelper还能做以下的一些操作:

  • 边界检测、加速度检测(eg:DrawerLayout边界触发拉出)

  • 移动到某个指定的位置(eg:点击Button,展开/关闭Drawerlayout)

  • 回调Drag Release(eg:DrawerLayout部分,手指抬起,自动展开/收缩)

    那么我们接下来对我们最基本的例子进行改造,包含上述的几个操作。
    我们再创建两个view,id叫autobackview(拖动后手指一抬起返回初始位置),edgeview(滑动边缘开始滑动的view):
    ids.xml:

<?xml version="1.0" encoding="utf-8"?><resources>    <!--可以被拖动的View-->    <item name="draggedview" type="id"/>    <!--拖动后手指一抬起返回初始位置-->    <item name="autobackview" type="id"/>    <!--滑动边缘开始滑动的view-->    <item name="edgeview" type="id"/></resources>

text_layout.xml:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:id="@+id/activity_main"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context="com.cisetech.demo.MainActivity">    <com.cisetech.demo.DragView        android:layout_width="match_parent"        android:layout_height="match_parent"        android:orientation="vertical"        android:layout_alignParentTop="true"        android:layout_alignParentLeft="true"        android:layout_alignParentStart="true">        <TextView            android:id="@id/draggedview"            android:layout_margin="10dp"            android:gravity="center"            android:layout_gravity="center"            android:background="#44ff00ff"            android:text="我可以被拖动"            android:layout_width="100dp"            android:layout_height="100dp"/>        <TextView            android:id="@id/autobackview"            android:layout_margin="10dp"            android:layout_gravity="center"            android:gravity="center"            android:background="#44ff"            android:text="我可以自动回到初始位置"            android:layout_width="100dp"            android:layout_height="100dp"/>        <TextView            android:id="@id/edgeview"            android:layout_margin="10dp"            android:layout_gravity="center"            android:gravity="center"            android:background="#44ff00"            android:text="滑动边缘移动我"            android:layout_width="100dp"            android:layout_height="100dp"/>    </com.cisetech.demo.DragView></RelativeLayout>

我们改改DragView:

package com.cisetech.demo;import android.content.Context;import android.graphics.Point;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.widget.LinearLayout;/** * author:yinqingy * date:2016-11-06 13:49 * blog:http://blog.csdn.net/vv_bug * desc: */public class DragView extends LinearLayout{    private ViewDragHelper mDragger;    private View mEdgeView,mAutoBackView;    private Point mAutoBackOriginPos=new Point();    public DragView(Context context) {        super(context);        init();    }    public DragView(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }    public DragView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    private void init() {        mDragger=ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {            @Override            public boolean tryCaptureView(View child, int pointerId) {                //当id为edgeview的时候,不允许其滑动                return child.getId()==R.id.draggedview||child.getId()==R.id.autobackview;            }            @Override            public int clampViewPositionHorizontal(View child, int left, int dx) {                return left;            }            @Override            public int clampViewPositionVertical(View child, int top, int dy) {                return top;            }            /**             * 当手指在边缘拖动的时候回调此方法             * edgeFlags分为left、top、right、bottom             */            @Override            public void onEdgeDragStarted(int edgeFlags, int pointerId) {                //当在边缘滑动的时候                mDragger.captureChildView(mEdgeView, pointerId);            }            //手指释放的时候回调            @Override            public void onViewReleased(View releasedChild, float xvel, float yvel) {                //mAutoBackView手指释放时可以自动回去                if (releasedChild.getId()==R.id.autobackview) {                    mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);                    invalidate();                }            }        });        //一定要加上这句代码,不然就checkNewEdgeDrag就不会进入判断了        mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        return mDragger.shouldInterceptTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        mDragger.processTouchEvent(event);        return true;    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        super.onLayout(changed, l, t, r, b);        //获取autoBackView的初始位置        mAutoBackOriginPos.x = mAutoBackView.getLeft();        mAutoBackOriginPos.y = mAutoBackView.getTop();    }    @Override    protected void onFinishInflate() {        super.onFinishInflate();        mEdgeView=findViewById(R.id.edgeview);        mAutoBackView=findViewById(R.id.autobackview);    }    @Override    public void computeScroll() {        if(mDragger.continueSettling(true))            invalidate();        }    }

运行代码:
这里写图片描述

我们来分析下代码:
代码中都有注释,我就不一一解释了,先解释下边缘滑动的代码:
首先:

@Override            public boolean tryCaptureView(View child, int pointerId) {                //当id为edgeview的时候,不允许其滑动                return child.getId()==R.id.draggedview||child.getId()==R.id.autobackview;            }

不允许edgeview直接滑动,所以返回的是false,
然后:

 /**             * 当手指在边缘拖动的时候回调此方法             * edgeFlags分为left、top、right、bottom             */            @Override            public void onEdgeDragStarted(int edgeFlags, int pointerId) {                //当在边缘滑动的时候                mDragger.captureChildView(mEdgeView, pointerId);            }

在当手指触碰到ViewGroup的边缘的时候,调用了mDragger.captureChildView方法,
最后:

//一定要加上这句代码,不然就checkNewEdgeDrag就不会进入判断了        mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);

很少的代码,我们的view就可以实现边缘滑动了(不了解边缘滑动的可以想象下侧滑菜单(^__^) 嘻嘻……),那么我们进入到VDH源码中看看为什么可以边缘滑动?
首先看看onEdgeDragStarted在哪调用的?

  private void reportNewEdgeDrags(float dx, float dy, int pointerId) {        int dragsStarted = 0;        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);        }    }

我们可以看到是在一个叫reportNewEdgeDrags的方法中调用的,那么reportNewEdgeDrags又是在哪调用的呢?
在VDH中的processTouchEvent方法中我们看到:

    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 {                    // Check to see if any pointer is now over a draggable view.                    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];                        reportNewEdgeDrags(dx, dy, pointerId);                        if (mDragState == STATE_DRAGGING) {                            // Callback might have started an edge drag.                            break;                        }                        final View toCapture = findTopChildUnder((int) x, (int) y);                        if (checkTouchSlop(toCapture, dx, dy)                                && tryCaptureViewForDrag(toCapture, pointerId)) {                            break;                        }                    }                    saveLastMotion(ev);                }                break;            }

当mDragState != STATE_DRAGGING的时候会调用reportNewEdgeDrags方法,在VDH中只有当mDragState ==STATE_DRAGGING的时候才能对view进行拖动,那么我们看看mDragState 在什么地方被置成STATE_DRAGGING标记的?

 /**     * Capture a specific child view for dragging within the parent. The callback will be notified     * but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to     * capture this view.     *     * @param childView Child view to capture     * @param activePointerId ID of the pointer that is dragging the captured child view     */    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中,我们很清晰的看到setDragState(STATE_DRAGGING);这么一段代码,当mDragg为STATE_DRAGGING状态的时候,当进入到processTouchEvent方法的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);                }

调了dragTo方法就可以拖动了,dragTo在前一篇博客中有提及,我就不再说明了,到此,边界拖动的代码已经解析完毕了。

接下来看看松手自动返回的代码:

   }            //手指释放的时候回调            @Override            public void onViewReleased(View releasedChild, float xvel, float yvel) {                //mAutoBackView手指释放时可以自动回去                if (releasedChild.getId()==R.id.autobackview) {                    mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);                    invalidate();                }            }        });

在手指松开的时候会调用onViewReleased方法,然后我们调用了VDH的settleCapturedViewAt方法,我们看看settleCapturedViewAt内部:

private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {        final int startLeft = mCapturedView.getLeft();        final int startTop = mCapturedView.getTop();        final int dx = finalLeft - startLeft;        final int dy = finalTop - startTop;        if (dx == 0 && dy == 0) {            // Nothing to do. Send callbacks, be done.            mScroller.abortAnimation();            setDragState(STATE_IDLE);            return false;        }        final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);        mScroller.startScroll(startLeft, startTop, dx, dy, duration);        setDragState(STATE_SETTLING);        return true;    }

其内部主要调用了forceSettleCapturedViewAt方法,说到底还是调用了mScroller.startScroll(startLeft, startTop, dx, dy, duration);方法,Scroller的用法不懂的自己去脑补啊,还是很重要的一个组件的(^__^) 嘻嘻……既然有Scroller,我们就要重写View的computeScroll方法,所以我们在DragView中有重写:

@Override    public void computeScroll() {        if(mDragger.continueSettling(true))            invalidate();        }    }

其实其continueSettling的内部想必知道Scroller的童鞋应该猜得出干了什么:

boolean keepGoing = mScroller.computeScrollOffset();

到此手指松开回到原来位置的代码也分析完毕了。

细心的童鞋可以发现,我们做测试用的View都是TextView,因为TextView本身就不具备可点击性,如果换成本身具有可点击性的Button,那么还会有一样的效果吗?
我们试试:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:id="@+id/activity_main"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context="com.cisetech.demo.MainActivity">    <com.cisetech.demo.DragView        android:layout_width="match_parent"        android:layout_height="match_parent"        android:orientation="vertical"        android:layout_alignParentTop="true"        android:layout_alignParentLeft="true"        android:layout_alignParentStart="true">        <Button            android:id="@id/draggedview"            android:layout_margin="10dp"            android:gravity="center"            android:layout_gravity="center"            android:background="#44ff00ff"            android:text="我可以被拖动"            android:layout_width="100dp"            android:layout_height="100dp"/>        <Button            android:id="@id/autobackview"            android:layout_margin="10dp"            android:layout_gravity="center"            android:gravity="center"            android:background="#44ff"            android:text="我可以自动回到初始位置"            android:layout_width="100dp"            android:layout_height="100dp"/>        <Button            android:id="@id/edgeview"            android:layout_margin="10dp"            android:layout_gravity="center"            android:gravity="center"            android:background="#44ff00"            android:text="滑动边缘移动我"            android:layout_width="100dp"            android:layout_height="100dp"/>    </com.cisetech.demo.DragView></RelativeLayout>

当我们换成Button后,我们运行发现,只有边界移动的view可以移动,其它两个view不管怎么滑动都没效果哦,为什么呢?

主要是因为,如果子View不消耗事件,那么整个手势(DOWN-MOVE*-UP)都是直接进入onTouchEvent,在onTouchEvent的DOWN的时候就确定了captureView。如果消耗事件,那么就会先走onInterceptTouchEvent方法,判断是否可以捕获,而在判断的过程中会去判断另外两个回调的方法:getViewHorizontalDragRange和getViewVerticalDragRange,只有这两个方法返回大于0的值才能正常的捕获。

所以,如果你用Button测试,或者给TextView添加了clickable = true ,都记得重写下面这两个方法:

@Overridepublic int getViewHorizontalDragRange(View child){     return getMeasuredWidth()-child.getMeasuredWidth();}@Overridepublic int getViewVerticalDragRange(View child){     return getMeasuredHeight()-child.getMeasuredHeight();}

方法的返回值应当是该childView横向或者纵向的移动的范围,当前如果只需要一个方向移动,可以只复写一个。
这个时候你肯定又会问“为什么重写这两个方法就可以了呢?”
(涉及到事件分发的知识,不懂的童鞋还是得脑补一下哈(^__^) 嘻嘻……)我们来看看原因:
在VDH中的shouldInterceptTouchEvent方法中我们看到这么一段代码:

  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);                    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;                        }                    }                    reportNewEdgeDrags(dx, dy, pointerId);                    if (mDragState == STATE_DRAGGING) {                        // Callback might have started an edge drag                        break;                    }                    if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {                        break;                    }                }                saveLastMotion(ev);                break;            }

代码有点长,我们看重点,我们看到这么一段代码:

final View toCapture = findTopChildUnder((int) x, (int) y);                    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;                        }                    }                    reportNewEdgeDrags(dx, dy, pointerId);                    if (mDragState == STATE_DRAGGING) {                        // Callback might have started an edge drag                        break;                    }                    if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {                        break;                    }

当pastSlop为true的时候,才会去跑:

 if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {                        break;                    }

当跑了tryCaptureViewForDrag的时候就会去走captureChildView方法:

    */    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);    }

而这个时候setDragState(STATE_DRAGGING);会给mDragState设置成STATE_DRAGGING,当设置成了STATE_DRAGGING,在shouldInterceptTouchEvent的最后会返回true:

return mDragState == STATE_DRAGGING;

当shouldInterceptTouchEvent返回true以后,我们自定义的ViewGroup中的onInterceptTouchEvent也就返回true了,因此直接拦截了子View的事件,所以接下来才会进ViewGoup的onTouchEvent方法,所以才可以滑动。
有点复杂的感觉额,但是如果很清晰的掌握了事件分发流程,还是很好理解的。

细心的童鞋会发现,我们的View拖动的边界没有限制,以至于都拖到ViewGroup外面去了,好吧,我就直接贴代码了。

左右的边界:
左边为getPaddingLeft(),右边为getWidth() - mDragView.getWidth() - getPaddingRight():

 public int clampViewPositionHorizontal(View child, int left, int dx)            {                final int leftBound = getPaddingLeft();                final int rightBound = getWidth() - mDragView.getWidth() -  getPaddingRight();                final int newLeft = Math.min(Math.max(left, leftBound), rightBound);                return newLeft;            }

上下的的边界: 上边为getPaddingTop(),下边为 getHeight() - child.getHeight() - getPaddingBottom():

 @Override            public int clampViewPositionVertical(View child, int top, int dy) {                final int topBound = getPaddingTop();                final int bottomBound = getHeight() - child.getHeight() -  getPaddingBottom();                final int newTop = Math.min(Math.max(top, topBound), bottomBound);                return newTop;            }

好吧!到此,VDH的基本用法就介绍到这里了,接下来会进入到VDH的实战部分,有兴趣的童鞋可以跟我一起进入VDH实战部分哦!!!
未完待续………….

本文部分内容来自:
http://blog.csdn.net/lmj623565791/article/details/46858663

最后附上demo的git链接:
https://github.com/913453448/SwipeBackLayout

0 0