前言
隨著入Android這個坑的時間越來越長,愈加覺得深入掌握原理以及技術(shù)輸出的重要性,會使用輪子和造一個好輪子還是有天壤之別的。授人以魚不如授人以漁,將一些經(jīng)驗分享出來,希望能夠讓更多的人更加深入地理解它,并幫助到有需要的朋友。本系列分為三篇,會由淺至深地對DrageHelper 進(jìn)行詳細(xì)講解。
目錄
ViewDragHelper 的介紹以及初步使用請閱讀這篇:
ViewDragHelper (一)- 介紹及簡單用例(入門篇)
ViewDragHelper 的源碼以及Callback的詳情介紹請閱讀這篇:
ViewDragHelper (二)- 源碼及原理解讀(進(jìn)階篇)
利用DrageHelper 打造仿陌陌APP視頻播放頁的demo請閱讀這篇:
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.
DrageHelper 它是Google官方推出的手勢滑動輔助類,極大程度地簡化了我們對控件的手勢滑動跟蹤及處理。讓我們能夠更加便捷地開發(fā)自定義ViewGroup控件,實現(xiàn)拖拽以及彈性滾動等功能。事實上,官方的SlidingPaneLayout和DrawerLayout都是利用ViewDragHelper實現(xiàn)的。掌握它,可以一定程度地減輕我們開發(fā)工作難度以及投入精力。
使用入門示例
接下來,我們主要通過一個簡單的拖拽以及回彈的demo(類似于QQ空間視頻播放頁),來講解如何利用DrageHelper 打造一個 ViewGroup 控件。
QQ空間視頻播放頁效果圖:

大致步驟如下:
第一步:
創(chuàng)建一個DraggableView類繼承自ViewGroup(或者也可用 FrameLayout , RelativeLayout, LinearLayout等)。
package com.test.demo;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.RelativeLayout;
/**
* Created by 小嵩 on 2017/9/10.
*/
public class MyDraggableView extends RelativeLayout{
private ViewDragHelper viewDragHelper;
public MyDraggableView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView() {
viewDragHelper = ViewDragHelper.create(this, 1.0f, new DraggableViewCallback(this));
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return viewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
viewDragHelper.processTouchEvent(event);
return true;
}
}
在onInterceptTouchEvent方法中,通過viewDragHelper.shouldInterceptTouchEvent(event)來決定我們是否應(yīng)該攔截當(dāng)前的事件,如果返回的是True,則會觸發(fā)onTouchEvent。
在onTouchEvent方法中,通過viewDragHelper.processTouchEvent(event)將事件分發(fā)給viewDragHelper。
對Android的事件分發(fā)機(jī)制若還不太理解的話,可自行查資料補(bǔ)一下相關(guān)知識。
第二步:
在init方法中用ViewDragHelper的靜態(tài)方法實例化ViewDragHelper對象
viewDragHelper = ViewDragHelper.create(this, 1.0f, new VerticalDraggableViewCallback(this));
其中第一個參數(shù)指的當(dāng)前的ViewGroup對象,第二個sensitivity參數(shù)指的是對滑動檢測的靈敏度,越大越敏感,所需觸發(fā)滑動的距離越小,默認(rèn)傳1.0f 即可。它的源碼如下:
/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param sensitivity Multiplier for how sensitive the helper should be about detecting
* the start of a drag. Larger values are more sensitive. 1.0f is normal.
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/
public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
final ViewDragHelper helper = create(forParent, cb);
helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
return helper;
}
由: helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); 可以顯而易見地看出,其實就是mTouchSlop 除以我們傳入的sensitivity然后重新賦值。而這個mTouchSlop 是怎么來的呢? 接著看源碼,發(fā)現(xiàn)是這一段代碼進(jìn)行賦值的:
final ViewConfiguration vc = ViewConfiguration.get(context);
mTouchSlop = vc.getScaledTouchSlop();
由此可見,mTouchSlop 它是獲取的系統(tǒng)判定是否觸發(fā)移動事件的閾值。即:單次移動大于這個值,才會判定是MOVE操作。
第三個參數(shù)為靜態(tài)回調(diào)對象CallBack,我們接下來實現(xiàn)相關(guān)CallBack方法來操作拖拽的View。
第三步:
實現(xiàn)ViewDragHelper.Callback的相關(guān)方法。
/**
* Created by 小嵩 on 2017/8/30.
*/
public class DraggableViewCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
}
其中,ViewDragHelper.Callback 是一個內(nèi)部靜態(tài)抽象類, tryCaptureView是必須實現(xiàn)的方法,其余是可選重寫的方法,一般來說,我們重寫:
clampViewPositionHorizontalView
clampViewPositionVertical
onViewReleased
onViewPositionChanged
這四個方法即可。更多方法的詳情及含義,可閱讀DrageHelper — 源碼深入解析(第二篇)。
tryCaptureView,可用于自由判定哪個子控件可被拖拽,返回true代表可拖拽,false則禁止。
第四步:
分別在 clampViewPositionVertical 和clampViewPositionHorizontal 方法中對它的可滑動邊界進(jìn)行控制。left , top 分別為即將移動到的位置,比如我希望只在的水平方向移動,則進(jìn)行如下處理:
/**
* 子控件水平方向位置改變時觸發(fā)
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//屏蔽掉水平方向
return 0;
}
同時,若我們只希望子控件向下平移,則做以下處理:
/**
* 子控件豎直方向位置改變時觸發(fā)
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
//不能滑出頂部
return Math.max(top, 0);
}
第五步:
在onViewReleased 方法中獲取移動距離,判斷拖拽距離是否超過閾值。若超過閾值,則執(zhí)行關(guān)閉動畫,否則處理回彈,Callback完整代碼如下:
package com.test.demo;
import android.support.v4.widget.ViewDragHelper;
import android.util.Log;
import android.view.View;
import android.view.ViewConfiguration;
/**
* ViewDragHelper.Callback 拖拽事件監(jiān)聽回調(diào)
*
* @author 小嵩
*/
class DraggableViewCallback extends ViewDragHelper.Callback {
private static final String TAG = "DraggableViewCallback";
private static float Y_MIN_VELOCITY = 300;//豎直方向關(guān)閉最小值 px
private MyDraggableView mDraggableView;
public DraggableViewCallback(MyDraggableView draggableView) {
this.mDraggableView = draggableView;
Y_MIN_VELOCITY = mDraggableView.getHeight() / 3;
}
/**
* 子控件位置改變時觸發(fā)(包括X和Y軸方向)
*
* @param left position.
* @param top position.
* @param dx change in X position from the last call.
* @param dy change in Y position from the last call.
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
mDraggableView.onViewPositionChanged(changedView, left, top, dx, dy);
}
/**
* 子控件豎直方向位置改變時觸發(fā)
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
//不能滑出頂部
return Math.max(top, 0);
}
/**
* 子控件水平方向位置改變時觸發(fā)
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
//屏蔽掉水平方向
return 0;
}
/**
* 手指松開時觸發(fā)
*
* @param releasedChild the captured child view now being released.
* @param xVel X velocity of the pointer as it left the screen in pixels per second.
* @param yVel Y velocity of the pointer as it left the screen in pixels per second.
*/
@Override
public void onViewReleased(View releasedChild, float xVel, float yVel) {
super.onViewReleased(releasedChild, xVel, yVel);
Log.d(TAG, "onViewReleased");
int top = releasedChild.getTop(); //獲取子控件Y值
int left = releasedChild.getLeft(); //獲取子控件X值
if (Math.abs(left) <= Math.abs(top)) {//若為豎直滑動
triggerOnReleaseActionsWhileVerticalDrag(top);
}
}
@Override
public boolean tryCaptureView(View view, int pointerId) {
return true;
}
/**
* 計算豎直方向的滑動
*/
private void triggerOnReleaseActionsWhileVerticalDrag(float yVel) {
if (yVel > 0 && yVel >= Y_MIN_VELOCITY) {
mDraggableView.closedToBottom();
Log.d(TAG, "ReleaseVerticalDrag" + ", closeToBottom");
} else {
mDraggableView.onReset();
Log.d(TAG, "ReleaseVerticalDrag" + ", onReset");
}
}
}
第六步:
在自定義控件MyDraggableView中處理監(jiān)聽回調(diào)事件。手指松開時,會有兩種情況:
1.當(dāng)拖拽滑動距離未達(dá)到我們設(shè)定的值,則重置到原來位置:
public void onReset() {
Log.d(TAG, "onReset");
viewDragHelper.settleCapturedViewAt(0, 0);
ViewCompat.postInvalidateOnAnimation(this);
}
2.拖拽滑動距離超過設(shè)定的值,滑向底部關(guān)閉:
public void closedToBottom() {
Log.d(TAG, "closedToBottom");
if (viewDragHelper.smoothSlideViewTo(this, 0, getHeight())) {
ViewCompat.postInvalidateOnAnimation(this);
notifyClosedToBottomListener();
}
}
其中,我們重寫了computeScroll 方法,以便在手指松開時,觸發(fā)系統(tǒng)自動滑動。代碼如下:
@Override
public void computeScroll() {
if (viewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
computeScroll的具體原理這里就不闡述了,不懂的話可以自行Google/百度 查找 View 的computeScroll 實現(xiàn)原理以及源碼。
到這兒我們就已經(jīng)實現(xiàn)了拖拽下拉關(guān)閉的功能了,效果演示如下:

類似地,如果我們需要實現(xiàn)向左或者向右拖拽回彈或者關(guān)閉的功能,只需要把ViewDragHelper.Callback里面clampViewPositionHorizontal以及clampViewPositionVertical方法稍加修改,然后在onViewReleased回調(diào)一下,執(zhí)行viewDragHelper.smoothSlideViewTo()方法讓View平順移動到指定位置即可。具體實際情況可自行實踐操作一波。
稍微修改一下代碼,改成左右拖拽,代碼如下:
/**
* 子控件豎直方向位置改變時觸發(fā)
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
// return Math.max(top, 0);//不能滑出頂部
return 0;
}
/**
* 子控件水平方向位置改變時觸發(fā)
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// return 0;
return left;
}
/**
* 手指松開時觸發(fā)
*/
@Override
public void onViewReleased(View releasedChild, float xVel, float yVel) {
super.onViewReleased(releasedChild, xVel, yVel);
Log.d(TAG, "onViewReleased");
int top = releasedChild.getTop(); //獲取子控件Y值
int left = releasedChild.getLeft(); //獲取子控件X值
if (Math.abs(left) <= Math.abs(top)) {//若為豎直滑動
triggerOnReleaseActionsWhileVerticalDrag(top);
} else {
triggerOnReleaseActionsWhileHorizontalDrag(left);
}
}
/**
* 計算水平方向
*/
private void triggerOnReleaseActionsWhileHorizontalDrag(int xVel) {
if (xVel > 0 && xVel >= X_MIN_VELOCITY) {
mDraggableView.closedToRight();
Log.d(TAG, "ReleaseVerticalDrag" + ", closedToRight");
} else if (xVel < 0 && Math.abs(xVel) >= X_MIN_VELOCITY) {
mDraggableView.closedToLeft();
Log.d(TAG, "ReleaseVerticalDrag" + ", closedToLeft");
} else {
mDraggableView.onReset();
Log.d(TAG, "ReleaseVerticalDrag" + ", onReset");
}
}
效果如下:

結(jié)語:
讀完這篇文章之后,若覺得有哪里寫的不夠詳細(xì)或是有更多的建議,歡迎指出~ 也非常感謝各位的支持和收藏點贊。
下一篇將圍繞源碼進(jìn)行解析它的運(yùn)行原理以及所提供的方法,文章鏈接:ViewDragHelper (二)- 源碼及原理解讀(進(jìn)階篇)
這篇文章將會詳細(xì)講解ViewDragHelper它提供的方法所代表的含義,以及實現(xiàn)原理等。相信讀完理解這篇文章的內(nèi)容之后,對ViewDragHelper的基本操作會有一個更全面的理解。
(By the way,最近工作有點忙,一篇文章躺在草稿箱N久,零零散散抽時間總算出爐了。第二篇和第三篇后續(xù)會抽空抓緊趕時間寫出來)