使用 ViewDragHelper 實現沿三個方向拖動的例子
1. 需求
先看一下效果圖:
效果圖
2. 需求分析
當手指拖動中間的原形圖示(稱為 home)時,只能向左,向右,向上三個方向拖動;
當 home 向左,向右,向上拖動時,home 的中心不能超過左,右,上三個圖示的中心;
當 home 向左,向右,向上拖動時,若 home 的中心,和左,右,上三個圖示的中心重合,就觸發一定的操作,這裡使用 toast 代替;
當 home 向左,向右,向上拖動時,若 home 的中心未達到左,右,上三個圖示的中心並且手指鬆開,那麼 home 會回到它原來的位置。
3. 實現
3.1 實現方案選擇
我最開始接到的需求是隻有左向和右向拖動,沒有向上的拖動,採用的方法是監聽 home 圖示的觸控事件,在 onTouch(View v, MotionEvent event)
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 可以跟著手指拖動了。
- 邊界控制
邊界控制是在 clampViewPositionVertical
和 clampViewPositionHorizontal
中完成。
進行邊界控制需要一些資料:左邊界,右邊界,上邊界,下邊界。
以算出左邊界為例,其它的計算可以類推:
設左邊界為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
時,呼叫 ViewDragHelper
的 settleCapturedViewAt(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;
}
執行一下程式,實現了全部的需求。