ViewDragHelper源碼分析

我每周會寫一篇源代碼分析的文章,以后也可能會有其他主題.
如果你喜歡我寫的文章的話,歡迎關注我的新浪微博@達達達達sky
地址: http://weibo.com/u/2030683111
每周我會第一時間在微博分享我寫的文章,也會積極轉發更多有用的知識給大家.謝謝關注_,說不定什么時候會有福利哈.


1.簡介

在上一篇文章SwipeBackLayout源代碼分析中,我們了解了ViewDragHelper是可以幫助我們處理各種拖拽事件的類.使用好ViewDragHelper能幫助我們做出各種酷炫的交互,今天我們就來分析一下ViewDragHelper的使用與實現:

2.使用方法

我們這里就以翔總的這篇文章中的例子來介紹一下ViewDragHelper的使用.另外,本文中的demo可以在
這里找到

首先我們創建一個DragLayout類并繼承自LinearLayout,然后我們準備在DragLayout放置三個View第一個用來被我們拖動然后停止在松手的位置,第二個可以被我們拖動,松手的時候滑動到指定位置,第三個只可以通過觸摸邊緣來進行拖動,


public class DragLayout extends LinearLayout {

    private ViewDragHelper mDragger;
    private View mDragView;
    private View mAutoBackView;
    private View mEdgeTrackerView;

    private Point mAutoBackOriginPos = new Point();

    public DragLayout(Context context) {
        this(context, null);
    }

    public DragLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initViewDragHelper();
    }

    private void initViewDragHelper() {
        mDragger = ViewDragHelper.create(this,myCallback);
        mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);
    }

    ViewDragHelper.Callback myCallback = new ViewDragHelper.Callback() {
        @Override
        //child為當前觸摸區域下的View,如果返回true,就可以拖拽.
        public boolean tryCaptureView(View child, int pointerId) {
            return child == mDragView || child == mAutoBackView;
        }

        //松手時的回調
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            if (releasedChild == mAutoBackView) {
                mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);
                invalidate();
            }
        }
        
        //邊緣觸摸開始時的回調
        @Override
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {
            mDragger.captureChildView(mEdgeTrackerView, pointerId);
        }

        //獲取水平方向允許拖拽的區域,這里是父布局的寬-子控件的寬
        @Override
        public int getViewHorizontalDragRange(View child) {
            return getMeasuredWidth() - child.getMeasuredWidth();
        }

        //獲取垂直方向允許拖拽的范圍
        @Override
        public int getViewVerticalDragRange(View child) {
            return getMeasuredHeight() - child.getMeasuredHeight();
        }

        //left為child即將移動到的水平位置的值,但是返回值會最終決定移動到的值
        //這里直接返回了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;
        }
    };

    @Override
    public void computeScroll() {
        if (mDragger.continueSettling(true)) {
            invalidate();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mDragger.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDragger.processTouchEvent(event);
        return true;
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mDragView = getChildAt(0);
        mAutoBackView = getChildAt(1);
        mEdgeTrackerView = getChildAt(2);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        mAutoBackOriginPos.x = mAutoBackView.getLeft();
        mAutoBackOriginPos.y = mAutoBackView.getTop();
    }
}

  1. 我們首先在構造方法里傳入了當前類的對象和我們定義的ViewDragHelper.Callback對象初始化了我們的ViewDragHelper,然后我們希望所有的邊緣觸摸都能觸發mEdgeTrackerView的拖動,所以我們緊接著調用了mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);方法.
  2. 在我們定義的Callback中,有多個回調方法,每個回調方法都有它的作用,在代碼里注釋比較清楚了,我們下面也會解析每一個Callback中回調方法的作用.
  3. 第三步我們需要在onInterceptTouchEvent()方法和onTouchEvent()將事件委托給ViewDragHelper去處理,這樣ViewDragHelper才能根據響應的事件并回調我們自己編寫的Callback接口來進行響應的處理,
  4. 由于ViewDragHelper中的滑動是交給Srcoller類來處理的所以這里我們要重寫computeScroll()方法,配合Scroller完成滾動動畫.
  5. 最后在onFinishInflate()里獲取到我們的View對象即可.

3.類關系圖

由于就一個類類圖我們就不畫了,但是作為一個強迫癥患者,這個標題必須有...

4.源碼分析

1.ViewDragHelper.Callback的實現

在分析ViewDragHelper之前,我們先來分析一下Callback的定義,看看Callback都定義了哪些方法:


    public static abstract class Callback {

        //當View的拖拽狀態改變時回調,state為STATE_IDLE,STATE_DRAGGING,STATE_SETTLING的一種
        //STATE_IDLE: 當前未被拖拽
        //STATE_DRAGGING:正在被拖拽
        //STATE_SETTLING: 被拖拽后需要被安放到一個位置中的狀態
        public void onViewDragStateChanged(int state) {}

        //當View被拖拽位置發生改變時回調
        //changedView :被拖拽的View
        //left : 被拖拽后View的left邊緣坐標
        //top : 被拖拽后View的top邊緣坐標
        //dx : 拖動的x偏移量
        //dy : 拖動的y偏移量
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}

        //當一個View被捕獲到準備開始拖動時回調,
        //capturedChild : 捕獲的View
        //activePointerId : 對應的PointerId
        public void onViewCaptured(View capturedChild, int activePointerId) {}

        //當被捕獲拖拽的View被釋放是回調
        //releasedChild : 被釋放的View
        //xvel : 釋放View的x方向上的加速度
        //yvel : 釋放View的y方向上的加速度
        public void onViewReleased(View releasedChild, float xvel, float yvel) {}

        //如果parentView訂閱了邊緣觸摸,則如果有邊緣觸摸就回調的接口
        //edgeFlags : 當前觸摸的flag 有: EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM
        //pointerId : 用來描述邊緣觸摸操作的id
        public void onEdgeTouched(int edgeFlags, int pointerId) {}

        //是否鎖定該邊緣的觸摸,默認返回false,返回true表示鎖定
        public boolean onEdgeLock(int edgeFlags) {
            return false;
        }

        //邊緣觸摸開始時回調
        //edgeFlags : 當前觸摸的flag 有: EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM
        //pointerId : 用來描述邊緣觸摸操作的id
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {}

        //在尋找當前觸摸點下的子View時會調用此方法,尋找到的View會提供給tryCaptureViewForDrag()來嘗試捕獲。
        //如果需要改變子View的遍歷查詢順序可改寫此方法,例如讓下層的View優先于上層的View被選中。
        public int getOrderedChildIndex(int index) {
            return index;
        }

        //獲取被拖拽View child 的水平拖拽范圍,返回0表示無法被水平拖拽
        public int getViewHorizontalDragRange(View child) {
            return 0;
        }

        //獲取被拖拽View child 的垂直拖拽范圍,返回0表示無法被水平拖拽
        public int getViewVerticalDragRange(View child) {
            return 0;
        }

        //嘗試捕獲被拖拽的View
        public abstract boolean tryCaptureView(View child, int pointerId);

        //決定拖拽View在水平方向上應該移動到的位置
        //child : 被拖拽的View
        //left : 期望移動到位置的View的left值
        //dx : 移動的水平距離
        //返回值 : 直接決定View在水平方向的位置
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return 0;
        }

        //決定拖拽View在垂直方向上應該移動到的位置
        //child : 被拖拽的View
        //top : 期望移動到位置的View的top值
        //dy : 移動的垂直距離
        //返回值 : 直接決定View在垂直方向的位置
        public int clampViewPositionVertical(View child, int top, int dy) {
            return 0;
        }
    }

想必注釋已經很清楚了,正是這些回調方法,再結合ViewDragHelper中的各種方法,來幫助我們實現各種各樣的拖拽的效果。

2.shouldInterceptTouchEvent()方法的實現

在這里我們假設大家都清楚了Android的事件分發機制,如果不清楚請看這里,要想處理觸摸事件,我們需要在onInterceptTouchEvent(MotionEvent ev)方法里判斷是否需要攔截這次觸摸事件,如果此方法返回true則觸摸事件將會交給onTouchEvent(MotionEvent event)處理,這樣我們就能處理觸摸事件了,所以我們在上面的使用方法里會這樣寫:


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mDragger.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDragger.processTouchEvent(event);
        return true;
    }

這樣就將是否攔截觸摸事件,以及處理觸摸事件委托給ViewDragHelper來處理了,所以我們先來看看ViewDragHelpershouldInterceptTouchEvent();方法的實現:


    public boolean shouldInterceptTouchEvent(MotionEvent ev) {
        //獲取action
        final int action = MotionEventCompat.getActionMasked(ev);
        //獲取action對應的index
        final int actionIndex = MotionEventCompat.getActionIndex(ev);

        //如果是按下的action則重置一些信息,包括各種事件點的數組
        if (action == MotionEvent.ACTION_DOWN) {
            // Reset things for a new event stream, just in case we didn't get
            // the whole previous stream.
            cancel();
        }
        //初始化mVelocityTracker
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);

        //根據action來做相應的處理
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                final float x = ev.getX();
                final float y = ev.getY();
                //獲取這個事件對應的pointerId,一般情況下只有一個手指觸摸時為0
                //兩個手指觸摸時第二個手指觸摸返回的pointerId為1,以此類推
                final int pointerId = MotionEventCompat.getPointerId(ev, 0);
                //保存點的數據
                //TODO (1)
                saveInitialMotion(x, y, pointerId);
                //獲取當前觸摸點下最頂層的子View
                //TODO (2)
                final View toCapture = findTopChildUnder((int) x, (int) y);

                //如果toCapture是已經捕獲的View,而且正在處于被釋放狀態
                //那么就重新捕獲
                if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
                    tryCaptureViewForDrag(toCapture, pointerId);
                }

                //如果觸摸了邊緣,回調callback的onEdgeTouched()方法
                final int edgesTouched = mInitialEdgesTouched[pointerId];
                if ((edgesTouched & mTrackingEdges) != 0) {
                    mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
                }
                break;
            }

            //當又有一個手指觸摸時
            case MotionEventCompat.ACTION_POINTER_DOWN: {
                final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
                final float x = MotionEventCompat.getX(ev, actionIndex);
                final float y = MotionEventCompat.getY(ev, actionIndex);

                //保存觸摸信息
                saveInitialMotion(x, y, pointerId);

                //因為同一時間ViewDragHelper只能操控一個View,所以當有新的手指觸摸時
                //只討論當無觸摸發生時,回調邊緣觸摸的callback
                //或者正在處于釋放狀態時重新捕獲View
                if (mDragState == STATE_IDLE) {
                    final int edgesTouched = mInitialEdgesTouched[pointerId];
                    if ((edgesTouched & mTrackingEdges) != 0) {
                        mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
                    }
                } else if (mDragState == STATE_SETTLING) {
                    // Catch a settling view if possible.
                    final View toCapture = findTopChildUnder((int) x, (int) y);
                    if (toCapture == mCapturedView) {
                        tryCaptureViewForDrag(toCapture, pointerId);
                    }
                }
                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 = MotionEventCompat.getPointerCount(ev);
                for (int i = 0; i < pointerCount; i++) {
                    final int pointerId = MotionEventCompat.getPointerId(ev, i);
                    final float x = MotionEventCompat.getX(ev, i);
                    final float y = MotionEventCompat.getY(ev, i);
                    //獲得拖拽偏移量
                    final float dx = x - mInitialMotionX[pointerId];
                    final float dy = y - mInitialMotionY[pointerId];
                    //獲取當前觸摸點下最頂層的子View
                    final View toCapture = findTopChildUnder((int) x, (int) y);
                    //如果找到了最頂層View,并且產生了拖動(checkTouchSlop()返回true)
                    //TODO (3)
                    final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
                    if (pastSlop) {
                        //根據callback的四個方法getView[Horizontal|Vertical]DragRange和
                        //clampViewPosition[Horizontal|Vertical]來檢查是否可以拖動
                        final int oldLeft = toCapture.getLeft();
                        final int targetLeft = oldLeft + (int) dx;
                        final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
                                targetLeft, (int) dx);
                        final int oldTop = toCapture.getTop();
                        final int targetTop = oldTop + (int) dy;
                        final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
                                (int) dy);
                        final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
                                toCapture);
                        final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
                        //如果都不允許移動則跳出循環
                        if ((horizontalDragRange == 0 || horizontalDragRange > 0
                                && newLeft == oldLeft) && (verticalDragRange == 0
                                || verticalDragRange > 0 && newTop == oldTop)) {
                            break;
                        }
                    }
                    //記錄并回調是否有邊緣觸摸
                    reportNewEdgeDrags(dx, dy, pointerId);
                    if (mDragState == STATE_DRAGGING) {
                        // Callback might have started an edge drag
                        break;
                    }
                    //如果產生了拖動則調用tryCaptureViewForDrag()
                    //TODO (4)
                    if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
                        break;
                    }
                }
                //保存觸摸點的信息
                saveLastMotion(ev);
                break;
            }

            //當有一個手指抬起時,清除這個手指的觸摸數據
            case MotionEventCompat.ACTION_POINTER_UP: {
                final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
                clearMotionHistory(pointerId);
                break;
            }

            //清除所有觸摸數據
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL: {
                cancel();
                break;
            }
        }

        //如果mDragState等于正在拖拽則返回true
        return mDragState == STATE_DRAGGING;
    }

上面就是整個shouldInterceptTouchEvent()的實現,上面的注釋也足夠清楚了,我們這里就先不分析某一種觸摸事件,大家可以看到我上面留了幾個TODO,下文會一起分析,這里我假設大家都已經對觸摸事件分發處理都有充分的理解了,我們下面就直接看ViewDragHelperprocessTouchEvent()方法的實現.

3.processTouchEvent()方法的實現


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

        ...(省去部分代碼)
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                ...(省去部分代碼)
                break;
            }

            case MotionEventCompat.ACTION_POINTER_DOWN: {
                ...(省去部分代碼)
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                //如果現在已經是拖拽狀態
                if (mDragState == STATE_DRAGGING) {
                    final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    final float x = MotionEventCompat.getX(ev, index);
                    final float y = MotionEventCompat.getY(ev, index);
                    final int idx = (int) (x - mLastMotionX[mActivePointerId]);
                    final int idy = (int) (y - mLastMotionY[mActivePointerId]);

                    //拖拽至指定位置
                    //TODO (5)
                    dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);

                    saveLastMotion(ev);
                } else {
                    // Check to see if any pointer is now over a draggable view.
                    //如果還不是拖拽狀態,就檢測是否經過了一個View
                    final int pointerCount = MotionEventCompat.getPointerCount(ev);
                    for (int i = 0; i < pointerCount; i++) {
                        final int pointerId = MotionEventCompat.getPointerId(ev, i);
                        final float x = MotionEventCompat.getX(ev, i);
                        final float y = MotionEventCompat.getY(ev, 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;
            }
            //當多個手指中的一個手機松開時
            case MotionEventCompat.ACTION_POINTER_UP: {
                final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
                //如果當前點正在被拖拽,則再剩余還在觸摸的點鐘尋找是否正在View上
                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 = MotionEventCompat.getPointerCount(ev);
                    for (int i = 0; i < pointerCount; i++) {
                        final int id = MotionEventCompat.getPointerId(ev, i);
                        if (id == mActivePointerId) {
                            // This one's going away, skip.
                            continue;
                        }

                        final float x = MotionEventCompat.getX(ev, i);
                        final float y = MotionEventCompat.getY(ev, 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.
                        //如果沒找到則釋放View
                        //TODO (6)
                        releaseViewForPointerUp();
                    }
                }
                clearMotionHistory(pointerId);
                break;
            }

            case MotionEvent.ACTION_UP: {
                //如果是拖拽狀態的釋放則調用
                //releaseViewForPointerUp()
                if (mDragState == STATE_DRAGGING) {
                    releaseViewForPointerUp();
                }
                cancel();
                break;
            }

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

上面就是processTouchEvent()方法的實現,我們省去了部分大致與shouldInterceptTouchEvent()相同的邏輯代碼,通過事件傳遞機制我們知道,如果程序已經進入到processTouchEvent()中,也就意味著觸摸事件就不會再向下傳遞,都會交給此方法處理,所以在這里我們就需要處理拖拽事件了,通過上面的注釋,我們也看到了在MotionEvent.ACTION_MOVE,MotionEventCompat.ACTION_POINTER_UP,MotionEvent.ACTION_UPMotionEvent.ACTION_CANCEL都分別進行了處理 ,我們知道觸摸事件大致的流程是:

ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP

再配合事件的分發機制,我們就能很清晰的分析出一次完整的事件調用過程,所以整個ViewDragHelper的拖拽過程也能很清晰的分為三個步驟:

捕獲拖拽目標View -> 拖拽目標View -> 處理目標View釋放操作

最后我們再分析上面兩段代碼的6個TODO:

4.saveInitialMotion()方法

    private void saveInitialMotion(float x, float y, int pointerId) {
        //確保各個數組的大小足夠存放數據
        ensureMotionHistorySizeForId(pointerId);
        //保存x坐標
        mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
        //保存y坐標
        mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
        //保存是否觸摸到邊緣
        mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
        //保存當前id是否在觸摸,用于后續驗證
        mPointersDown |= 1 << pointerId;
    }

5.findTopChildUnder()方法

    public View findTopChildUnder(int x, int y) {
        final int childCount = mParentView.getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
            if (x >= child.getLeft() && x < child.getRight() &&
                    y >= child.getTop() && y < child.getBottom()) {
                return child;
            }
        }
        return null;
    }

代碼很簡單就是根據xy坐標和來找到指定View,注意這里回調了callback中的getOrderedChildIndex()方法,所以我們可以在這里返回指定的Viewindex.

6.checkTouchSlop()方法

    private boolean checkTouchSlop(View child, float dx, float dy) {
        if (child == null) {
            return false;
        }
        final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
        final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;

        if (checkHorizontal && checkVertical) {
            return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
        } else if (checkHorizontal) {
            return Math.abs(dx) > mTouchSlop;
        } else if (checkVertical) {
            return Math.abs(dy) > mTouchSlop;
        }
        return false;
    }

用來根據mTouchSlop最小拖動的距離來判斷是否屬于拖動,mTouchSlop根據我們設定的靈敏度決定.

7.tryCaptureViewForDrag()方法


    boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
        //如果已經捕獲該View 直接返回true
        if (toCapture == mCapturedView && mActivePointerId == pointerId) {
            // Already done!
            return true;
        }
        //根據mCallback.tryCaptureView()方法來最終決定是否可以捕獲View
        if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
            mActivePointerId = pointerId;
            //如果可以則調用captureChildView(),并返回true
            captureChildView(toCapture, pointerId);
            return true;
        }
        return false;
    }

可以看到如果可以捕獲View則調用了captureChildView()方法:


    public void captureChildView(View childView, int activePointerId) {
        if (childView.getParent() != mParentView) {
            throw new IllegalArgumentException("captureChildView: parameter must be a descendant " +
                    "of the ViewDragHelper's tracked parent view (" + mParentView + ")");
        }
        //賦值mCapturedView
        mCapturedView = childView;
        mActivePointerId = activePointerId;
        //回調callback
        mCallback.onViewCaptured(childView, activePointerId);
        //設定mDragState的狀態為STATE_DRAGGING
        setDragState(STATE_DRAGGING);
    }

如果程序執行到這里,就證明View已經處于拖拽狀態了,后續的觸摸操作,將直接根據mDragStateSTATE_DRAGGING的狀態處理.

8.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) {
            //回調callback來決定View最終被拖拽的x方向上的偏移量
            clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
            //移動View
            ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
        }
        if (dy != 0) {
            //回調callback來決定View最終被拖拽的y方向上的偏移量
            clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
            //移動View
            ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
        }

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

因為dragTo()方法是在processTouchEvent()中的MotionEvent.ACTION_MOVE case被調用所以當程序運行到這里時View就會不斷的被拖動了。如果一旦手指釋放則最終會調用releaseViewForPointerUp()方法

8.releaseViewForPointerUp()方法的實現


    private void releaseViewForPointerUp() {
        //計算出當前x和y方向上的加速度
        mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
        final float xvel = clampMag(
                VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
                mMinVelocity, mMaxVelocity);
        final float yvel = clampMag(
                VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
                mMinVelocity, mMaxVelocity);
        dispatchViewReleased(xvel, yvel);
    }

計算完加速度后就調用了dispatchViewReleased():


    private void dispatchViewReleased(float xvel, float yvel) {
        //設定當前正處于釋放階段
        mReleaseInProgress = true;
        //回調callback的onViewReleased()方法
        mCallback.onViewReleased(mCapturedView, xvel, yvel);
        mReleaseInProgress = false;

        //設定狀態
        if (mDragState == STATE_DRAGGING) {
            // onViewReleased didn't call a method that would have changed this. Go idle.
            //如果onViewReleased()中沒有調用任何方法,則狀態設定為STATE_IDLE
            setDragState(STATE_IDLE);
        }
    }

所以最后釋放后的處理交給了callback中的onViewReleased()方法,如果我們什么都不做,那么這個被拖拽的View就是停止在當前位置,或者我們可以調用ViewDragHelper提供給我們的這幾個方法:

  • settleCapturedViewAt(int finalLeft, int finalTop)
    以松手前的滑動速度為初速動,讓捕獲到的View自動滾動到指定位置。只能在Callback的onViewReleased()中調用。
  • flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)
    以松手前的滑動速度為初速動,讓捕獲到的View在指定范圍內fling。只能在Callback的onViewReleased()中調用。
  • smoothSlideViewTo(View child, int finalLeft, int finalTop)
    指定某個View自動滾動到指定的位置,初速度為0,可在任何地方調用。

引用自這篇文章,具體釋放后的原理我們就不分析了,其實就是配合Scroller這個類來實現,具體也可以參照上面這篇文章。好,我們關于ViewDragHelper的源碼分析就到這里.

5.開源項目中的使用

ViewDragHelper在各種關于拖拽和各種手勢動畫的開源庫中使用廣泛,我這里就簡要列出一些,大家可以多去看看是如何使用ViewDragHelper的:

6.個人評價

ViewDragHelper的出現,大大簡化了我們開發相關觸摸和拖拽功能的復雜度和代碼量,幫助我們比較容易的實現各種效果,讓我們開發酷炫的交互更加容易了。但是從一些開源項目中發現,ViewDragHelper中還是有一些不足之處,比如給Scroller提供了一個固定的Interpolator,導致如果我們想實現例如反彈效果的話,還要把ViewDragHelper的代碼拷貝一份并修改Interpolator,這樣做肯定是不太好的.當然建議我們自己修改一個ViewDragHelper后如果項目里有多處使用,可以包裝成一個提供給我們自己項目的模塊使用,防止出現更多的多余代碼.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內容