View滚动帮助类ViewDragHelper解析

来源:互联网 发布:立式数控车床编程 编辑:程序博客网 时间:2024/06/06 00:58

ViewDraHelper解析


ViewDragHelper 是什么?

  ViewDragHelper 是Google 编写的帮助我们处理拖拽的一个帮助类,如果我们自己去处理TouchEvent事件,判断处理TouchEvent的数据,达到我们拖拽View的一个目的也是可以的,但是过程肯定是有点繁琐的,ViewDragHelper 通过处理 MotionEvent 事件,判断是否是拖拽 以及拖拽方向、速率等然后回调给我们去实现我们自己真正想要实现的功能。

如何使用ViewDragHelper

ViewDragHelper 是通过工厂方法的方式创建的,下面是它的构造函数

/*** forParent 必须是ViewGroup,ViewDragHelper *并不是直接作用于要被拖动的View,而是使其控制的视图容器中的子View可以被拖动* sensitivity 敏感参数,一般都指定1.0f 表示正常速率* Callback 回调类,ViewDragHelper 在处理 touchevent 事件后,回调这个类的方法*/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

public MyDragLayout(Context context) {  this(context, null);}public MyDragLayout(Context context, AttributeSet attrs) {  super(context, attrs, defStyle);  //DragHelperCallback 是我们继承抽象接口Callback的一个实现类  mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());}

ViewGroup区别于View的一个特点是,它有一个 onInterceptTouchEvent(MotionEvent ev) 方法,事件最先传递给这个方法,如果返回值返回 true,表示自己拦截 这一整个事件序列,不向子View进行分发,自己处理,即调用自己的onTouchEvent方法,我们需要再这个方法中,调用 ViewDragHelpershouldInterceptTouchEvent() 方法,让他处理是否拦截事件。

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

同样,在 onTouchEvnet 中,也应该将事件交给 ViewDragHelper 处理

@Override    public boolean onTouchEvent(MotionEvent event) {        mDragHelper.processTouchEvent(event);        return true;    }

先不去了解 ViewDragHelpe 对 TouchEvent 的处理流程,先看一下,ViewDragHelper.Callback 为我们提供了哪些回调方法

    public static abstract class Callback {        /**         * 在拖拽状态改变的时候调用.         * @param state 新的拖拽状态         *         * @see #STATE_IDLE 处于空闲状态         * @see #STATE_DRAGGING 拖拽中         * @see #STATE_SETTLING 自动沉落         */        public void onViewDragStateChanged(int state) {}        /**         * 在 一个子 View 的位置改变的时候调用         *         * @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         * @param dy Change in Y position from the last call         */        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}        /**         * 当 captured View 被捕获的时候调用         * Called when a child view is captured for dragging or settling. The ID of the pointer         * currently dragging the captured view is supplied. If activePointerId is         * identified as {@link #INVALID_POINTER} the capture is programmatic instead of         * pointer-initiated.         *         * @param capturedChild Child view that was captured         * @param activePointerId Pointer id tracking the child capture         */        public void onViewCaptured(View capturedChild, int activePointerId) {}        /**         * 当View 被释放,其实也就是 手指抬起的时候调用         * xvel yvel 分别表示 x y方向上拖动的 px/每秒         * Called when the child view is no longer being actively dragged.         * The fling velocity is also supplied, if relevant. The velocity values may         * be clamped to system minimums or maximums.         *         * <p>Calling code may decide to fling or otherwise release the view to let it         * settle into place. It should do so using {@link #settleCapturedViewAt(int, int)}         * or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes         * one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING}         * and the view capture will not fully end until it comes to a complete stop.         * If neither of these methods is invoked before <code>onViewReleased</code> returns,         * the view will stop in place and the ViewDragHelper will return to         * {@link #STATE_IDLE}.</p>         *         * @param releasedChild The captured child view now being released         * @param xvel X velocity of the pointer as it left the screen in pixels per second.         * @param yvel Y velocity of the pointer as it left the screen in pixels per second.         */        public void onViewReleased(View releasedChild, float xvel, float yvel) {}        /**         * 当触摸到边界时回调         * Called when one of the subscribed edges in the parent view has been touched         * by the user while no child view is currently captured.         *         * @param edgeFlags A combination of edge flags describing the edge(s) currently touched         * @param pointerId ID of the pointer touching the described edge(s)         * @see #EDGE_LEFT         * @see #EDGE_TOP         * @see #EDGE_RIGHT         * @see #EDGE_BOTTOM         */        public void onEdgeTouched(int edgeFlags, int pointerId) {}        /**        *  返回true的时候会锁住当前的边界,false则unLock。         * Called when the given edge may become locked. This can happen if an edge drag         * was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)}         * was called. This method should return true to lock this edge or false to leave it         * unlocked. The default behavior is to leave edges unlocked.         *         * @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;        }        /**         *  在边界拖动时回调         * Called when the user has started a deliberate drag away from one         * of the subscribed edges in the parent view while no child view is currently captured.         *         * @param edgeFlags A combination of edge flags describing the edge(s) dragged         * @param pointerId ID of the pointer touching the described edge(s)         * @see #EDGE_LEFT         * @see #EDGE_TOP         * @see #EDGE_RIGHT         * @see #EDGE_BOTTOM         */        public void onEdgeDragStarted(int edgeFlags, int pointerId) {}        /**         * 改变同一个坐标(x,y)去寻找captureView位置的方法。         * Called to determine the Z-order of child views.         *         * @param index the ordered position to query for         * @return index of the view that should be ordered at position <code>index</code>         */        public int getOrderedChildIndex(int index) {            return index;        }        /**         * 返回值表示这个child View 竖向可以 拖拽的范围,0表示不能拖拽         * Return the magnitude of a draggable child view's horizontal range of motion in pixels.         * This method should return 0 for views that cannot move horizontally.         *         * @param child Child view to check         * @return range of horizontal motion in pixels         */        public int getViewHorizontalDragRange(View child) {            return 0;        }        /**         * Return the magnitude of a draggable child view's vertical range of motion in pixels.         * This method should return 0 for views that cannot move vertically.         *         * @param child Child view to check         * @return range of vertical motion in pixels         */        public int getViewVerticalDragRange(View child) {            return 0;        }        /**         *          * @param 拖拽点击的child view         * @param pointerId ID of the pointer attempting the capture         * @return true 表示可以捕获这个子view;即可以被拖拽,false 不运行         */        public abstract boolean tryCaptureView(View child, int pointerId);        /**         * 返回值表示child View left的位置,一般直接返回left         * Restrict the motion of the dragged child view along the horizontal axis.         * The default implementation does not allow horizontal motion; the extending         * class must override this method and provide the desired clamping.         *         *         * @param 拖拽的子View         * @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;        }        /**         * Restrict the motion of the dragged child view along the vertical axis.         * The default implementation does not allow vertical motion; the extending         * class must override this method and provide the desired clamping.         *         *         * @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;        }    }

那么如何让子view 可拖拽呢,简单的实现3个回调方法

class DragHelperCallback extends ViewDragHelper.Callback {        @Override        public boolean tryCaptureView(View child, int pointerId) {            return true;        }        //返回值表示view 新的left        @Override        public int clampViewPositionHorizontal(View child, int left, int dx) {            return left;        }        @Override        public int clampViewPositionVertical(View child, int top, int dy) {            return top;        }    }

  这样实现的时候,我们发现了一个问题,view的拖拽可以超出父view的边界,这时候clampViewPostion.. 方法就不能简单的返回 left 了

class DragHelperCallback extends ViewDragHelper.Callback {        @Override        public boolean tryCaptureView(View child, int pointerId) {            return true;        }        @Override        public int clampViewPositionHorizontal(View child, int left, int dx) {            //可能我们给自定义的layout 设置了padding属性            final int leftBound = getPaddingLeft();            //left 的 最大界限 是布局的宽 - padding - 拖拽view的宽            final int rightBound = getWidth() - leftBound -child.getWidth();            //系统帮我们判断的left 和 我们的margin(margin可能为负) 取最大值,在和 最大界限取最小值            return Math.min(Math.max(leftBound,left),rightBound);        }        @Override        public int clampViewPositionVertical(View child, int top, int dy) {            final int topBound = getPaddingTop();            final int bottomBound = getHeight() - topBound -child.getHeight();            return Math.min(Math.max(topBound,top),bottomBound);        }    }

实现拖拽释放后的自动滚动(拖拽)功能

// 如果我们应该在这个方法中        // 调用settleCapturedViewAt(int, int) or flingCapturedView(int, int, int, int).        // 如果调用了其中一个 ViewDragHelper会进入 STATE_SETTLING 状态        // 如果没有 会进入STATE_IDLE 状态        @Override        public void onViewReleased(View releasedChild, float xvel, float yvel) {            mDragHelper.settleCapturedViewAt(200,200);            //不要忘记调用invalidate(),            invalidate();        }

拖拽后的移动用的是scroller 实现的 ,所以重写 layout 的 computeScroll 方法

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

ViewDragHelper还有一个移动View的方法是

smoothSlideViewTo(View child, int finalLeft, int finalTop)

可以在任何地方调用,效果和settleCapturedViewAt()类似,因为它们最终都调用了forceSettleCapturedViewAt()来启动自动滚动,区别在于settleCapturedViewAt()会以最后松手前的滑动速率为初速度将View滚动到最终位置,而smoothSlideViewTo()滚动的初速度是0。forceSettleCapturedViewAt()里有地方调用了Callback里的方法,所以再来看看这个方法:

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

可以看到自动滑动是靠Scroll类完成,在这里生成了调用mScroller.startScroll()需要的参数。再来看看计算滚动时间的方法computeSettleDuration():

private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {    xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);    yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);    final int absDx = Math.abs(dx);    final int absDy = Math.abs(dy);    final int absXVel = Math.abs(xvel);    final int absYVel = Math.abs(yvel);    final int addedVel = absXVel + absYVel;    final int addedDistance = absDx + absDy;    final float xweight = xvel != 0 ? (float) absXVel / addedVel :            (float) absDx / addedDistance;    final float yweight = yvel != 0 ? (float) absYVel / addedVel :            (float) absDy / addedDistance;    int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));    int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child));    return (int) (xduration * xweight + yduration * yweight);}

clampMag()方法确保参数中给定的速率在正常范围之内。最终的滚动时间还要经过computeAxisDuration()算出来,通过它的参数可以看到最终的滚动时间是由dx、xvel、mCallback.getViewHorizontalDragRange()共同影响的。看computeAxisDuration():

private int computeAxisDuration(int delta, int velocity, int motionRange) {    if (delta == 0) {        return 0;    }    final int width = mParentView.getWidth();    final int halfWidth = width / 2;    final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);    final float distance = halfWidth + halfWidth *            distanceInfluenceForSnapDuration(distanceRatio);    int duration;    velocity = Math.abs(velocity);    if (velocity > 0) {        duration = 4 * Math.round(1000 * Math.abs(distance / velocity));    } else {        final float range = (float) Math.abs(delta) / motionRange;        duration = (int) ((range + 1) * BASE_SETTLE_DURATION);    }    return Math.min(duration, MAX_SETTLE_DURATION);}

直接看14~19行,如果给定的速率velocity不为0,就通过距离除以速率来算出时间;如果velocity为0,就通过要滑动的距离(delta)除以总的移动范围(motionRange,就是Callback里getViewHorizontalDragRange()、getViewVerticalDragRange()返回值)来算出时间。最后还会对计算出的时间做过滤,最终时间反正是不会超过MAX_SETTLE_DURATION的,源码里的取值是600毫秒,所以不用担心在Callback里getViewHorizontalDragRange()、getViewVerticalDragRange()返回错误的数而导致自动滚动时间过长了。

在调用settleCapturedViewAt()、flingCapturedView()和smoothSlideViewTo()时,还需要实现mParentView的computeScroll():

@Overridepublic void computeScroll() {    if (mDragHelper.continueSettling(true)) {        ViewCompat.postInvalidateOnAnimation(this);    }}
1 0
原创粉丝点击