ViewDragHelper原理解讀

一.拖拽滑動

其中拖住滑動是通過dragTo方法來實現(xiàn)的

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()來實現(xiàn)左右、上下拖動的。

二.自動滑動

ViewDragHelper中有兩種方式可以實現(xiàn)子View自動滑動到某個位置,分別是

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

通過代碼可以看到它們都會調(diào)用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來實現(xiàn)的,我們知道Scroller是需配合在View的computeScroll()方法中不斷調(diào)用invalidate(),從而不斷重新繪制視圖位置達到視圖自動滑動的效果的,所以我們使用ViewDragHelper時需要重寫自定義View的computeScroll()方法,如

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

ViewDragHelper中還有一個方法可以實現(xiàn)在拖拽滑動松手后進行慣性滑動,最終停留位置由松手時的位置和速度來決定,它和上面的settleCapturedViewAt()都必須在mReleaseInProgresstrue的時候才能生效,也就是必須在CallbackonViewReleased()方法中調(diào)用。它也是通過調(diào)用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()方法設(shè)置要監(jiān)測哪個方向屏幕邊緣的滑動,而是否是邊緣滑動是通過對觸摸事件中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,以及當(dāng)前手指是否是觸摸在屏幕某一個邊緣,具體是通過當(dāng)前觸摸點x或y是否在邊緣一個很小的距離范圍來來判定。如果是邊緣觸摸并且方向和setEdgeTrackingEnabled()方法設(shè)置要監(jiān)測的方向一致,則會回調(diào)CallbackonEdgeTouched()方法。

通過上面的ACTION_MOVE中代碼看到手指滑動過程中,會調(diào)用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的時候是否是邊緣觸摸,要監(jiān)測的是否是該方向,該邊緣方向是否是鎖定狀態(tài),以及是否是Action_down后首次該邊緣方向的ACTION_MOVE觸發(fā)多個條件來進行判定,如果判定成功則會調(diào)用CallbackonEdgeDragStarted()方法。

其中某個邊緣方向是否是鎖定狀態(tài)通過CallbackonEdgeLock()方法來獲取,我們可以在這個回調(diào)中動態(tài)改變返回值來動態(tài)設(shè)置是否可以觸發(fā)onEdgeDragStarted()方法,而真正要實現(xiàn)邊緣觸摸的時候可以拖拽出某個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源碼解析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容