知识梳理:ViewDragHelper

ViewDragHelper出来这么久了,今天终于回想起它了,看过国外的相关blog资料,也看过翔哥blog,清晰的说明了使用方法,知乎上也有不少的资料,所以呢决定重新整理一下,顺便梳理知识。(只有自己亲自动手写过,才能更好的掌握,只看别人的blog,只能说你了解过,能使用,但是遇到需求变更,就只能各个群里拜大神)

/** * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number * of useful operations and state tracking for allowing a user to drag and reposition * views within their parent ViewGroup. */

ViewDragHelper类对于ViewGroup自定义是有非常大的帮助的,他可以跟踪用户的拖拽轨迹,并重新定位。

public class ViewDragHelper {

   /** * Apps should use ViewDragHelper.create() to get a new instance. * This will allow VDH to use internal compatibility implementations for different * platform versions. * 如果你不想new 出实例对象,也可以通过ViewDragHelper.create(context,callback)方法获得实例。 * @param context Context to initialize config-dependent params from * @param forParent Parent view to monitor */
    private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
        //传入的ViewGroup和CallBack回调实例不能为空 
        if (forParent == null) {
            throw new IllegalArgumentException("Parent view may not be null");
        }
        if (cb == null) {
            throw new IllegalArgumentException("Callback may not be null");
        }
        //初始化变量 
        mParentView = forParent;
        mCallback = cb;

        final ViewConfiguration vc = ViewConfiguration.get(context);
        final float density = context.getResources().getDisplayMetrics().density;
        mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);

        mTouchSlop = vc.getScaledTouchSlop();
        mMaxVelocity = vc.getScaledMaximumFlingVelocity();
        mMinVelocity = vc.getScaledMinimumFlingVelocity();
        mScroller = ScrollerCompat.create(context, sInterpolator);
    }
}

ViewConfiguration 类中的值一遍在自定义高级控件都会用到,内部定义很多变量值,各有其用途,比如上例代码块中的mTouchSlop:在可滑动的控件中用于区别单击子控件和滑动操作的一个值,mMaxVelocity、mMinVelocity 最大Fling滑动速度和最大Fling滑动速度等。传入Scroller的动画曲线差值器,初始化一个Scroller类

   /** * Interpolator defining the animation curve for mScroller */
    private static final Interpolator sInterpolator = new Interpolator() {
        public float getInterpolation(float t) {
            t -= 1.0f;
            return t * t * t * t * t + 1.0f;
        }
    };

ViewDragHelper提供的create方法有两个,第二个方法多传入了sensitivity影响拖拽的敏感系数。sensitivity值越大,mTouchSlop 就越小越敏感,从而影响到滑动控件的滑动和点击事件的分发。

  /** * Factory method to create a new ViewDragHelper. * * @param forParent Parent view to monitor * @param cb Callback to provide information and receive events * @return a new ViewDragHelper instance */
    public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
        return new ViewDragHelper(forParent.getContext(), forParent, cb);
    }

    /** * Factory method to create a new ViewDragHelper. * * @param forParent Parent view to monitor * @param sensitivity Multiplier for how sensitive the helper should be about detecting * the start of a drag. Larger values are more sensitive. 1.0f is normal. * @param cb Callback to provide information and receive events * @return a new ViewDragHelper instance */
    public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
        final ViewDragHelper helper = create(forParent, cb);
        helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
        return helper;
    }

再来了解静态抽象类ViewDragHelper.Callback的定义

    public static abstract class Callback {
        /** * Called when the drag state changes. See the <code>STATE_*</code> constants * for more information. * 拖动状态改变时调用的方法 * @param state The new drag state * * @see #STATE_IDLE * @see #STATE_DRAGGING * @see #STATE_SETTLING */
        public void onViewDragStateChanged(int state) {}

        /** * Called when the captured view's position changes as the result of a drag or settle. * 视图位置发生了变化,捕获到相关的数据,并回调相关数值 * @param changedView View whose position changed(发生变化的视图) * @param left New X coordinate of the left edge of the view(视图左边缘坐标) * @param top New Y coordinate of the top edge of the view(视图上边缘坐标) * @param dx Change in X position from the last call(拖拽的x距离) * @param dy Change in Y position from the last call(拖拽的Y距离) */
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}

        /** * 当子view被拖曳或被settle, 而被捕获时回调的方法. * @param capturedChild Child view that was captured * @param activePointerId Pointer id tracking the child capture(跟踪子View捕捉到的指针标识) */
        public void onViewCaptured(View capturedChild, int activePointerId) {}

        /** * 当子view不再被拖曳时调用.如果有需要,fling滑动的速度也会被提供.速度值会介于 * 系统最小化和最大值之间.(也就是构造函数里面初始化的两个值) * @param releasedChild The captured child view now being released * @param xvel X x轴离开的速率 * @param yvel Y y轴离开的速率 */
        public void onViewReleased(View releasedChild, float xvel, float yvel) {}

        /** * 当父view其中一个被标记可拖曳的边缘被用户触摸, 同时父view里没有子view被捕获响应时回调该方法. * * edgeFlags 受到拖拽影响的边缘 * pointerId 跟踪View拖拽边缘是的指针标识 * @see #EDGE_LEFT * @see #EDGE_TOP * @see #EDGE_RIGHT * @see #EDGE_BOTTOM * @see #EDGE_ALL */
        public void onEdgeTouched(int edgeFlags, int pointerId) {}

        /** * 当原来可以拖曳的边缘被锁定不可拖曳时回调,比如抽屉控件+ViewPager嵌套 * ViewPager左右滑动选择是否锁定抽屉控件的滑动 * @param edgeFlags A combination of edge flags describing the edge(s) locked * @return true to lock the edge, false to leave it unlocked */
        public boolean onEdgeLock(int edgeFlags) {
            return false;
        }

        /** * 当用户用开始从屏幕边缘拖曳,并且父view中没有子view影响时调用. */
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {}

        /** * 子视图的z轴的顺序值。 */
        public int getOrderedChildIndex(int index) {
            return index;
        }

        /** * 返回一个子视图的水平拖动范围值,如果值为0,则不能水平拖动 * @param child Child view to check * @return range of horizontal motion in pixels */
        public int getViewHorizontalDragRange(View child) {
            return 0;
        }

        /** * 返回一个子视图的垂直拖动范围值,如果值为0,则不能垂直拖动 * @param child Child view to check * @return range of vertical motion in pixels */
        public int getViewVerticalDragRange(View child) {
            return 0;
        }

        /** * 当我们通过指针标识移动子View,会回调该函数,如果该函数返回为true,则允许我们移动子View位置。 * 如果子View已经被捕获,那么就会导致重复调用,从而指针标识控制了移动。 * 如果该方法返回为true,捕获到了子View,onViewCaptured该方法随即被调用, * * @param child Child the user is attempting to capture * @param pointerId ID of the pointer attempting the capture * @return true if capture should be allowed, false otherwise */
        public abstract boolean tryCaptureView(View child, int pointerId);

        /** * 限制的沿水平轴拖子视图 * 默认实现不允许水平拖拽 * 扩展类必须覆盖该方法,并提供所需的阀值。 * @param child Child view being dragged * @param left Attempted motion along the X axis * @param dx Proposed change in position for left * @return The new clamped position for left */
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return 0;
        }

        /** * 限制的沿垂直轴拖子视图 * 默认实现不允许垂直拖拽 * 扩展类必须覆盖该方法,并提供所需的阀值。 * @param child Child view being dragged * @param top Attempted motion along the Y axis * @param dy Proposed change in position for top * @return The new clamped position for top */
        public int clampViewPositionVertical(View child, int top, int dy) {
            return 0;
        }
    }

ViewDragHelper类里面也有许多常量,下面来逐一了解。

    /** * A null/invalid pointer ID.无效ID */
    public static final int INVALID_POINTER = -1;

    /** * A view is not currently being dragged or animating as a result of a fling/snap. * 没有被拖拽或没有拖拽相关动画执行的状态 */
    public static final int STATE_IDLE = 0;

    /** * A view is currently being dragged. The position is currently changing as a result * of user input or simulated user input. * 子View根据用户拖拽改变位置的状态 */
    public static final int STATE_DRAGGING = 1;

    /** * A view is currently settling into place as a result of a fling or * predefined non-interactive motion. * 根据标识设置改变view的位置的过程,simple:A------>>------B这个过程 */
    public static final int STATE_SETTLING = 2;

    /** * Edge flag indicating that the left edge should be affected. * 拖拽会影响到的左侧边缘 */
    public static final int EDGE_LEFT = 1 << 0;

    /** * Edge flag indicating that the right edge should be affected. * 拖拽会影响到的右侧边缘 */
    public static final int EDGE_RIGHT = 1 << 1;

    /** * Edge flag indicating that the top edge should be affected. * 拖拽会影响到的顶部边缘 */
    public static final int EDGE_TOP = 1 << 2;

    /** * Edge flag indicating that the bottom edge should be affected. * 拖拽会影响到的底部边缘 */
    public static final int EDGE_BOTTOM = 1 << 3;

    /** * Edge flag set indicating all edges should be affected. * 四周都可以被拖拽 */
    public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;

    /** * Indicates that a check should occur along the horizontal axis * 拖拽方向:水平 */
    public static final int DIRECTION_HORIZONTAL = 1 << 0;

    /** * Indicates that a check should occur along the vertical axis * 拖拽方向:垂直 */
    public static final int DIRECTION_VERTICAL = 1 << 1;

    /** * Indicates that a check should occur along all axes * 拖拽方向:水平和垂直皆可 */
    public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;

    private static final int EDGE_SIZE = 20; // 边缘值大小 20dp

    private static final int BASE_SETTLE_DURATION = 256; //settle基本的时间值 256ms
    private static final int MAX_SETTLE_DURATION = 600; //settle的最大时间值 600ms

    // Current drag state; idle, dragging or settling 拖拽有三个状态,当前的拖拽状态变量
    private int mDragState;

    // Distance to travel before a drag may begin 触发拖拽的最大值
    private int mTouchSlop;

    // Last known position/pointer tracking 跟踪子拖拽View的指针标识
    private int mActivePointerId = INVALID_POINTER;
    //初始化记录拖拽的x坐标值
    private float[] mInitialMotionX;
    //初始化记录拖拽的y坐标值
    private float[] mInitialMotionY;
    private float[] mLastMotionX;
    private float[] mLastMotionY;
    private int[] mInitialEdgesTouched;
    //边缘拖拽的距离变化
    private int[] mEdgeDragsInProgress;
    //边缘拖拽被锁定的集合
    private int[] mEdgeDragsLocked;
    private int mPointersDown;

    private VelocityTracker mVelocityTracker;
    private float mMaxVelocity;
    private float mMinVelocity;
    //边缘距离大小
    private int mEdgeSize;

    private int mTrackingEdges;
    //兼容的Scroller
    private ScrollerCompat mScroller;

    private final Callback mCallback;

    private View mCapturedView;
    private boolean mReleaseInProgress;

    private final ViewGroup mParentView;

再来细看ViewDragHelper内部一些基本的set get方法定义

    /** * 设置最小的滑动速度 * * @param minVel Minimum velocity to detect */
    public void setMinVelocity(float minVel) {
        mMinVelocity = minVel;
    }

    public float getMinVelocity() {
        return mMinVelocity;
    }

    /** * 获取当前的拖拽状态 * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. * @return The current drag state */
    public int getViewDragState() {
        return mDragState;
    }

    /** * 设置能被跟踪的边缘 * @see #EDGE_LEFT * @see #EDGE_TOP * @see #EDGE_RIGHT * @see #EDGE_BOTTOM */
    public void setEdgeTrackingEnabled(int edgeFlags) {
        mTrackingEdges = edgeFlags;
    }

    /** * 边缘距离大小 * @return The size of an edge in pixels * @see #setEdgeTrackingEnabled(int) */
    public int getEdgeSize() {
        return mEdgeSize;
    }
    /** * @return 当前跟踪捕获的子视图 */
    public View getCapturedView() {
        return mCapturedView;
    }

    /** * @return 当天捕获到的拖拽的子View对应的指针标识 * or {@link #INVALID_POINTER}. */
    public int getActivePointerId() {
        return mActivePointerId;
    }

    //****************此处略********************

上面两个方法的mCapturedView、mActivePointerId变量,在调用captureChildView()方法时初始化,同时改变拖拽状态,

    /** * @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;
        //子view被拖曳或被settle, 而被捕获时Callback回调方法.
        mCallback.onViewCaptured(childView, activePointerId);
        setDragState(STATE_DRAGGING);
    }

VelocityTracker主要用跟踪触摸屏事件的速率(滑动速度),你可以调用getXVelocity() 、getXVelocity()获得横、竖方向的速率,下面的cancel方法,对速率跟踪的VelocityTracker进行了回收。

    /** * 这个方法等同于MotionEvent.ACTION_CANCEL一样 * {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event. */
    public void cancel() {
        //跟踪的指针标识变成无效的
        mActivePointerId = INVALID_POINTER;
        //清空跟踪的历史记录
        clearMotionHistory();

        if (mVelocityTracker != null) {
            //释放
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    private void clearMotionHistory() {
        if (mInitialMotionX == null) {
            return;
        }
        /** * 这是我以前没用过的,在这里做个备注吧 * * Arrays.fill(float[] array, float value); * simple: boolean [] flags = new boolean[2]; * Arrays.fill( flags, true); * result: flags={true,true} * * / Arrays.fill(mInitialMotionX, 0); //............略............... mPointersDown = 0; } 

abort方法先调用了上面的cancel方法,随即改变拖拽状态,如果当前状态是settling,还需要停止滑动动画,并且执行Callback回调函数onViewPositionChanged(),通过scroller计算出滑动的距离变化

    /** * {@link #cancel()}, but also abort all motion in progress and snap to the end of any * animation. */
    public void abort() {
        cancel();
        if (mDragState == STATE_SETTLING) {
            final int oldX = mScroller.getCurrX();
            final int oldY = mScroller.getCurrY();
            mScroller.abortAnimation();
            final int newX = mScroller.getCurrX();
            final int newY = mScroller.getCurrY();
            mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY);
        }
        setDragState(STATE_IDLE);
    }

当你拖拽的子View完成后MotionEvent.ACTION_UP时,子View所在的位置不是他自身应该处的位置,会调用Callback.onViewReleased()方法回调,进行相应的位置变换(例如:DrawLayout拖拽画出距离过小,会让滑出视图回到原来的位置),onViewReleased()方法回调后我们一般会用到下面这个方法smoothSlideViewTo(),

    /** * @param 要移动的View * @param 移动View到距离屏幕左侧的距离 * @param 移动View到距离屏幕顶部的距离。 */
    public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
        mCapturedView = child;
        mActivePointerId = INVALID_POINTER;
        //根据距离判断能否继续滑动(如果能继续滑动在forceSettleCapturedView()方法里面调用scroller.startScroll()继续滑动)
        boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
        if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) {
            // If we're in an IDLE state to begin with and aren't moving anywhere, we
            // end up having a non-null capturedView with an IDLE dragState
            mCapturedView = null;
        }

        return continueSliding;
    }

 /** * Settle the captured view at the given (left, top) position. * * @param finalLeft Target left position for the captured view * @param finalTop Target top position for the captured view * @param xvel Horizontal velocity * @param yvel Vertical velocity * @return true if animation should continue through {@link #continueSettling(boolean)} calls */
    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;
    }

粗略了解了Callback.onViewReleased()相关方法,再来看看clampMag(),该方法用途是保证参数中给定的速率是正确的值。从滑动时间的计算可以看出,滑动速率也是滑动时间影响之一。

    /** * * @param value Value to clamp * @param absMin Absolute value of the minimum significant value to return * @param absMax Absolute value of the maximum value to return * @return The clamped value with the same sign as <code>value</code> */
    private int clampMag(int value, int absMin, int absMax) {
        final int absValue = Math.abs(value);
        if (absValue < absMin) return 0;
        if (absValue > absMax) return value > 0 ? absMax : -absMax;
        return value;
    }

ViewDragHelper的内部方法是在太多了,就不挨着细看了,说几个重要方法开始demo吧,在onInterceptTouchEvent()方法里调ViewDragHelper的shouldInterceptTouchEvent()方法选择是否拦截事件分发,在onTouchEvent()方法里调用ViewDragHelper()的processTouchEvent()方法,ACTION_DOWN时返回true,则可以继续接收后续事件(drag),对于drag的实现ViewHelper已经帮我们实现了,如果你对事件分发不太了解的建议先去看看ViewGroup View 相关的资料。

下面来简单实践一下拖拽效果,如下图

知识梳理:ViewDragHelper_第1张图片


/** * Created by LanYan on 2016/1/19. */
public class DragLinearLayout extends LinearLayout{

    private TextView textView1,textView2;

    private final ViewDragHelper mViewDragHelper;
    private Point textViewOldOptions = new Point();

    public DragLinearLayout(Context context) {
        this(context, null);
    }

    public DragLinearLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

    }

根据ViewDragHelper说明,我们需要重写onInterceptTouchEvent拦截方法和onTouchEvent触摸方法,交给ViewDragHelper处理。

   @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        mViewDragHelper.processTouchEvent(event);
        return true;
    }

接着我们来初始化我们的需要被拖拽的视图控件,onFinishInflate()该方法调用时机在系统解析XML完成,把子View全部添加完成后,一般在自定义ViewGroup常用到,在这个方法中初始化自己需要用到的控件。

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        textView1 = (TextView) getChildAt(0);
        textView2 = (TextView) getChildAt(1);
    }

在构造函数里面需要初始化ViewDragHelper类,设置他的触摸边界、范围等。

 mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {

            @Override
            public void onViewDragStateChanged(int state) {
                super.onViewDragStateChanged(state);
                Log.i("info", "DragStatus:"+state);
            }

            @Override
            public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
                super.onViewPositionChanged(changedView, left, top, dx, dy);
                Log.i("info", "left:" + left+",top:"+top+",distanceX:"+dx+",distanceY:"+dy);
            }

            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                //允许被捕获拖拽的view视图
                return child == textView1||child == textView2;
            }

            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                super.onViewReleased(releasedChild, xvel, yvel);
                Log.i("info", "xvel:" + xvel+",yvel"+yvel);
                if(releasedChild==textView1){
                //回到初始位置,并且锁定边缘不能再被拖拽 mViewDragHelper.settleCapturedViewAt(textViewOldOptions.x,textViewOldOptions.y);
                    invalidate();
                    mViewDragHelper.setEdgeTrackingEnabled(0);
                }
            }

            @Override
            public void onEdgeTouched(int edgeFlags, int pointerId) {
                super.onEdgeTouched(edgeFlags, pointerId);
                Log.i("info", "EdgeFlags onTouch:" + edgeFlags);
            }

            @Override
            public void onEdgeDragStarted(int edgeFlags, int pointerId) {
                super.onEdgeDragStarted(edgeFlags, pointerId);
                //边缘部分拖拽尝试捕获跟踪
                mViewDragHelper.captureChildView(textView1,pointerId);
            }

            @Override
            public boolean onEdgeLock(int edgeFlags) {
                Log.i("info", "EdgeFlags lock:" + edgeFlags);
                return false;
            }

            @Override
            public int getViewHorizontalDragRange(View child) {
                return getMeasuredWidth()-child.getMeasuredWidth();
            }

            @Override
            public int getViewVerticalDragRange(View child) {
                return getMeasuredHeight()-child.getMeasuredHeight();
            }

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

            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                return top;
            }
        });
        //触摸边界为左侧,其他边界参照ViewDragHelper常量定义
        mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);

就上面这样看似没问题的代码,其实还存在一个bug,在onViewReleased方法中调用了settleCapturedViewAt方法,如果你看过我上面贴的代码不难发现,settleCapturedViewAt方法内部还调用了下面一段代码

  private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
        //....................此处略.................
        final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
        mScroller.startScroll(startLeft, startTop, dx, dy, duration);

        setDragState(STATE_SETTLING);
        return true;
    }

调用mScroller.startScroll()是不会有滚动效果的,只有在computeScroll()获取滚动情况,做出滚动的响应,而computeScroll在父控件执行drawChild时,会调用这个方法。

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

对于该效果的简单自定义,源码已上传:http://download.csdn.net/detail/analyzesystem/9411522

ViewDragHelper对我们自定义ViewGroup的帮助是相当大的,想当初我看了翔哥的blog自定义横向的ViewPager,我也学着去写了个支持横纵向的GuideViewPager,都要自己去检测滑动速率方向之类,哎,往事不堪回首,一个速率bug让我调试了半天时间。关于ViewDragHelper的相关自定义翔哥有个LeftDrawLayout,看完之后决定加深理解,于是乎自己写了一个RightDrawLayout,鉴于时间关系,不能在加班了,就放到下一篇DrawLayout blog。

你可能感兴趣的:(DragHelper)