ViewDragHelper原理解读

一.拖拽滑动

其中拖住滑动是通过dragTo方法来实现的

private void dragTo(int left, int top, int dx, int dy) {
        int clampedX = left;
        int clampedY = top;
        final int oldLeft = mCapturedView.getLeft();
        final int oldTop = mCapturedView.getTop();
        if (dx != 0) {
            clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
            ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
        }
        if (dy != 0) {
            clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
            ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
        }

        if (dx != 0 || dy != 0) {
            final int clampedDx = clampedX - oldLeft;
            final int clampedDy = clampedY - oldTop;
            mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
                    clampedDx, clampedDy);
        }
    }

可以看到最终是通过ViewCompat.offsetLeftAndRight()ViewCompat.offsetTopAndBottom()来实现左右、上下拖动的。

二.自动滑动

ViewDragHelper中有两种方式可以实现子View自动滑动到某个位置,分别是

public boolean smoothSlideViewTo(@NonNull View child, int finalLeft, int finalTop) {
        **********
}
public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
        **********
}

通过代码可以看到它们都会调用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;
}

可以看到最终滑动是通过OverScroller来实现的,我们知道Scroller是需配合在View的computeScroll()方法中不断调用invalidate(),从而不断重新绘制视图位置达到视图自动滑动的效果的,所以我们使用ViewDragHelper时需要重写自定义View的computeScroll()方法,如

@Override
public void computeScroll() {
    super.computeScroll();
    if (mDragHelper != null && mDragHelper.continueSettling(true)) {
        invalidate();
    }
}

ViewDragHelper中还有一个方法可以实现在拖拽滑动松手后进行惯性滑动,最终停留位置由松手时的位置和速度来决定,它和上面的settleCapturedViewAt()都必须在mReleaseInProgresstrue的时候才能生效,也就是必须在CallbackonViewReleased()方法中调用。它也是通过调用OverScroller,所以使用时同样需重写自定义View的computeScroll()方法。

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) mVelocityTracker.getXVelocity(mActivePointerId),
            (int) mVelocityTracker.getYVelocity(mActivePointerId),
            minLeft, maxLeft, minTop, maxTop);

    setDragState(STATE_SETTLING);
}

三.边缘滑动

/**
 * Enable edge tracking for the selected edges of the parent view.
 * The callback's {@link Callback#onEdgeTouched(int, int)} and
 * {@link Callback#onEdgeDragStarted(int, int)} methods will only be invoked
 * for edges for which edge tracking has been enabled.
 *
 * @param edgeFlags Combination of edge flags describing the edges to watch
 * @see #EDGE_LEFT
 * @see #EDGE_TOP
 * @see #EDGE_RIGHT
 * @see #EDGE_BOTTOM
 */
public void setEdgeTrackingEnabled(int edgeFlags) {
    mTrackingEdges = edgeFlags;
}

ViewDragHelper通过setEdgeTrackingEnabled()方法设置要监测哪个方向屏幕边缘的滑动,而是否是边缘滑动是通过对触摸事件中Action_Down时的位置进行判令的

    public boolean shouldInterceptTouchEvent(@NonNull MotionEvent ev) {
        **********
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                final float x = ev.getX();
                final float y = ev.getY();
                final int pointerId = ev.getPointerId(0);
                saveInitialMotion(x, y, pointerId);

               **********

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

            case MotionEvent.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) {
                    final int edgesTouched = mInitialEdgesTouched[pointerId];
                    if ((edgesTouched & mTrackingEdges) != 0) {
                        mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
                    }
                } else if (mDragState == STATE_SETTLING) {
                   **********
                }
                break;
            }

            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];

                    **********
                    reportNewEdgeDrags(dx, dy, pointerId);
                    **********
                }
                saveLastMotion(ev);
                break;
            }

            **********
        }

        return mDragState == STATE_DRAGGING;
    }
    public void processTouchEvent(@NonNull MotionEvent ev) {
        **********
        switch (action) {
            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);

               **********

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

            case MotionEvent.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) {
                    **********
                    final int edgesTouched = mInitialEdgesTouched[pointerId];
                    if ((edgesTouched & mTrackingEdges) != 0) {
                        mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
                    }
                } else if (isCapturedViewUnder((int) x, (int) y)) {
                    **********
                }
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                if (mDragState == STATE_DRAGGING) {
                    **********
                } 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);
                        **********
                    }
                    saveLastMotion(ev);
                }
                break;
            }

            **********
        }
    }

其中saveInitialMotion()方法如下

private void saveInitialMotion(float x, float y, int pointerId) {
    ensureMotionHistorySizeForId(pointerId);
    mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
    mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
    mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
    mPointersDown |= 1 << pointerId;
}
private int getEdgesTouched(int x, int y) {
    int result = 0;

    if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT;
    if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP;
    if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT;
    if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM;

    return result;
}

可以看出ACTION_DOWN时会记录触摸点的x,y坐标位置,手指id,以及当前手指是否是触摸在屏幕某一个边缘,具体是通过当前触摸点x或y是否在边缘一个很小的距离范围来来判定。如果是边缘触摸并且方向和setEdgeTrackingEnabled()方法设置要监测的方向一致,则会回调CallbackonEdgeTouched()方法。

通过上面的ACTION_MOVE中代码看到手指滑动过程中,会调用reportNewEdgeDrags()方法来对边缘滑动做进一步的处理

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);
    }
}
private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
    final float absDelta = Math.abs(delta);
    final float absODelta = Math.abs(odelta);

    if ((mInitialEdgesTouched[pointerId] & edge) != edge  || (mTrackingEdges & edge) == 0
            || (mEdgeDragsLocked[pointerId] & edge) == edge
            || (mEdgeDragsInProgress[pointerId] & edge) == edge
            || (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
        return false;
    }
    if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {
        mEdgeDragsLocked[pointerId] |= edge;
        return false;
    }
    return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
}

可以看到会通过mInitialEdgesTouched中记录的该手指最开始Action_down的时候是否是边缘触摸,要监测的是否是该方向,该边缘方向是否是锁定状态,以及是否是Action_down后首次该边缘方向的ACTION_MOVE触发多个条件来进行判定,如果判定成功则会调用CallbackonEdgeDragStarted()方法。

其中某个边缘方向是否是锁定状态通过CallbackonEdgeLock()方法来获取,我们可以在这个回调中动态改变返回值来动态设置是否可以触发onEdgeDragStarted()方法,而真正要实现边缘触摸的时候可以拖拽出某个view(比如某一个下拉菜单)还需要在onEdgeDragStarted()中做其他处理,如

ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
      **********
        @Override
        public void onEdgeTouched(int edgeFlags, int pointerId) {
        super.onEdgeTouched(edgeFlags, pointerId);
      }

      @Override
      public boolean onEdgeLock(int edgeFlags) {
        return super.onEdgeLock(edgeFlags);
      }

      @Override
      public void onEdgeDragStarted(int edgeFlags, int pointerId) {
        mDragHelper.captureChildView(mBottomMenu, pointerId);
      }
      **********
};

参考链接:
ViewDragHelper 的基本使用(一)
ViewDragHelper原理与使用
ViewDragHelper源码解析
Android ViewDragHelper源码解析

你可能感兴趣的:(ViewDragHelper原理解读)