ViewDragHelper(二)— 源码解析(进阶篇)

声明:本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

本篇为该系列的第二篇,侧重讲解ViewDragHelper 的实现原理和源码逻辑,以及它所提供的Callback。

目录

ViewDragHelper 的介绍以及初步使用请阅读这篇:
ViewDragHelper (一)- 介绍及简单用例(入门篇)
ViewDragHelper 的源码以及Callback的详情介绍请阅读这篇:
ViewDragHelper (二)- 源码及原理解读(进阶篇)
利用DrageHelper 打造仿陌陌APP视频播放页的demo请阅读这篇:
ViewDragHelper (三)- 打造仿陌陌视频播放页(深入篇)

一、 UML 类图及流程图

1.1 ViewDragHelper的UML类图如下所示:

图1-1. UML类图

在使用ViewDragHelper过程中,主要涉及到如下四个类:

  • MyDraggableView
    我们自定义的ViewGroup类。
  • ViewDragHelper
    帮助类,是我们本篇文章主要分析的对象。
  • Callback
    ViewDragHelper的内部抽象静态类,主要用于事件处理结果的回调及事件监听。
  • DraggableViewCallback
    继承于Callback,是它的实现类,ViewDragHelper里面处理的事件,我们可以通过该实现类进行监听回调。

1.2 ViewDragHelper的事件流程图如下所示:

图1-2. MotionEvent事件流程图

MotionEvent事件是从上往下传递的,如果其中的一个onInterceptTouchEvent返回了true,则表示该View拦截此事件系列,此后的MOVE,UP都不会再调用onInterceptTouchEvent,而是会直接调用自己的onTouchEvent方法。

第一篇文章里面提及的,我们自定义的ViewGroup控件的 onInterceptTouchEvent 方法,是通过 viewDragHelper.shouldInterceptTouchEvent(ev) 方法的返回值来决定是否拦截,当它返回 true 时,会直接触发该类自己的onTouchEvent方法;在onTouchEvent事件里面通过viewDragHelper 的 processTouchEvent(ev) 方法,将MotionEvent传递给viewDragHelper 内部,让viewDragHelper 对事件进行分析处理。以上就是在使用viewDragHelper时,事件分发的大概流程以及它的处理过程了,接下来将分析我们在onTouch 方法里将事件传递给viewDragHelper之后 ,它内部是如何对事件进行分析处理的。

本文由于篇幅关系,重点讲解的是以下几个部分:

  1. 抽象内部静态类 ViewDrageHelper .Callback。
  2. ViewDrageHelper 内部部分源码逻辑。
  3. VelocityTracker。
  4. ScrollerCompat。

二、ViewDragHelper源码

由UML类图我们不难看出,ViewDragHelper 是在我们自定义ViewGroup类的构造方法中初始化的,而Callback 是一个ViewDrageHelper 的内部静态抽象类。在创建ViewDragHelper 对象时,我们需要传入一个继承自Callback 的实现类实例对象进去。下面我们一步一步来剖析它的内部逻辑。

2.1 构造器

 /**
     * Apps should use ViewDragHelper.create() to get a new instance.
     * This will allow VDH to use internal compatibility implementations for different
     * platform versions.
     *
     * @param context Context to initialize config-dependent params from
     * @param forParent Parent view to monitor
     */
    private ViewDragHelper(Context context, ViewGroup forParent, Callback cb)

由以上源码我们看到,它的构造器是私有的,也就是说我们并不能直接在外部通过new ViewDragHelper()的方式来创建对象。那么我们需要如何创建一个新的ViewDragHelper对象呢?不急,我们接着往下看。

2.2 创建对象

我们贴上关于创建对象以及初始化相关的完整源代码,其实,通过构造方法上面的英文注释可以知道,Google提供了两个工厂方法,让开发者去创建一个新的ViewDragHelper对象。如下所示:

  1. create(ViewGroup forParent, Callback cb)
    该方法在return 时,利用构造器创建了一个新的ViewDragHelper实例。

  2. create(ViewGroup forParent, float sensitivity, Callback cb)
    该方法内部,先调用了第一个工厂方法,得到新ViewDragHelper实例,之后又初始化了 mTouchSlop、mMaxVelocity 、mMinVelocity 、mScroller 等数据和对象。

不难看出含有sensitivity 这个参数的create方法,内部也是调用了create(forParent, cb)方法,只是它对mTouchSlop做了一下处理,传入的灵敏度(sensitivity值)越大,mTouchSlop的值越小。假设当前手机的系统mTouchSlop 大小为24dp, 若我们传入的sensitivity = 3.0f ,则mTouchSlop = 8 dp,即单次滑动距离超过8dp,就会触发系统的 MOVE事件。它的源码如下:


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

    /**
     * Apps should use ViewDragHelper.create() to get a new instance.
     * This will allow VDH to use internal compatibility implementations for different
     * platform versions.
     *
     * @param context Context to initialize config-dependent params from
     * @param forParent Parent view to monitor
     */
    private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
        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);
    }

2.3 滑动相关

smoothSlideViewTo方法

该方法用于平顺地滑动控件到指定位置。 child代表子控件对象, finalLeft代表滑动结束时,子控件左边所处的位置, finalTop 代表子控件顶部的位置。

那么,smoothSlideViewTo方法内部做了哪些操作呢?下面我们来看一看源代码:

    public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
        mCapturedView = child;
        mActivePointerId = INVALID_POINTER;

        boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
        if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) {
            mCapturedView = null;
        }

        return continueSliding;
    }

我们可以看到,它是一个布尔型的方法,如果此方法返回 true,则我们应该调用continueSettling方法让它继续滑动,直到返回false,这次滑动才算完成。

settleCapturedViewAt方法

该方法是以松手前的滑动速度为初值,让捕获到的子View自动滑动到指定位置,它只能在Callback的onViewReleased()中使用,若mReleaseInProgress不为True,则会抛出IllegalStateException异常。传递的两个参数分别是结束时子控件的位置,其内部最终调用的是forceSettleCapturedViewAt 方法。

    public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
        if (!mReleaseInProgress) {
            throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to "
                    + "Callback#onViewReleased");
        }

        return forceSettleCapturedViewAt(finalLeft, finalTop,
                (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
                (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId));
    }
forceSettleCapturedViewAt 方法
    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;
    }

由以上可看出,最终它是交给Scroller去处理滑动的,并且,滑动的时长是通过computeSettleDuration方法计算得到。那么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);
    }

通过上面的一系列计算过后,得到的就是自动滑动所需的时间(毫秒)。

2.4 MotionEvent 相关

processTouchEvent 方法

若ViewDragHelper接受并处理父控件传递过来的触摸事件,则该方法内部会分析MotionEvent 事件,并根据需要,触发监听回调事件。需要强调的是:父控件的onTouchEvent实现方法需要调用processTouchEvent 方法,才能将事件传递给ViewDragHelper让其分析处理。

我们阅读其源码发现,首先,它做了如下操作:

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

        if (action == MotionEvent.ACTION_DOWN) {
            // Reset things for a new event stream, just in case we didn't get
            // the whole previous stream.
            cancel();
        }

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);
        ...
}

很显然,在ACTION_DOWN 即手指开始按下时,调用cancel方法重置了一下状态,以防以没有得到当前事件序列的完整事件输入流,而导致出错。

紧接着,若mVelocityTracker(速度跟踪器)对象为空,则通过VelocityTracker 的内部静态方法obtain 来创建一个新的对象,并通过addMovement将触摸事件添加监听,用于捕获用户手指滑动屏幕的速度。

然后通过switch 语句处理各种类型的ACTION事件,具体如下:

ACTION_DOWN 事件:

case MotionEvent.ACTION_DOWN: {
                final float x = ev.getX();
                final float y = ev.getY();
                final int pointerId = ev.getPointerId(0);
                final View toCapture = findTopChildUnder((int) x, (int) y);

                saveInitialMotion(x, y, pointerId);

                // Since the parent is already directly processing this touch event,
                // there is no reason to delay for a slop before dragging.
                // Start immediately if possible.
                tryCaptureViewForDrag(toCapture, pointerId);

                final int edgesTouched = mInitialEdgesTouched[pointerId];
                if ((edgesTouched & mTrackingEdges) != 0) {
                    mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
                }
                break;
            }

ACTION_DOWN是在第一个手指按下时触发,ViewDragHelper内部做了如下操作:

  1. 保存初始化x、y位置及pointerId。
  2. 调用tryCaptureViewForDrag 方法。直接回调true,因为父控件已经处理了ACTION_DOWN 事件。
  3. 若按下区域是在边缘,则触发onEdgeTouched 回调。

ACTION_POINTER_DOWN 事件:

case MotionEventCompat.ACTION_POINTER_DOWN: {
                final int pointerId = ev.getPointerId(actionIndex);
                final float x = ev.getX(actionIndex);
                final float y = ev.getY(actionIndex);

                saveInitialMotion(x, y, pointerId);

                // A ViewDragHelper can only manipulate one view at a time.
                if (mDragState == STATE_IDLE) {
                    // If we're idle we can do anything! Treat it like a normal down event.

                    final View toCapture = findTopChildUnder((int) x, (int) y);
                    tryCaptureViewForDrag(toCapture, pointerId);

                    final int edgesTouched = mInitialEdgesTouched[pointerId];
                    if ((edgesTouched & mTrackingEdges) != 0) {
                        mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
                    }
                } else if (isCapturedViewUnder((int) x, (int) y)) {
                    // We're still tracking a captured view. If the same view is under this
                    // point, we'll swap to controlling it with this pointer instead.
                    // (This will still work if we're "catching" a settling view.)

                    tryCaptureViewForDrag(mCapturedView, pointerId);
                }
                break;
            }

由以上源码我们可以看出:

  1. 若mDragState 状态 为STATE_IDLE ,即处于闲置状态,则处理逻辑同ACTION_DOWN。
  2. 否则 直接调用tryCaptureViewForDrag 处理拖拽动作。

ACTION_MOVE 事件:

            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;
            }
  1. 如果 mDragState 状态为 STATE_DRAGGING,即拖拽状态。判断pointerId是否为无效id, 是则跳过。
  2. 获取触摸的x、y 位置,并调用dragTo 处理拖拽事件,然后调用saveLastMotion保存一下当前Motion。
  3. 若mDragState 状态不是 STATE_DRAGGING,则检查一遍 pointerId列表,看是否有Id处于可拖动状态并进行处理。

ACTION_POINTER_UP 事件:

 case MotionEventCompat.ACTION_POINTER_UP: {
                final int pointerId = ev.getPointerId(actionIndex);
                if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
                    // Try to find another pointer that's still holding on to the captured view.
                    int newActivePointer = INVALID_POINTER;
                    final int pointerCount = ev.getPointerCount();
                    for (int i = 0; i < pointerCount; i++) {
                        final int id = ev.getPointerId(i);
                        if (id == mActivePointerId) {
                            // This one's going away, skip.
                            continue;
                        }

                        final float x = ev.getX(i);
                        final float y = ev.getY(i);
                        if (findTopChildUnder((int) x, (int) y) == mCapturedView
                                && tryCaptureViewForDrag(mCapturedView, id)) {
                            newActivePointer = mActivePointerId;
                            break;
                        }
                    }

                    if (newActivePointer == INVALID_POINTER) {
                        // We didn't find another pointer still touching the view, release it.
                        releaseViewForPointerUp();
                    }
                }
                clearMotionHistory(pointerId);
                break;
            }

如果mDragState 状态为 STATE_DRAGGING ,并且 pointerId 为当前行动的Id,则遍历一次pointerId 列表并进行处理,最后调用clearMotionHistory清除事件的历史记录。

ACTION_UP 事件:

  case MotionEvent.ACTION_UP: {
                if (mDragState == STATE_DRAGGING) {
                    releaseViewForPointerUp();
                }
                cancel();
                break;
            }

如果 mDragState 状态为 STATE_DRAGGING, 则调用releaseViewForPointerUp方法,该方法会计算当前滑动速度,并调用dispatchViewReleased方法,计算松开手指时的X、Y轴的速度,并通过mCallback的onViewReleased方法回调出去。然后调用cancel重置状态。

ACTION_CANCEL 事件:

  case MotionEvent.ACTION_CANCEL: {
                if (mDragState == STATE_DRAGGING) {
                    dispatchViewReleased(0, 0);
                }
                cancel();
                break;
            }

如果mDragState 状态为 STATE_DRAGGING,则直接调用dispatchViewReleased方法,传递的初始X、Y轴速度为0;然后调用cancel重置状态。

三、ViewDragHelper.Callback 部分解读

以上介绍了ViewDragHelper 类内部对MotionEvent事件处理的逻辑,那么它在处理完成后,是如何通知ViewGroup的呢? 很明显,ViewDragHelper 的静态内部抽象类Callback ,它的职责就是将触发的事件及结果返回给ViewGroup的。前面我们已经讲过了,我们在创建ViewDragHelper的过程中,需要实例化一个继承自ViewDragHelper.Callback的实现类,并将这个实现类的实例对象传入了ViewDragHelper,因此ViewDragHelper通过create方法传递进来的参数,持有实现类的对象实例。

在我们的实现类 DraggableViewCallback 中,我们可根据需求来覆盖父类Callback所提供的方法以实现相关监听。其中,抽象类Callback的抽象方法: tryCaptureView() 是必须要在DraggableViewCallback 中实现的。

首先我们看这个抽象内部静态类的完整源代码:

  public abstract static 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
         * @param dy Change in Y position from the last call
         */
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}

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

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

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

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

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

        /**
         * Called when the user's input indicates that they want to capture the given child view
         * with the pointer indicated by pointerId. The callback should return true if the user
         * is permitted to drag the given view with the indicated pointer.
         *
         * <p>ViewDragHelper may call this method multiple times for the same view even if
         * the view is already captured; this indicates that a new pointer is trying to take
         * control of the view.</p>
         *
         * <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)}
         * will follow if the capture is successful.</p>
         *
         * @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);

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

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

英文水平不赖的朋友也可以直接阅读英文源码注释,下面是我对这些方法的一些个人理解及总结,用中文写出来以方便快速阅读:

onViewDragStateChanged(int state) 方法

 当View的拖拽状态改变时,回调该方法。state有三种状态:
 STATE_IDLE = 0    当前处于闲置状态
 STATE_DRAGGING = 1   正在被拖拽的状态
 STATE_SETTLING = 2   拖拽后被安放到一个位置中的状态

onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 方法

 View被拖拽,位置发生改变时回调
 changedView :被拖拽的View
 left : 被拖拽后 View的 left 坐标
 top : 被拖拽后 View的 top 坐标
 dx :  拖动的x偏移量
 dy :  拖动的y偏移量

public void onViewCaptured(View capturedChild, int activePointerId) 方法

  当子控件被捕获到准备开始拖动时回调
  capturedChild : 捕获的View
  activePointerId : 对应的PointerId

public void onViewReleased(View releasedChild, float xvel, float yvel) 方法

   当被捕获拖拽的View被释放时回调
   releasedChild : 被释放的View
   xvel : 释放View的x方向上的加速度
   yvel : 释放View的y方向上的加速度

public void onEdgeTouched(int edgeFlags, int pointerId) 方法

   如果parentView订阅了边缘触摸,则如果有边缘触摸就回调的接口
   edgeFlags : 当前触摸的flag 有: EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM
   pointerId : 用来描述边缘触摸操作的id

public boolean onEdgeLock(int edgeFlags) 方法

是否锁定该边缘的触摸,默认返回false,返回true表示锁定

public void onEdgeDragStarted(int edgeFlags, int pointerId)

边缘触摸开始时回调
edgeFlags : 当前触摸的flag 有: EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM
pointerId : 用来描述边缘触摸操作的id

public int getOrderedChildIndex(int index)

 在寻找当前触摸点下的子View时会调用此方法,寻找到的View会提供给tryCaptureViewForDrag()来尝试捕获。
 如果需要改变子View的遍历查询顺序可改写此方法,例如让下层的View优先于上层的View被选中。

public int getViewHorizontalDragRange(View child)

获取被拖拽View child 的水平拖拽范围,返回0表示无法被水平拖拽

public int getViewVerticalDragRange(View child)

获取被拖拽View child 的竖直拖拽范围,返回0表示无法被竖直拖拽

public abstract boolean tryCaptureView(View child, int pointerId);

是否捕获被拖拽的子View,child 为被触摸的子控件, 返回 true则表示允许拖拽,返回false则表示禁止。

public int clampViewPositionHorizontal(View child, int left, int dx)

该方法决定被拖拽的View在水平方向上应该移动到的位置。
child : 被拖拽的View
left : 期望移动到位置的View的left值
dx : 移动的水平距离
返回值 : 直接决定View在水平方向的位置

public int clampViewPositionVertical(View child, int top, int dy)

该方法决定被拖拽的View在垂直方向上应该移动到的位置。
child : 被拖拽的View
top : 期望移动到位置的View的top值
dy : 移动的垂直距离
返回值 : 直接决定View在垂直方向的位置

四、VelocityTracker

VelocityTracker 它是一个跟踪触摸事件速度的帮助类,可以实现flinging(快速滑动)或者其他类似这样的手势。通过 obtain方法来创建一个新实例。它所提供的方法有如下几个:

addMovement ():捕获某个MotionEvent 的速度。
recycle (): 将该对象回收,并且在调用该方法之后就不能再调用它。
clear (): 重置VelocityTracker 对象恢复到初始状态。
computeCurrentVelocity ():计算当前速度。
getXVelocity (): 获取 X方向的速度。
getYVelocity (): 获取 Y方向的速度。

在ViewDragHelper 类中,它被使用的地方有如下几处:

  1. shouldInterceptTouchEvent 方法里面 对它有进行初始化,并调用addMovement方法将事件添加进去。
  2. processTouchEvent 方法里面 对它有进行初始化,并调用addMovement方法将事件添加进去。
  3. flingCapturedView方法里,调用mScroller 的 fling 方法,用到了它。
  4. settleCapturedViewAt 方法里 return 调用forceSettleCapturedViewAt 方法时,传入了mVelocityTracker,用于捕获手指离开屏幕的那一刻X、Y方向的滑动速度。
  5. cancel方法里,调用了VelocityTracker.recycle() 方法并且重置对象为null。

五、ScrollerCompat

ScrollerCompat是一个实现View平滑滚动的Helper类。从ScrollerCompat的源码我们可以看出,它其实就是封装了OverScroller。ScrollerCompat类的内部截图如下:

图片.png

事实上,我们常用的ScrollView,它内部也是通过OverScroller 来实现的。有图有真相:

图片.png

说到OverScroller,我们可能立马会想起Scroller,那么OverScroller和Scroller有什么区别呢?

事实上,这两个类它都属于Scrollers,Scroller属于早期的API,在API 11所提供的。而OverScroller是在API 19才新增的。翻阅他们内部源码我们不难看出,这两个类大部分的API是一致的。从字面上我们可以看出,Over的意思就是超出,即OverScroller提供了对超出滑动边界情况的处理逻辑,OverScroller的功能及逻辑相对而言比较完善。关于ScrollerCompat、Scroller、OverScroller的解读,大家有兴趣可自行查阅相关资料,这里就不作深入讨论了。

ScrollerCompat 在 ViewDragHelper 类中使用到的地方有如下几处:

1. abort () 方法

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

不难看出,该方法主要利用mScroller来获取当前X、Y位置以及动画终止后的X、Y位置。并通过onViewPositionChanged 回调外部newX、newY,以及dx、dy。

2. forceSettleCapturedViewAt () 方法

 final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
 mScroller.startScroll(startLeft, startTop, dx, dy, duration);

主要用于平顺滑动处理,duration 时长取决于初始速度及终点距离长短。

3. flingCapturedView() 方法

 public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {
        if (!mReleaseInProgress) {
            throw new IllegalStateException("Cannot flingCapturedView outside of a call to "
                    + "Callback#onViewReleased");
        }

        mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(),
                (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
                (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
                minLeft, maxLeft, minTop, maxTop);

        setDragState(STATE_SETTLING);
    }

可以看出其实就是对 mScroller.fling () 方法的封装。

4. continueSettling() 方法

该方法主要利用mScroller 获取当前位置CurrX、CurrY,以及最终滑动停留的位置FinalX、FinalY。然后处理动画,生成惯性滑动的效果。

总结

到此ViewDragHelper的源码就解析完了,我们由此可知,ViewDragHelper本质上是对MotionEvent的分析及处理,并提供了一系列的监听回调方法,来帮助我们减轻开发负担,更为方便地处理控件的滑动拖拽逻辑。总而言之,深入阅读源码,过程虽然会有点辛苦,但理解程度会有很大的提升~ 有兴趣的朋友可自行查看它的源代码,第三篇将会是深入实战篇。后续有时间会陆续写好分享出来。感谢支持~ 希望能帮助到有需要的人。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269