ViewDragHelper(二)— 源碼解析(進階篇)

聲明:本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發(fā)布

本篇為該系列的第二篇,側(cè)重講解ViewDragHelper 的實現(xiàn)原理和源碼邏輯撰糠,以及它所提供的Callback。

目錄

ViewDragHelper 的介紹以及初步使用請閱讀這篇:
ViewDragHelper (一)- 介紹及簡單用例(入門篇)
ViewDragHelper 的源碼以及Callback的詳情介紹請閱讀這篇:
ViewDragHelper (二)- 源碼及原理解讀(進階篇)
利用DrageHelper 打造仿陌陌APP視頻播放頁的demo請閱讀這篇:
ViewDragHelper (三)- 打造仿陌陌視頻播放頁(深入篇)

一咧栗、 UML 類圖及流程圖

1.1 ViewDragHelper的UML類圖如下所示:

圖1-1. UML類圖

在使用ViewDragHelper過程中,主要涉及到如下四個類:

  • MyDraggableView
    我們自定義的ViewGroup類万哪。
  • ViewDragHelper
    幫助類盈蛮,是我們本篇文章主要分析的對象娘侍。
  • Callback
    ViewDragHelper的內(nèi)部抽象靜態(tài)類,主要用于事件處理結果的回調(diào)及事件監(jiān)聽岂嗓。
  • DraggableViewCallback
    繼承于Callback汁展,是它的實現(xiàn)類,ViewDragHelper里面處理的事件厌殉,我們可以通過該實現(xiàn)類進行監(jiān)聽回調(diào)食绿。

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

圖1-2. MotionEvent事件流程圖

MotionEvent事件是從上往下傳遞的,如果其中的一個onInterceptTouchEvent返回了true公罕,則表示該View攔截此事件系列器紧,此后的MOVE,UP都不會再調(diào)用onInterceptTouchEvent楼眷,而是會直接調(diào)用自己的onTouchEvent方法铲汪。

第一篇文章里面提及的熊尉,我們自定義的ViewGroup控件的 onInterceptTouchEvent 方法,是通過 viewDragHelper.shouldInterceptTouchEvent(ev) 方法的返回值來決定是否攔截掌腰,當它返回 true 時狰住,會直接觸發(fā)該類自己的onTouchEvent方法;在onTouchEvent事件里面通過viewDragHelper 的 processTouchEvent(ev) 方法齿梁,將MotionEvent傳遞給viewDragHelper 內(nèi)部催植,讓viewDragHelper 對事件進行分析處理。以上就是在使用viewDragHelper時勺择,事件分發(fā)的大概流程以及它的處理過程了创南,接下來將分析我們在onTouch 方法里將事件傳遞給viewDragHelper之后 ,它內(nèi)部是如何對事件進行分析處理的省核。

本文由于篇幅關系稿辙,重點講解的是以下幾個部分:

  1. 抽象內(nèi)部靜態(tài)類 ViewDrageHelper .Callback。
  2. ViewDrageHelper 內(nèi)部部分源碼邏輯气忠。
  3. VelocityTracker邻储。
  4. ScrollerCompat。

二笔刹、ViewDragHelper源碼

由UML類圖我們不難看出芥备,ViewDragHelper 是在我們自定義ViewGroup類的構造方法中初始化的,而Callback 是一個ViewDrageHelper 的內(nèi)部靜態(tài)抽象類舌菜。在創(chuàng)建ViewDragHelper 對象時萌壳,我們需要傳入一個繼承自Callback 的實現(xiàn)類實例對象進去。下面我們一步一步來剖析它的內(nèi)部邏輯日月。

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()的方式來創(chuàng)建對象爱咬。那么我們需要如何創(chuàng)建一個新的ViewDragHelper對象呢尺借?不急,我們接著往下看精拟。

2.2 創(chuàng)建對象

我們貼上關于創(chuàng)建對象以及初始化相關的完整源代碼燎斩,其實,通過構造方法上面的英文注釋可以知道蜂绎,Google提供了兩個工廠方法栅表,讓開發(fā)者去創(chuàng)建一個新的ViewDragHelper對象。如下所示:

  1. create(ViewGroup forParent, Callback cb)
    該方法在return 時师枣,利用構造器創(chuàng)建了一個新的ViewDragHelper實例怪瓶。

  2. create(ViewGroup forParent, float sensitivity, Callback cb)
    該方法內(nèi)部,先調(diào)用了第一個工廠方法践美,得到新ViewDragHelper實例洗贰,之后又初始化了 mTouchSlop找岖、mMaxVelocity 、mMinVelocity 敛滋、mScroller 等數(shù)據(jù)和對象许布。

不難看出含有sensitivity 這個參數(shù)的create方法,內(nèi)部也是調(diào)用了create(forParent, cb)方法绎晃,只是它對mTouchSlop做了一下處理爹脾,傳入的靈敏度(sensitivity值)越大,mTouchSlop的值越小箕昭。假設當前手機的系統(tǒng)mTouchSlop 大小為24dp, 若我們傳入的sensitivity = 3.0f ,則mTouchSlop = 8 dp解阅,即單次滑動距離超過8dp落竹,就會觸發(fā)系統(tǒng)的 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方法內(nèi)部做了哪些操作呢?下面我們來看一看源代碼:

    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,則我們應該調(diào)用continueSettling方法讓它繼續(xù)滑動分别,直到返回false遍愿,這次滑動才算完成。

settleCapturedViewAt方法

該方法是以松手前的滑動速度為初值耘斩,讓捕獲到的子View自動滑動到指定位置沼填,它只能在Callback的onViewReleased()中使用,若mReleaseInProgress不為True括授,則會拋出IllegalStateException異常坞笙。傳遞的兩個參數(shù)分別是結束時子控件的位置,其內(nèi)部最終調(diào)用的是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內(nèi)部又做了什么呢?我們繼續(xù)往下看:

 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接受并處理父控件傳遞過來的觸摸事件简十,則該方法內(nèi)部會分析MotionEvent 事件,并根據(jù)需要撬腾,觸發(fā)監(jiān)聽回調(diào)事件螟蝙。需要強調(diào)的是:父控件的onTouchEvent實現(xiàn)方法需要調(diào)用processTouchEvent 方法,才能將事件傳遞給ViewDragHelper讓其分析處理民傻。

我們閱讀其源碼發(fā)現(xiàn)胰默,首先,它做了如下操作:

 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 即手指開始按下時牵署,調(diào)用cancel方法重置了一下狀態(tài),以防以沒有得到當前事件序列的完整事件輸入流喧半,而導致出錯奴迅。

緊接著,若mVelocityTracker(速度跟蹤器)對象為空挺据,則通過VelocityTracker 的內(nèi)部靜態(tài)方法obtain 來創(chuàng)建一個新的對象取具,并通過addMovement將觸摸事件添加監(jiān)聽,用于捕獲用戶手指滑動屏幕的速度扁耐。

然后通過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是在第一個手指按下時觸發(fā),ViewDragHelper內(nèi)部做了如下操作:

  1. 保存初始化x婉称、y位置及pointerId块仆。
  2. 調(diào)用tryCaptureViewForDrag 方法。直接回調(diào)true,因為父控件已經(jīng)處理了ACTION_DOWN 事件酿矢。
  3. 若按下區(qū)域是在邊緣榨乎,則觸發(fā)onEdgeTouched 回調(diào)。

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 狀態(tài) 為STATE_IDLE 瘫筐,即處于閑置狀態(tài)蜜暑,則處理邏輯同ACTION_DOWN。
  2. 否則 直接調(diào)用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 狀態(tài)為 STATE_DRAGGING肛捍,即拖拽狀態(tài)。判斷pointerId是否為無效id, 是則跳過之众。
  2. 獲取觸摸的x拙毫、y 位置,并調(diào)用dragTo 處理拖拽事件棺禾,然后調(diào)用saveLastMotion保存一下當前Motion缀蹄。
  3. 若mDragState 狀態(tài)不是 STATE_DRAGGING,則檢查一遍 pointerId列表,看是否有Id處于可拖動狀態(tài)并進行處理缺前。

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 狀態(tài)為 STATE_DRAGGING 蛀醉,并且 pointerId 為當前行動的Id,則遍歷一次pointerId 列表并進行處理衅码,最后調(diào)用clearMotionHistory清除事件的歷史記錄拯刁。

ACTION_UP 事件:

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

如果 mDragState 狀態(tài)為 STATE_DRAGGING, 則調(diào)用releaseViewForPointerUp方法逝段,該方法會計算當前滑動速度垛玻,并調(diào)用dispatchViewReleased方法,計算松開手指時的X奶躯、Y軸的速度帚桩,并通過mCallback的onViewReleased方法回調(diào)出去。然后調(diào)用cancel重置狀態(tài)嘹黔。

ACTION_CANCEL 事件:

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

如果mDragState 狀態(tài)為 STATE_DRAGGING朗儒,則直接調(diào)用dispatchViewReleased方法,傳遞的初始X参淹、Y軸速度為0;然后調(diào)用cancel重置狀態(tài)乏悄。

三浙值、ViewDragHelper.Callback 部分解讀

以上介紹了ViewDragHelper 類內(nèi)部對MotionEvent事件處理的邏輯,那么它在處理完成后檩小,是如何通知ViewGroup的呢开呐? 很明顯,ViewDragHelper 的靜態(tài)內(nèi)部抽象類Callback 规求,它的職責就是將觸發(fā)的事件及結果返回給ViewGroup的筐付。前面我們已經(jīng)講過了,我們在創(chuàng)建ViewDragHelper的過程中阻肿,需要實例化一個繼承自ViewDragHelper.Callback的實現(xiàn)類瓦戚,并將這個實現(xiàn)類的實例對象傳入了ViewDragHelper,因此ViewDragHelper通過create方法傳遞進來的參數(shù)丛塌,持有實現(xiàn)類的對象實例较解。

在我們的實現(xiàn)類 DraggableViewCallback 中,我們可根據(jù)需求來覆蓋父類Callback所提供的方法以實現(xiàn)相關監(jiān)聽赴邻。其中印衔,抽象類Callback的抽象方法: tryCaptureView() 是必須要在DraggableViewCallback 中實現(xiàn)的。

首先我們看這個抽象內(nèi)部靜態(tài)類的完整源代碼:

  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的拖拽狀態(tài)改變時,回調(diào)該方法。state有三種狀態(tài):
 STATE_IDLE = 0    當前處于閑置狀態(tài)
 STATE_DRAGGING = 1   正在被拖拽的狀態(tài)
 STATE_SETTLING = 2   拖拽后被安放到一個位置中的狀態(tài)

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

 View被拖拽与帆,位置發(fā)生改變時回調(diào)
 changedView :被拖拽的View
 left : 被拖拽后 View的 left 坐標
 top : 被拖拽后 View的 top 坐標
 dx :  拖動的x偏移量
 dy :  拖動的y偏移量

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

  當子控件被捕獲到準備開始拖動時回調(diào)
  capturedChild : 捕獲的View
  activePointerId : 對應的PointerId

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

   當被捕獲拖拽的View被釋放時回調(diào)
   releasedChild : 被釋放的View
   xvel : 釋放View的x方向上的加速度
   yvel : 釋放View的y方向上的加速度

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

   如果parentView訂閱了邊緣觸摸,則如果有邊緣觸摸就回調(diào)的接口
   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)

邊緣觸摸開始時回調(diào)
edgeFlags : 當前觸摸的flag 有: EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM
pointerId : 用來描述邊緣觸摸操作的id

public int getOrderedChildIndex(int index)

 在尋找當前觸摸點下的子View時會調(diào)用此方法了赌,尋找到的View會提供給tryCaptureViewForDrag()來嘗試捕獲。
 如果需要改變子View的遍歷查詢順序可改寫此方法鲤桥,例如讓下層的View優(yōu)先于上層的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 它是一個跟蹤觸摸事件速度的幫助類,可以實現(xiàn)flinging(快速滑動)或者其他類似這樣的手勢箱沦。通過 obtain方法來創(chuàng)建一個新實例辩恼。它所提供的方法有如下幾個:

addMovement ():捕獲某個MotionEvent 的速度。
recycle (): 將該對象回收谓形,并且在調(diào)用該方法之后就不能再調(diào)用它灶伊。
clear (): 重置VelocityTracker 對象恢復到初始狀態(tài)。
computeCurrentVelocity ():計算當前速度寒跳。
getXVelocity (): 獲取 X方向的速度聘萨。
getYVelocity (): 獲取 Y方向的速度。

在ViewDragHelper 類中童太,它被使用的地方有如下幾處:

  1. shouldInterceptTouchEvent 方法里面 對它有進行初始化米辐,并調(diào)用addMovement方法將事件添加進去。
  2. processTouchEvent 方法里面 對它有進行初始化书释,并調(diào)用addMovement方法將事件添加進去翘贮。
  3. flingCapturedView方法里,調(diào)用mScroller 的 fling 方法爆惧,用到了它狸页。
  4. settleCapturedViewAt 方法里 return 調(diào)用forceSettleCapturedViewAt 方法時,傳入了mVelocityTracker扯再,用于捕獲手指離開屏幕的那一刻X肴捉、Y方向的滑動速度。
  5. cancel方法里叔收,調(diào)用了VelocityTracker.recycle() 方法并且重置對象為null齿穗。

五、ScrollerCompat

ScrollerCompat是一個實現(xiàn)View平滑滾動的Helper類饺律。從ScrollerCompat的源碼我們可以看出窃页,它其實就是封裝了OverScroller。ScrollerCompat類的內(nèi)部截圖如下:

圖片.png

事實上,我們常用的ScrollView脖卖,它內(nèi)部也是通過OverScroller 來實現(xiàn)的乒省。有圖有真相:

圖片.png

說到OverScroller,我們可能立馬會想起Scroller畦木,那么OverScroller和Scroller有什么區(qū)別呢袖扛?

事實上,這兩個類它都屬于Scrollers十籍,Scroller屬于早期的API蛆封,在API 11所提供的。而OverScroller是在API 19才新增的勾栗。翻閱他們內(nèi)部源碼我們不難看出惨篱,這兩個類大部分的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 回調(diào)外部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本質(zhì)上是對MotionEvent的分析及處理面粮,并提供了一系列的監(jiān)聽回調(diào)方法,來幫助我們減輕開發(fā)負擔继低,更為方便地處理控件的滑動拖拽邏輯熬苍。總而言之袁翁,深入閱讀源碼柴底,過程雖然會有點辛苦,但理解程度會有很大的提升~ 有興趣的朋友可自行查看它的源代碼梦裂,第三篇將會是深入實戰(zhàn)篇似枕。后續(xù)有時間會陸續(xù)寫好分享出來。感謝支持~ 希望能幫助到有需要的人年柠。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末凿歼,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子冗恨,更是在濱河造成了極大的恐慌答憔,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掀抹,死亡現(xiàn)場離奇詭異虐拓,居然都是意外死亡,警方通過查閱死者的電腦和手機傲武,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門蓉驹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人揪利,你說我怎么就攤上這事态兴。” “怎么了疟位?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵瞻润,是天一觀的道長。 經(jīng)常有香客問我甜刻,道長绍撞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任得院,我火速辦了婚禮傻铣,結果婚禮上,老公的妹妹穿的比我還像新娘祥绞。我一直安慰自己矾柜,他們只是感情好阱驾,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著怪蔑,像睡著了一般里覆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上缆瓣,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天喧枷,我揣著相機與錄音,去河邊找鬼弓坞。 笑死隧甚,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的渡冻。 我是一名探鬼主播戚扳,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼族吻!你這毒婦竟也來了帽借?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤超歌,失蹤者是張志新(化名)和其女友劉穎砍艾,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體巍举,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡脆荷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了懊悯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜓谋。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖炭分,靈堂內(nèi)的尸體忽然破棺而出桃焕,到底是詐尸還是另有隱情,我是刑警寧澤欠窒,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站退子,受9級特大地震影響岖妄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜寂祥,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一荐虐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧丸凭,春花似錦福扬、人聲如沸腕铸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽狠裹。三九已至,卻和暖如春汽烦,著一層夾襖步出監(jiān)牢的瞬間涛菠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工撇吞, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留俗冻,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓牍颈,卻偏偏與公主長得像迄薄,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子煮岁,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355