How to use Android ViewDragHelper
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
若你喜歡這篇文章歡迎推薦給大家
若你喜歡我的部落格歡迎追隨我