How to use Android ViewDragHelper

Willy Chang
11 min readMar 13, 2017

--

What’s 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.

2013 Google I/O 大會上介紹一款新的layout:DrawerLayout,而現在這個layout已經被廣泛使用,其中DrawerLayout中拖動(drag)事件就是使用ViewDragHelper(VDH)來做處理,有興趣的人可自行參考android.support.v4.widget.DrawerLayout。

Android官方對它的定義是(以下內容將ViewDragHelper 簡稱為VDH)

  • 可用於自定義ViewGroup
  • 提供有效操作和狀態追蹤
  • 可在parent ViewGroup 中拖移和重新確認child view改變的位置

Application of Android ViewDragHelper

前陣子想實作類似vertical viewpager頁面切換的效果,但一直沒有找到比較好的解法,stackoverflow有一些方法提供參考,不過這些方法都存在一些問題。

我參考了android-vertical-slide-view的作法並針對內部細節進行調整優化,實作前我們需要了解以下概念:

  • 需要建立兩個垂直且緊黏在一起的View,一個顯示在畫面上,另一個顯示在畫面外。
  • 若view中放入能夠scroll的元件時需要特別注意TouchEvent傳遞狀況,何時要將event丟給parent view又或者自行處理。
  • ViewDragHelper.Callback 是連結VDH和parent view的溝通橋梁。
  • VDH可指定view拖動的方向

Analysis of ViewDragHelper API

首先需要建立一個ViewDragHelper.Callback來處理view拖動,其拖動的對象必須為ViewGroup。

ViewDragHelper.create(this, 1.0f, new ViewDragHelperCallback());
  • 參數1: 要拖動的ViewGroup
  • 參數2: 敏感度,數值越大越敏感
  • 參數3:VDH的callback

要讓VDH能夠處理相關的拖動事件就需要將拖動時觸發事件狀態傳給VDH,所以我們要針對onInterceptTouchEvent、onTouchEvent 做特別的處理。

  • onInterceptTouchEvent:將攔截的事件都交給VDH來處理。

當view1 和 view2切換時,onInterceptTouchEvent return false

return false:不攔截此touch event

return true:攔截此touch event

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

if (mFirstView.getTop() < 0 && mFirstView.getBottom() > 0) {
return false;
}

boolean yScroll = mGestureDetectorCompat.onTouchEvent(ev);
boolean shouldIntercept = mVdh.shouldInterceptTouchEvent(ev);

if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
mVdh.processTouchEvent(ev);
}

return yScroll && shouldIntercept;
}
  • onTouchEvent:統一交給VDH並由VDH.callback來處理拖動效果。

需注意processTouchEvent(event)可能會造成crash,所以若要將程式碼加入你的專案中,記得使用try catch比較保險,相關內容可以參考

public boolean onTouchEvent(MotionEvent event) {

try {
mViewDragHelper.processTouchEvent(event);
} catch (Exception ex) {
ex.printStackTrace();
}
return true;
}
  • tryCaptureView:決定parent view中哪些child view可被拖動,假設有兩個child view(mFirstView 和 mSecondView),如下:若只想拖動mFirstView只需return child == mFirstView 即可!!

pointerId:區分多點觸控時的 id position

public boolean tryCaptureView(View child, int pointerId) {
return child == mFirstView;
}
  • clampViewPositionVertical(View child, int top, int dy)

return value:將要移動到的位置座標

參數2:當前滑動view的最終y座標位置

Q1:若只想讓view在layout中拖動,該如何做呢?

A1:當拖動的距離超過layout邊界時,直接回傳邊界座標即可。

Q2:若layout中有padding能讓view拖動範圍不超過padding嗎?

A2:可以,只需在回傳時加入padding計算。

接下來實作的概念很簡單,當mFirstView "top > 0" 時,表示mFirstView已經滑到頂部,反之mSecondView "top < 0" 時,表示mSecondView已經滑動底部。

public int clampViewPositionVertical(View child, int top, int dy) {
int finalTop = top;

if (child == mFirstView) {
if (top > 0) {
finalTop = 0;
}
} else if (child == mSecondView) {
if (top < 0) {
finalTop = 0;
}
}

return finalTop;
}

若沒有針對top、bottom邊界進行偵測會發生什麼事情呢?

若滑動不受限制,當滑到頂部或底部時就會直接滑出邊界
  • onViewPositionChanged(View changed, int left, int top, int dx, int dy)

位置改變時會call 此method

可在滑動時更改 scale進行大小縮放

dx, dy:新、舊位置的偏移量

public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {    if (changedView == mView1) {
mView2.offsetTopAndBottom(dy);
} else if (changedView == mView2) {
mView1.offsetTopAndBottom(dy);
}

ViewCompat.postInvalidateOnAnimation(ViewDragLayout.this);
}
  • onViewReleased(View releasedChild, float xvel, float yvel)

滑動結束時會call此method

需要連同computeScroll() 一起實作,因為onViewReleased 最後會call startScroll method

xvel: 水平方向的速度,往右為正

yvel: 垂直方向的速度,往下為正

  • computeScroll()

滑動時鬆手後會以一定的速度繼續自動滑動並逐漸停止,類似拋東西鬆手後自動滑動到指定位置

@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}

最後實作的概念其實很簡單,主要是當view滑動超過某個距離門檻或者滑動速度超過某個固定數值時,就改變畫面相關位置。

public void onViewReleased(View release, float xvel, float yvel) {
int finalTop = 0;

if (release == mFirstView) {
if (yvel < -VEL_THRESHOLD
|| (release.getTop() < -DISTANCE_THRESHOLD)) {

finalTop = -mFirstHeight;
}
} else if (release == mSecondView) {
if (yvel > VEL_THRESHOLD
|| (release.getTop() > DISTANCE_THRESHOLD)) {

finalTop = mFirstHeight;
}
}

if (mVdh.smoothSlideViewTo(release, 0, finalTop)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}

KeyPoint

(1)注意view1 與 view2切換後最新位置

(2)ViewDragHelper callback調用method順序為何?

  • tryCaptureView
  • clampViewPositionVertical
  • onViewPositionChanged
  • onViewReleased

(3) 若需要滾動效果,必須override computeScroll()

Conclusion

ViewDragHelper 確實幫我們省下許多drag麻煩問題且能透過簡單的callback機制完成畫面切換,下一篇文章中將介紹合併RecyclerView使用方法,到時候將會遇到一些較為棘手的問題,下篇文章中再做更詳細的介紹吧。

最後附上Sample Code

若你喜歡這篇文章歡迎推薦給大家

若你喜歡我的部落格歡迎追隨我

https://play.google.com/store/apps/details?id=com.devwilly.tutorial.viewdraghelperex

Reference

--

--