1. 程式人生 > >使用 ViewDragHelper 實現沿三個方向拖動的例子

使用 ViewDragHelper 實現沿三個方向拖動的例子

1. 需求

先看一下效果圖:




效果圖

2. 需求分析

當手指拖動中間的原形圖示(稱為 home)時,只能向左,向右,向上三個方向拖動;
當 home 向左,向右,向上拖動時,home 的中心不能超過左,右,上三個圖示的中心;
當 home 向左,向右,向上拖動時,若 home 的中心,和左,右,上三個圖示的中心重合,就觸發一定的操作,這裡使用 toast 代替;
當 home 向左,向右,向上拖動時,若 home 的中心未達到左,右,上三個圖示的中心並且手指鬆開,那麼 home 會回到它原來的位置。

3. 實現

3.1 實現方案選擇

我最開始接到的需求是隻有左向和右向拖動,沒有向上的拖動,採用的方法是監聽 home 圖示的觸控事件,在 onTouch(View v, MotionEvent event)

回撥方法中再設定 home 圖示的 setTranslationX(float translationX) 來實現的。
之後,新需求就增加了向上的拖動,在達到紅包的中心時,就開啟紅包。採用的方法是在現有的程式碼基礎上修改,也算完成了任務。但是裡面的判斷很多,實現起來很複雜。寫完之後,就是感到這是堆出來的程式碼,磨出來的程式碼,總之,就是不好。這部分程式碼,就不提供了。
隨後,在群裡問了這個問題,有人說使用 ViewDragHelper 可以實現的。看一下,ViewDragHelper 的文件說明:

/**
 * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number
 * of useful operations and state tracking for allowing a user to drag and reposition
 * views within their parent ViewGroup.
 */

ViewDragHelper 是一個工具類, 用來寫自定義的 ViewGroup 。它提供了一些好用的操作和狀態追蹤,使使用者可以在 view 的父容器裡拖動和重新放置那些 view。

3.2 程式碼實現

自定義 ViewGroup

看了上面的文件說明,瞭解到需要把要拖動的 view,放在一個自定義的 ViewGroup 裡面。

/**
 * 固定向拖動 ViewGroup
 * @author wzc
 * @date 2018/5/6
 */
public class DirectionDragLayout extends ConstraintLayout {

    private
final ViewDragHelper mViewDragHelper; public DirectionDragLayout(Context context, AttributeSet attrs) { super(context, attrs); // 1, 建立 ViewDragHelper 的例項 // 參一 : 當前的ViewGroup物件 Parent view to monitor // 參二 : 靈敏度 Multiplier for how sensitive the helper should be about detecting // the start of a drag. Larger values are more sensitive. 1.0f is normal. // 參三 : 提供資訊和接收事件的回撥 mViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragCallback()); } // 2, 在onInterceptTouchEvent和onTouchEvent中呼叫VDH的方法 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // 通過使用mDragHelper.shouldInterceptTouchEvent(ev)來決定我們是否應該攔截當前的事件 return mViewDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { // 通過mDragHelper.processTouchEvent(event)來處理事件 mViewDragHelper.processTouchEvent(event); return true; // 返回 true,表示事件被處理了。 } class ViewDragCallback extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(View child, int pointerId) { return false; } } }

在 Activity 的佈局中使用自定義的 ViewGroup

activity_directiondrag.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="#44000000"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <com.wzc.t20_vdh.DirectionDragLayout
        android:layout_alignParentBottom="true"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!--閱讀-->
        <ImageView
            android:id="@+id/iv_read"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="@+id/iv_home"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/iv_arrow_left"
            app:layout_constraintTop_toTopOf="@+id/iv_home"
            app:srcCompat="@drawable/read"/>

        <!--箭頭-->
        <ImageView
            android:id="@+id/iv_arrow_left"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:rotation="180"
            android:src="@drawable/lock_slide"
            app:layout_constraintBottom_toBottomOf="@+id/iv_home"
            app:layout_constraintLeft_toRightOf="@+id/iv_read"
            app:layout_constraintRight_toLeftOf="@+id/iv_home"
            app:layout_constraintTop_toTopOf="@+id/iv_home"/>

        <!--箭頭-->
        <ImageView
            android:id="@+id/iv_arrow_right"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/lock_slide"
            app:layout_constraintBottom_toBottomOf="@+id/iv_home"
            app:layout_constraintLeft_toRightOf="@+id/iv_home"
            app:layout_constraintRight_toLeftOf="@+id/iv_unlock"
            app:layout_constraintTop_toTopOf="@+id/iv_home"/>

        <!--解鎖-->
        <ImageView
            android:id="@+id/iv_unlock"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="@+id/iv_home"
            app:layout_constraintLeft_toRightOf="@+id/iv_arrow_right"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="@+id/iv_home"
            app:srcCompat="@drawable/unlock"/>

        <!--箭頭-->
        <ImageView
            android:id="@+id/iv_arrow_top"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            android:src="@drawable/lock_slide_up"
            app:layout_constraintBottom_toTopOf="@+id/iv_home"
            app:layout_constraintLeft_toLeftOf="@+id/iv_home"
            app:layout_constraintRight_toRightOf="@+id/iv_home"/>

        <!--紅包-->
        <ImageView
            android:id="@+id/iv_redbag"
            android:layout_width="44dp"
            android:layout_height="44dp"
            android:layout_marginBottom="16dp"
            android:layout_marginTop="16dp"
            app:layout_constraintBottom_toTopOf="@+id/iv_arrow_top"
            app:layout_constraintLeft_toLeftOf="@+id/iv_home"
            app:layout_constraintRight_toRightOf="@+id/iv_home"
            app:srcCompat="@drawable/ic_lock_redbag"/>

        <!--滑動的原點-->
        <ImageView
            android:id="@+id/iv_home"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toRightOf="@+id/iv_arrow_left"
            app:layout_constraintRight_toLeftOf="@+id/iv_arrow_right"
            android:layout_marginBottom="16dp"
            app:srcCompat="@drawable/circle"/>
    </com.wzc.t20_vdh.DirectionDragLayout>
</RelativeLayout>

預覽圖和gif效果圖開始部分是一樣的。

在 Activity 中使用這個佈局:

/**
 * 固定向拖動頁面
 * @author wzc
 * @date 2018/5/6
 */
public class DirectionDragActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_directiondrag);
    }
}

DirectionDragActivity 中的程式碼就完成了,之後不會在這裡新增任何程式碼了。餘下的任務都會在 DirectionDragLayout 中完成。

完善 DirectionDragLayout 中的程式碼

  • 隨著手指拖動:
/**
 * 固定向拖動 ViewGroup
 * @author wzc
 * @date 2018/5/6
 */
public class DirectionDragLayout extends ConstraintLayout {

    private final ViewDragHelper mViewDragHelper;
    /**
     * 左邊的View
     */
    private View mReadView;
    /**
     * 右邊的View
     */
    private View mUnlockView;
    /**
     * 上邊的View
     */
    private View mRedbagView;
    /**
     * 中間的View,就是要拖動的View
     */
    private View mHomeView;

    public DirectionDragLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 1, 建立 ViewDragHelper 的例項
        // 參一 : 當前的ViewGroup物件 Parent view to monitor
        // 參二 : 靈敏度 Multiplier for how sensitive the helper should be about detecting
        // the start of a drag. Larger values are more sensitive. 1.0f is normal.
        // 參三 : 提供資訊和接收事件的回撥
        mViewDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragCallback());
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        // 3,獲取View
        mReadView = findViewById(R.id.iv_read);
        mUnlockView = findViewById(R.id.iv_unlock);
        mRedbagView = findViewById(R.id.iv_redbag);
        mHomeView = findViewById(R.id.iv_home);
    }

    // 2, 在onInterceptTouchEvent和onTouchEvent中呼叫VDH的方法
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 通過使用mDragHelper.shouldInterceptTouchEvent(ev)來決定我們是否應該攔截當前的事件
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 通過mDragHelper.processTouchEvent(event)來處理事件
        mViewDragHelper.processTouchEvent(event);
        return true; // 返回 true,表示事件被處理了。
    }

    class ViewDragCallback extends ViewDragHelper.Callback {
        // 4, 這個是必須重寫的方法,
        // 返回true,表示允許捕獲該子view
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            // 當 child 是 mHomeView 時,才允許捕獲。
            return child == mHomeView;
        }
        // 5, 限制被拖拽的子view沿縱軸的運動
        // 如果不重寫,就不能實現縱向的拖動
        // 參一:child 表示正在拖拽的 view
        // 參二:top Attempted motion along the Y axis 理解為拖動的那個view想要到達位置的top值
        // 參三:增量,變化量
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return top;
        }
        // 6, 限制被拖拽的子view沿橫軸的運動
        // 如果不重寫,就不能實現橫向的拖動
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return left;
        }
    }

}

增加的程式碼是第3,4,5,6處,執行一下,效果是 home 可以跟著手指拖動了。

  • 邊界控制

邊界控制是在 clampViewPositionVerticalclampViewPositionHorizontal 中完成。
進行邊界控制需要一些資料:左邊界,右邊界,上邊界,下邊界。
以算出左邊界為例,其它的計算可以類推:
設左邊界為leftBound,home 的寬度為 homeWidth,左邊 view 的中心點 x 座標為 leftViewCenterX。
那麼,當 home 達到左邊界時,home 的中心和左邊 view 的中心是重合的,即:
leftBound + homeWidth / 2 = leftViewCenterX;
leftBound = leftViewCenterX - homeWidth / 2;

onLayout() 方法中獲取需要的邊界值:

    Point mHomeCenterPoint = new Point();
    Point mReadCenterPoint = new Point();
    Point mUnlockCenterPoint = new Point();
    Point mRedbagCenterPoint = new Point();
    Point mHomeOriginalPoint = new Point();

    private int mTopBound;
    private int mBottomBound;
    private int mLeftBound;
    private int mRightBound;
    // 7, 計算邊界
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        mHomeOriginalPoint.x = mHomeView.getLeft();
        mHomeOriginalPoint.y = mHomeView.getTop();

        mHomeCenterPoint.x = mHomeView.getLeft() + mHomeView.getMeasuredWidth() / 2;
        mHomeCenterPoint.y = mHomeView.getTop() + mHomeView.getMeasuredHeight() / 2;

        mReadCenterPoint.x = mReadView.getLeft() + mReadView.getMeasuredWidth() / 2;
        mReadCenterPoint.y = mReadView.getTop() + mReadView.getMeasuredHeight() / 2;

        mUnlockCenterPoint.x = mUnlockView.getLeft() + mUnlockView.getMeasuredWidth() / 2;
        mUnlockCenterPoint.y = mUnlockView.getTop() + mUnlockView.getMeasuredHeight() / 2;

        mRedbagCenterPoint.x = mRedbagView.getLeft() + mRedbagView.getMeasuredWidth() / 2;
        mRedbagCenterPoint.y = mRedbagView.getTop() + mRedbagView.getMeasuredHeight() / 2;

        mTopBound = mRedbagCenterPoint.y - mHomeView.getMeasuredHeight() / 2;
        mBottomBound = mHomeCenterPoint.y - mHomeView.getMeasuredHeight() / 2;
        mLeftBound = mReadCenterPoint.x - mHomeView.getMeasuredWidth() / 2;
        mRightBound = mUnlockCenterPoint.x - mHomeView.getMeasuredWidth() / 2;
    }

上下左右邊界控制

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            // 8, 上下邊界控制
            final int newTop = Math.min(Math.max(mTopBound, top), mBottomBound);
            return newTop;
        }
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            // 9, 左右邊界控制
            final int newLeft = Math.min(Math.max(mLeftBound, left), mRightBound);
            return newLeft;
        }

執行一下程式,可以看到實現了邊界控制。

  • 鬆手返回起始點

重寫 onViewReleased() 方法,這個方法在釋放拖拽的 view 時,會回撥。

    class ViewDragCallback extends ViewDragHelper.Callback {
        // 10, 鬆手返回起始點
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            // 判斷釋放的 view 是不是 mHomeView
            if (releasedChild == mHomeView) {
                // 讓釋放的 view 停在給定的位置
                mViewDragHelper.settleCapturedViewAt(mHomeOriginalPoint.x, mHomeOriginalPoint.y);
                invalidate();
            }
        }
    }
    // 10, 鬆手返回起始點
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mViewDragHelper.continueSettling(true)) {
            invalidate();
        }
    }

onViewReleased() 方法中,判斷釋放的 view 是 mHomeView 時,呼叫 ViewDragHelpersettleCapturedViewAt(int finalLeft, int finalTop) 方法,讓 view 停在給定的位置。這個方法內部是使用 Scroller ,所以記得緊接著這行程式碼需要呼叫 invalidate(); , 並且重寫 computeScroll() 方法。

執行一下程式,達到了效果。

  • 固定向拖拽

宣告兩個標記,用來記錄當前是在進行哪種拖拽,再在 clampViewPositionVertical()clampViewPositionHorizontal() 方法中根據 dx,dy 的絕對值是否大於 0,來決定首先進行哪種拖拽。

一旦一種拖拽先決定好,那麼另外一種拖拽,在本次拖拽過程中就不會生效了。通過這種方法,實現了固定向拖拽的效果。

    /**
     * 當前正在水平拖拽的標記
     */
    private boolean mIsHorizontalDrag = false;
    /**
     * 當前正在豎直拖拽的標記
     */
    private boolean mIsVerticalDrag = false;

    class ViewDragCallback extends ViewDragHelper.Callback {

        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            // 11, 固定向拖拽
            if (mIsHorizontalDrag) {
                return mHomeView.getTop();
            }
            if (Math.abs(dy) > 0) {
                mIsVerticalDrag = true;
            }
            // 8, 上下邊界控制
            final int newTop = Math.min(Math.max(mTopBound, top), mBottomBound);
            return newTop;
        }

        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            // 11, 固定向拖拽
            if (mIsVerticalDrag) {
                return mHomeView.getLeft();
            }
            if (Math.abs(dx) > 0) {
                mIsHorizontalDrag = true;
            }
            // 9, 左右邊界控制
            final int newLeft = Math.min(Math.max(mLeftBound, left), mRightBound);
            return newLeft;
        }
    }

需要注意的是,在新的拖拽發生前,清除掉拖拽標記的值。注意清除標記要在攔截事件前,而不是在 onTouchEvent() 中。寫在後者中,是沒有任何效果的。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 在 VDH 攔截事件前,記得重置拖拽標記
        resetFlags();
        // 通過使用mDragHelper.shouldInterceptTouchEvent(ev)來決定我們是否應該攔截當前的事件
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }

    private void resetFlags() {
        mIsHorizontalDrag = false;
        mIsVerticalDrag = false;
    }
  • 到達邊界時觸發操作

onViewPositionChanged() 中進行,這個方法當拖拽的 view 位置發生變化時被回撥。

    /**
     * 是否到達邊界的標記
     */
    private boolean mIsReachBound;
    class ViewDragCallback extends ViewDragHelper.Callback {

        // 當拖拽的View的位置發生變化的時候回撥(特指capturedview)
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            // 12, 到達邊界時觸發操作
            if (mIsReachBound) {
                return;
            }
            if (left <= mLeftBound) {
                mIsReachBound = true;
                Toast.makeText(getContext(), "到達左邊界", Toast.LENGTH_SHORT).show();
            }
            if (left >= mRightBound) {
                mIsReachBound = true;
                Toast.makeText(getContext(), "到達右邊界", Toast.LENGTH_SHORT).show();
            }
            if (top <= mTopBound) {
                mIsReachBound = true;
                Toast.makeText(getContext(), "到達上邊界", Toast.LENGTH_SHORT).show();
            }
        }
    }

這裡使用標記 mIsReachBound , 為了防止到達邊界時,多次觸發操作。
同樣地,記得在攔截事件前,把此標記的值清除掉。

    private void resetFlags() {
        mIsHorizontalDrag = false;
        mIsVerticalDrag = false;
        mIsReachBound = false;
    }

執行一下程式,實現了全部的需求。

參考