ViewDragHelper: 實現ViewGroup的子View拖動

自定義ViewGroup最常添加的功能就是子View的拖動,若是你的事件分發及處理的基本功很是紮實,那麼徹底能夠本身實現這個功能。然而幸運的是,系統提供了一個工具類ViewDragHelper,它提供了這個功能實現的框架,這樣就大大提升了開發的效率。php

本文不只僅告訴你這個工具類該怎麼使用,並且也會分析它的設計原理。只有掌握原理了,才能在實際中作到以不變應萬變。java

本文須要你對事件的分發和處理有基本的認識,若是你還沒掌握,能夠參考我以前寫的三篇文章android

  1. 事件分發之View事件處理
  2. ViewGroup事件分發和處理源碼分析
  3. 手把手教你如何寫事件處理的代碼

若是你對事件分發和處理的流程不熟悉,你可能從本文中只學到如何使用ViewDragHelper類,可是並不會掌握它的精華。app

ViewDragHelper實現事件處理

既然ViewDragHelper是一個工具框架類,那麼對事件的處理確定也是作好了封裝。假設有一個自定義ViewGroup類,名字叫作VDHLayout。咱們來看下如何使用ViewDragHelper類實現事件的處理。框架

public class VDHLayout extends ViewGroup {
    ViewDragHelper mViewDragHelper;
    public VDHLayout(Context context) {
        this(context, null);
    }

    public VDHLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 建立ViewDragHelper對象,回調參數用來控制子View的拖動
        mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(@NonNull View child, int pointerId) {
                return false;
            }
        });
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            // 簡單點,只操做第一個子View
            View first = getChildAt(0);
            first.layout(getPaddingLeft(), getPaddingTop(), getPaddingLeft() + first.getMeasuredWidth(),
                    getPaddingTop() + first.getMeasuredHeight());
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 利用ViewDragHelper來判斷是否須要截斷
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 利用ViewDragHelper來處理子View的拖拽
        mViewDragHelper.processTouchEvent(event);
        return true;
    }
}
複製代碼

VDHLayout繼承自ViewGroup,爲了簡單起見,只對它的第一個子View進行佈局,這就是在onLayout()中的操做。ide

事件處理的代碼是在onInterceptTouchEvent()onTouchEvent()方法中實現的,從代碼中能夠看到,分別用ViewDragHelper.shouldInterceptTouchEvent()ViewDragHelper.processTouchEvent()來處理事件。工具

實現ViewDragHelper的回調

如今,咱們已經成功地用ViewDragHelper實現了事件的處理,那麼子View的拖動是在哪裏控制的呢?這個實際上是在建立ViewDragHelper對象的時候,用傳入的回調參數控制的。從代碼中能夠看到,咱們只實現了回調中的一個方法tryCaptureView(),這個方法也是必需要實現的。源碼分析

根據事件分發和處理的原理可知,VDHLayout的子View是否能處理ACTION_DOWN事件,關乎着VDHLayout的事件分發和處理的邏輯。ViewDragHelper的回調固然也是受這個的影響的,所以我將分兩部分來說解如何實現回調。佈局

子View不處理事件

首先咱們來看下子View不處理事件的狀況。post

根據View事件分發和處理的原理可知,若是一個View不設置任何監聽事件,而且不可點擊,也不可長按,那麼這個View就不處理任何事件。

理論上講的有點抽象,舉個例子,例如 在XML佈局中給VDHLayout添加一個ImageView控件

<?xml version="1.0" encoding="utf-8"?>
<com.bxll.vdhdemo.VDHLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">

    <ImageView android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/ic_launcher_round" />

</com.bxll.vdhdemo.VDHLayout>
複製代碼

這個ImageView沒有任何監聽事件,默認不可點擊也不可長按的,所以它就是一個不處理事件的子View。

如今以這個佈局爲例進行分析,當手指點擊ImageView的時候,因爲子View,也就是ImageView,不處理事件,因此ACTION_DOWN事件必定會先通過VDHLayout.onInterceptTouchEvent(),再通過VDHLayout.onTouchEvent()

根據事件處理的經驗,真正的處理邏輯其實都在VDHLayout.onTouchEvent()中,它的實現以下

public boolean onTouchEvent(MotionEvent event) {
        // 利用ViewDragHelper來處理子View的拖拽
        mViewDragHelper.processTouchEvent(event);
        return true;
    }
複製代碼

因爲VDHLayout要經過觸摸事件控制子View拖動,所以在onTouchEvent()中必需要返回true

能夠看到,是用ViewDragHelper.processTouchEvent()來實現VDHLayout.onTouchEvent()的,如今來看看ViewDragHelper.processTouchEvent()是如何處理ACTION_DOWN事件的

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);
                // 1. 找到事件做用於哪一個子View
                final View toCapture = findTopChildUnder((int) x, (int) y);
                // 保存座標值
                saveInitialMotion(x, y, pointerId);
                // 2. 嘗試捕獲這個用於拖動的子View
                tryCaptureViewForDrag(toCapture, pointerId);
                // 邊緣觸摸回調
                final int edgesTouched = mInitialEdgesTouched[pointerId];
                if ((edgesTouched & mTrackingEdges) != 0) {
                    mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
                }
                break;
            }

            // ...
        }
    }
複製代碼

首先經過findTopChildUnder()方法找到手指按下的那個子View

public View findTopChildUnder(int x, int y) {
        final int childCount = mParentView.getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            // getOrderedChildIndex()回調決定了獲取哪一個子View
            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;
    }
複製代碼

原理很簡單,就是經過x,y座標值找到子View,然而咱們能夠發現,回調方法getOrderedChildIndex()決定了究竟是哪一個子View被找到。從這裏能夠看出,手指操做的並不必定都是最上面的子View。

找到了ACTION_DOWN做用的子View後,就經過tryCaptureViewForDrag()來嘗試捕獲這個子View

boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
        if (toCapture == mCapturedView && mActivePointerId == pointerId) {
            // Already done!
            return true;
        }
        // 經過回調判斷這個子View是否能被捕獲
        if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
            mActivePointerId = pointerId;
            captureChildView(toCapture, pointerId);
            return true;
        }
        return false;
    }
複製代碼

首先經過tryCaptureView()回調方法判斷子View是否可以被捕獲,被捕獲的子View才能被用來拖動。

若是可以被捕獲,那麼就調用captureChildView()通知子View被捕獲

public void captureChildView(@NonNull View childView, int activePointerId) {
        // mCapturedView表明被用來拖動的目標
        mCapturedView = childView;
        mActivePointerId = activePointerId;
        // 回調通知View被捕獲 
        mCallback.onViewCaptured(childView, activePointerId);
        // 設置爲拖動狀態
        setDragState(STATE_DRAGGING);
    }
複製代碼

captureChildView()是經過onViewCaptured()進行回調,通知子View已經被捕獲。

如今,來總結下ViewDragHelper.processTouchEvent()ACTION_DOWN事件的處理中,回調作了哪些事事情(只列舉主要的回調)

  1. 經過getOrderedChildIndex()回調,判斷ACTION_DOWN做用於哪一個子View。
  2. 經過tryCaptureView()回調,判斷子View是否能被捕獲。
  3. 經過onViewCaptured()回調,通知哪一個子View被捕獲。

ACTION_DOWN處理完了,如今咱們來看看ACTION_MOVE事件如何處理的。

因爲子View不處理事件,ACTION_MOVE事件交由VDHLayout.onTouchEvent()處理,也就是交給了ViewDragHelper.processTouchEvent()處理。

public void processTouchEvent(@NonNull MotionEvent ev) {
        // ...

        switch (action) {
            // ...

            case MotionEvent.ACTION_MOVE: {
                if (mDragState == STATE_DRAGGING) {
                    // 判斷手指是否有效
                    if (!isValidPointerForActionMove(mActivePointerId)) break;

                    final int index = ev.findPointerIndex(mActivePointerId);
                    final float x = ev.getX(index);
                    final float y = ev.getY(index);
                    // 獲取x,y軸上拖動的距離差
                    final int idx = (int) (x - mLastMotionX[mActivePointerId]);
                    final int idy = (int) (y - mLastMotionY[mActivePointerId]);
                    // 對於目標View執行拖動
                    dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);

                    saveLastMotion(ev);
                } else {
                    // ...
                }
                break;
            }

            // ...
        }
    }
複製代碼

ViewDragHelper.processTouchEvent()ACTION_MOVE的處理中,首先計算在x,y軸上移動的距離差,而後經過dragTo()方法拖動剛剛捕獲的子View。

咱們注意下dragTo()第一個參數和第二個參數,它指的是目標View(被捕獲的子View)理論上要移動到的座標點。

private void dragTo(int left, int top, int dx, int dy) {
        // clampedX, clampedY表示目標View要拖動到的終點座標
        int clampedX = left;
        int clampedY = top;
        // 獲取目標View的起始座標
        final int oldLeft = mCapturedView.getLeft();
        final int oldTop = mCapturedView.getTop();
        if (dx != 0) {
            // 若是拖動的距離大於0,經過回調獲取目標View最終要拖動到的x座標值
            clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
            // 目標View在水平方向移動
            ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
        }
        if (dy != 0) {
            // 若是拖動的距離大於0,經過回調獲取目標View最終要拖動到的x座標值
            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;
            // 回調通知,目標View實際移動到(clampedX, clampedY),以及x,y軸實際移動的距離差爲clampedDx, clampedDy
            mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
                    clampedDx, clampedDy);
        }
    }
複製代碼

x,y方向上,只要任意一個方向上手指拖動的距離大於0, 那麼就經過clampViewPositionHorizontal()/clampViewPositionVertical()回調方法,計算目標View實際須要拖動到的終點座標。

經過回調計算出來終點座標後,就把目標View移動到這個計算出來的座標點上。

最後,只要x,y方向上拖動距離大於0,那麼就經過onViewPositionChanged()回調方法,通知目標View實際拖動到哪一個座標,以及實際拖動的距離差。

如今咱們明白了,ViewDragHelper.processTouchEvent()處理ACTION_MOVE,實際上就是處理目標View的拖動,它用到了以下回調

  1. clampViewPositionHorizontal()clampViewPositionVertical()回調,用來計算目標View拖動的實際座標。
  2. onViewPositionChanged()回調,通知目標View實際被拖動到哪一個座標,以及在x,y軸上拖動的實際距離差。

實現子View不處理事件回調

有了前面的理論基礎,如今咱們來實現下回調,讓不處理事件的子View可以被拖動,並且只容許在水平方向上被拖動。

mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(@NonNull View child, int pointerId) {
                // 爲簡單起見,全部的View均可以被拖動
                return true;
            }

            /** * 控制目標View在x方向的移動。 */
            @Override
            public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
                // 不容許垂直方向移動
                return 0;
            }

            /** * 控制目標View在y方向的移動。 */
            @Override
            public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
                // 水平方向移動不能超出父View範圍
                return Math.min(Math.max(0, left), getWidth() - child.getWidth());
            }
        });
複製代碼

因爲咱們不容許垂直方向的拖動,所以clampViewPositionHorizontal()要返回0,clampViewPositionVertical()的返回值要控制在VDHLayout範圍內滑動。效果以下

VDH_H

在前面的分析中還有其它的一些回調,能夠根據實際項目要求進行復寫實現。

子View處理事件

如今來分析子View可以處理事件的狀況。讓子View能處理事件最簡單的方式是設置它能夠點擊,例如

<com.bxll.vdhdemo.VDHLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">

    <ImageView android:clickable="true" android:layout_width="100dp" android:layout_height="100dp" android:src="@mipmap/ic_launcher_round" />

</com.bxll.vdhdemo.VDHLayout>
複製代碼

當利用這個佈局再次運行程序的時候,你會發現原來能夠拖動的ImageView不能被拖動了。這是由於事件的處理邏輯改變了,從而ViewDragHelper的實現邏輯也改變了。

因爲子View能處理事件,所以對於ACTION_DOWN事件,就只會通過VDHLayout.onInterceptTouchEvent()方法,而並不會通過VDHLayout.onTouchEvent()方法。從前面的代碼實現可知,VDHLayout.onInterceptTouchEvent()是由ViewDragHelper.shouldInterceptTouchEvent()實現。然而ViewDragHelper.shouldInterceptTouchEvent()方法對於ACTION_DOWN只是簡單一些簡單處理,並不會截斷事件。

所以咱們須要分析ACTION_MOVE是如何被ViewDragHelper.shouldInterceptTouchEvent()截斷的。

public boolean shouldInterceptTouchEvent(@NonNull MotionEvent ev) {
        // ...

        switch (action) {
            // ...

            case MotionEvent.ACTION_MOVE: {
                // ...
                
                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];
                    
                    final View toCapture = findTopChildUnder((int) x, (int) y);
                    // 1. 判斷是否達到拖動的標準
                    final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
                    // 一個不截斷的狀況:若是拖動標準,卻沒有實際的拖動距離,那就不截斷事件
                    if (pastSlop) {
                        //獲取新,舊座標值
                        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);
                        // 經過回調獲取x,y方向的拖動範圍
                        final int hDragRange = mCallback.getViewHorizontalDragRange(toCapture);
                        final int vDragRange = mCallback.getViewVerticalDragRange(toCapture);
                        // 沒有實際的拖動距離就不截斷事件
                        if ((hDragRange == 0 || (hDragRange > 0 && newLeft == oldLeft))
                                && (vDragRange == 0 || (vDragRange > 0 && newTop == oldTop))) {
                            break;
                        }
                    }
                    // 報告邊緣動
                    reportNewEdgeDrags(dx, dy, pointerId);
                    if (mDragState == STATE_DRAGGING) {
                        // Callback might have started an edge drag
                        break;
                    }
                    
                    // 2. 若是達到拖動的臨界距離,那麼就嘗試捕獲子View
                    if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
                        break;
                    }
                }
                saveLastMotion(ev);
                break;
            }

        
        }
        // 若是成功捕獲子View,那麼狀態就會被設置爲STATE_DRAGGING,也就表明截斷事件
        return mDragState == STATE_DRAGGING;
    }
複製代碼

ViewDragHelper.shouldInterceptTouchEvent()考慮了多手指的狀況,爲了簡化分析,只考慮單手指的狀況。

第一步,判斷是否達到拖動的條件,有兩個條件

  1. 事件必需要做用於某個子View
  2. checkTouchSlop()返回true

根據事件處理的經驗,若是要截斷ACTION_MOVE事件,必需要有條件地截斷。

checkTouchSlop()方法用來判斷是否達到的拖動的臨界距離

private boolean checkTouchSlop(View child, float dx, float dy) {
        if (child == null) {
            return false;
        }
        // 經過回調方法判斷x,y方向是否容許拖動
        final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
        final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;

        // 若是x或y方向容許拖動,根據拖動的距離計算是否達到拖動的臨界值
        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;
        }
        // 若是x和y方向都不容許拖動,那就永遠不可能達到拖動臨界值
        return false;
    }
複製代碼

首先經過getViewHorizontalDragRange()getViewVerticalDragRange()獲取x,y方向拖動範圍,只要這個範圍大於0,就表明能夠在x,y方向上拖動。而後根據哪一個方向能夠拖動,就相應的計算拖動的距離是否達到了臨界距離。

如今回到shouldInterceptTouchEvent()方法的第二步,當達到了拖動條件後,就調用tryCaptureViewForDrag()嘗試捕獲目標View,這個方法在前面已經分析過,它會首先回調tryCaptureView()肯定目標View是否能被拖動,若是能拖動,再回調onViewCaptured()通知目標View已經捕獲,最後設置狀態爲STATE_DRAGGING

當狀態設置爲了STATE_DRAGGING後,那麼ViewDragHelper.shouldInterceptTouchEvent()返回值就是true,也就是說VDHLayout.onInterceptTouchEvent()截斷了ACTION_MOVE事件。

VDHLayout.onInterceptTouchEvent()截斷了ACTION_MOVE事件後,後續的ACTION_MOVE事件就交給了VDHLayout.onTouchEvent()方法,也就是交給了ViewDragHelper.processTouchEvent()處理。這個方法以前分析過,就是處理目標View的拖動。

那麼如今咱們來總結下ViewDragHelper.shouldInterceptTouchEvent()在處理ACTION_MOVE截斷的時候,用到哪些關鍵回調

  1. getViewHorizontalDragRange()getViewVerticalDragRange()方法判斷x,y方向上是否能夠拖動。返回值大於0表示能夠拖動。

實現View處理事件的回調

通過剛纔的分析,咱們知道,對於一個能處理事件的子View,若是想讓它能被拖動,必須複寫getViewHorizontalDragRange()getViewVerticalDragRange()回調,用於告訴ViewDragHelper,在相應的方向上容許被拖動。

那麼如今,咱們就來解決子View(能處理事件)不能拖動的問題,咱們仍然只讓子View在水平方向上被拖動

mViewDragHelper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(@NonNull View child, int pointerId) {
                // 爲簡單起見,全部的View均可以被拖動
                return true;
            }

            /** * 控制目標View在x方向的移動。 */
            @Override
            public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
                // 不容許垂直方向移動
                return 0;
            }

            /** * 控制目標View在y方向的移動。 */
            @Override
            public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
                // 水平方向移動不能超出父View範圍
                return Math.min(Math.max(0, left), getWidth() - child.getWidth());
            }

            @Override
            public int getViewHorizontalDragRange(@NonNull View child) {
                // 因爲只容許目標View在VDHLayout中水平拖動,所以水平拖動範圍就是VDHLayout的寬度減去目標View寬度
                return getWidth() - child.getWidth();
            }

            @Override
            public int getViewVerticalDragRange(@NonNull View child) {
                // 因爲不容許垂直方向拖動,所以拖動範圍也就是0
                return 0;
            }
        });
    }
複製代碼

因爲咱們只容許水平方向拖動,所以getViewVerticalDragRange()返回的垂直方向的拖動範圍就是0,getViewHorizontalDragRange()返回的水平方向的拖動範圍就是getWidth() - child.getWidth()

邊緣觸摸

ViewDragHelper有一個邊緣觸摸功能,這個邊緣觸摸的功能比較簡單,所以我並不打算從源碼進行分析,而只是從API角度進行說明。

要向觸發邊緣滑動功能,首先要調用ViewDragHelper.setEdgeTrackingEnabled(int edgeFlags)方法,設置哪一個邊緣容許跟蹤。參數有以下幾個可用值

public static final int EDGE_LEFT = 1 << 0;

    public static final int EDGE_RIGHT = 1 << 1;

    public static final int EDGE_TOP = 1 << 2;

    public static final int EDGE_BOTTOM = 1 << 3;

    public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;
複製代碼

邊緣觸摸的回調有以下幾個

/** * Called when one of the subscribed edges in the parent view has been touched * by the user while no child view is currently captured. */
        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. */
        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. */
        public void onEdgeDragStarted(int edgeFlags, int pointerId) {}        
複製代碼

註釋已經很清楚的解釋了這幾個回調的時機,我獻醜來翻譯下

  1. onEdgeTouched(): 當沒有子View被捕獲,而且容許邊緣觸摸,當用戶觸摸邊緣時回調。
  2. onEdgeLock(): 用來鎖定鎖定哪一個邊緣。這個回調是在onEdgeTouched()以後,開始拖動以前調用的。
  3. onEdgeDragStarted(): 當沒有子View被捕獲,而且容許邊緣觸摸,當用戶已經開始拖動的時候回調。

系統控件DrawerLayout就是利用ViewDragHelper的邊緣滑動功能實現的。因爲篇幅緣由,我就不用例子來展現邊緣觸摸的功能如何使用了。

ViewDragHelper實現View滑動

ViewDragHelper還有一個View定義的功能,利用OverScroller實現。有以下幾個方法

/** * Settle the captured view at the given (left, top) position. * The appropriate velocity from prior motion will be taken into account. * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} * on each subsequent frame to continue the motion until it returns false. If this method * returns false there is no further work to do to complete the movement. */    
    public boolean settleCapturedViewAt(int finalLeft, int finalTop) {}
    
    /** * Animate the view <code>child</code> to the given (left, top) position. * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} * on each subsequent frame to continue the motion until it returns false. If this method * returns false there is no further work to do to complete the movement. */
    public boolean smoothSlideViewTo(@NonNull View child, int finalLeft, int finalTop) {}
    
    /** * Settle the captured view based on standard free-moving fling behavior. * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame * to continue the motion until it returns false. */
    public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {}
複製代碼

從註釋能夠能夠看出,這個三個方法都須要在下一幀刷新的時候調用continueSettling(),這個就與OverScroller的用法是一致的。

如今,來利用settleCapturedViewAt()方法實現一個功能,讓拖動的View被釋放後,回到原點。

當拖動的View被釋放後,會回調onViewReleased()方法

public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
    if (mViewDragHelper.settleCapturedViewAt(0, 0)) {
        invalidate();
    }
}
複製代碼

因爲利用的是OverScroller來實現的,所以必須調用進行重繪。重繪的時候,會調用控件的computeScroll()方法,在這裏調用剛纔說講的continueSettling()方法

public void computeScroll() {
    if (mViewDragHelper.continueSettling(true)) {
        invalidate();
    }
}
複製代碼

continueSettling()也是對OverScroller邏輯的封裝,若是返回true就表明這個定位操做還在進行中,所以還須要繼續調用重繪操做。

想了解其中的原理,你必定要熟悉OverScroller的原理。

如此一來就能夠實現以下效果

Settling

結束

不少絢麗的視圖拖動操做,每每都是用ViewDragHelper實現的,這個工具類簡直是一個集大成之做,咱們須要徹底掌握它,這樣咱們才能遊刃有餘地在自定義ViewGroup中實現各類牛逼的View拖動效果。

相關文章
相關標籤/搜索