View滚动帮助类ViewDragHelper解析

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():

@Override
public void computeScroll() {
    if (mDragHelper.continueSettling(true)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卓修武

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值