Android 一步一步教你使用ViewDragHelper

希爾瓦娜斯女神發表於2015-08-12

在自定義viewgroup的時候 要重寫onInterceptTouchEventonTouchEvent 這2個方法 是非常麻煩的事情,好在谷歌後來

推出了ViewDragHelper這個類。可以極大方便我們自定義viewgroup.

先看一個簡單效果 一個layout裡有2個圖片 其中有一個可以滑動 一個不能滑

 

這個效果其實還蠻簡單的(原諒我讓臭腳不能動 讓BABY動)

佈局檔案:

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5     android:orientation="vertical">
 6 
 7     <com.example.administrator.viewdragertestapp.DragLayout
 8         android:layout_width="match_parent"
 9         android:layout_height="match_parent"
10         android:orientation="vertical">
11 
12         <ImageView
13             android:id="@+id/iv1"
14             android:layout_width="wrap_content"
15             android:layout_height="wrap_content"
16             android:layout_gravity="center_horizontal"
17             android:src="@drawable/a1"></ImageView>
18 
19         <ImageView
20             android:id="@+id/iv2"
21             android:layout_width="wrap_content"
22             android:layout_height="wrap_content"
23             android:layout_gravity="center_horizontal"
24             android:src="@drawable/a2"></ImageView>
25 
26 
27     </com.example.administrator.viewdragertestapp.DragLayout>
28 
29 </LinearLayout>

 

然後我們看一下自定義的layout 如何實現2個子view 一個可以滑動 一個不能滑動的

 

 1 package com.example.administrator.viewdragertestapp;
 2 
 3 import android.content.Context;
 4 import android.support.v4.widget.ViewDragHelper;
 5 import android.util.AttributeSet;
 6 import android.view.MotionEvent;
 7 import android.view.View;
 8 import android.widget.ImageView;
 9 import android.widget.LinearLayout;
10 import android.widget.TextView;
11 
12 /**
13  * Created by Administrator on 2015/8/12.
14  */
15 public class DragLayout extends LinearLayout {
16 
17     private ViewDragHelper mDragger;
18 
19     private ViewDragHelper.Callback callback;
20 
21     private ImageView iv1;
22     private ImageView iv2;
23 
24     @Override
25     protected void onFinishInflate() {
26         iv1 = (ImageView) this.findViewById(R.id.iv1);
27         iv2 = (ImageView) this.findViewById(R.id.iv2);
28         super.onFinishInflate();
29 
30     }
31 
32     public DragLayout(Context context) {
33         super(context);
34 
35     }
36 
37     public DragLayout(Context context, AttributeSet attrs) {
38         super(context, attrs);
39         callback = new DraggerCallBack();
40         //第二個引數就是滑動靈敏度的意思 可以隨意設定
41         mDragger = ViewDragHelper.create(this, 1.0f, callback);
42     }
43 
44     class DraggerCallBack extends ViewDragHelper.Callback {
45 
46         //這個地方實際上函式返回值為true就代表可以滑動 為false 則不能滑動
47         @Override
48         public boolean tryCaptureView(View child, int pointerId) {
49             if (child == iv2) {
50                 return false;
51             }
52             return true;
53         }
54 
55         @Override
56         public int clampViewPositionHorizontal(View child, int left, int dx) {
57             return left;
58         }
59 
60         @Override
61         public int clampViewPositionVertical(View child, int top, int dy) {
62             return top;
63         }
64     }
65 
66 
67     @Override
68     public boolean onInterceptTouchEvent(MotionEvent ev) {
69         //決定是否攔截當前事件
70         return mDragger.shouldInterceptTouchEvent(ev);
71     }
72 
73     @Override
74     public boolean onTouchEvent(MotionEvent event) {
75         //處理事件
76         mDragger.processTouchEvent(event);
77         return true;
78     }
79 
80 
81 }

 

然後再完善一下這個layout,剛才滑動的時候我們的view 出了螢幕的邊界很不美觀 現在我們修改2個函式 讓滑動的範圍

在這個螢幕之內(準確的說是在這個layout之內,因為我們的佈局檔案layout充滿了螢幕 所以看上去是在螢幕內)

 1  //這個地方實際上left就代表 你將要移動到的位置的座標。返回值就是最終確定的移動的位置。
 2         // 我們要讓view滑動的範圍在我們的layout之內
 3         //實際上就是判斷如果這個座標在layout之內 那我們就返回這個座標值。
 4         //如果這個座標在layout的邊界處 那我們就只能返回邊界的座標給他。不能讓他超出這個範圍
 5         //除此之外就是如果你的layout設定了padding的話,也可以讓子view的活動範圍在padding之內的.
 6 
 7         @Override
 8         public int clampViewPositionHorizontal(View child, int left, int dx) {
 9             //取得左邊界的座標
10             final int leftBound = getPaddingLeft();
11             //取得右邊界的座標
12             final int rightBound = getWidth() - child.getWidth() - leftBound;
13             //這個地方的含義就是 如果left的值 在leftbound和rightBound之間 那麼就返回left
14             //如果left的值 比 leftbound還要小 那麼就說明 超過了左邊界 那我們只能返回給他左邊界的值
15             //如果left的值 比rightbound還要大 那麼就說明 超過了右邊界,那我們只能返回給他右邊界的值
16             return Math.min(Math.max(left, leftBound), rightBound);
17         }
18 
19         //縱向的註釋就不寫了 自己體會
20         @Override
21         public int clampViewPositionVertical(View child, int top, int dy) {
22             final int topBound = getPaddingTop();
23             final int bottomBound = getHeight() - child.getHeight() - topBound;
24             return Math.min(Math.max(top, topBound), bottomBound);
25         }

 

我們看下效果

 

然後我們可以再加上一個回彈的效果,就是你把babay拉倒一個位置 然後鬆手他會自動回彈到初始位置

其實思路很簡單 就是你鬆手的時候 回到初始的座標位置即可。

  1 package com.example.administrator.viewdragertestapp;
  2 
  3 import android.content.Context;
  4 import android.graphics.Point;
  5 import android.support.v4.widget.ViewDragHelper;
  6 import android.util.AttributeSet;
  7 import android.view.MotionEvent;
  8 import android.view.View;
  9 import android.widget.ImageView;
 10 import android.widget.LinearLayout;
 11 import android.widget.TextView;
 12 
 13 /**
 14  * Created by Administrator on 2015/8/12.
 15  */
 16 public class DragLayout extends LinearLayout {
 17 
 18     private ViewDragHelper mDragger;
 19 
 20     private ViewDragHelper.Callback callback;
 21 
 22     private ImageView iv1;
 23     private ImageView iv2;
 24 
 25     private Point initPointPosition = new Point();
 26 
 27     @Override
 28     protected void onFinishInflate() {
 29         iv1 = (ImageView) this.findViewById(R.id.iv1);
 30         iv2 = (ImageView) this.findViewById(R.id.iv2);
 31         super.onFinishInflate();
 32 
 33     }
 34 
 35     public DragLayout(Context context) {
 36         super(context);
 37 
 38     }
 39 
 40     public DragLayout(Context context, AttributeSet attrs) {
 41         super(context, attrs);
 42         callback = new DraggerCallBack();
 43         //第二個引數就是滑動靈敏度的意思 可以隨意設定
 44         mDragger = ViewDragHelper.create(this, 1.0f, callback);
 45     }
 46 
 47     class DraggerCallBack extends ViewDragHelper.Callback {
 48 
 49         //這個地方實際上函式返回值為true就代表可以滑動 為false 則不能滑動
 50         @Override
 51         public boolean tryCaptureView(View child, int pointerId) {
 52             if (child == iv2) {
 53                 return false;
 54             }
 55             return true;
 56         }
 57 
 58 
 59         //這個地方實際上left就代表 你將要移動到的位置的座標。返回值就是最終確定的移動的位置。
 60         // 我們要讓view滑動的範圍在我們的layout之內
 61         //實際上就是判斷如果這個座標在layout之內 那我們就返回這個座標值。
 62         //如果這個座標在layout的邊界處 那我們就只能返回邊界的座標給他。不能讓他超出這個範圍
 63         //除此之外就是如果你的layout設定了padding的話,也可以讓子view的活動範圍在padding之內的.
 64 
 65         @Override
 66         public int clampViewPositionHorizontal(View child, int left, int dx) {
 67             //取得左邊界的座標
 68             final int leftBound = getPaddingLeft();
 69             //取得右邊界的座標
 70             final int rightBound = getWidth() - child.getWidth() - leftBound;
 71             //這個地方的含義就是 如果left的值 在leftbound和rightBound之間 那麼就返回left
 72             //如果left的值 比 leftbound還要小 那麼就說明 超過了左邊界 那我們只能返回給他左邊界的值
 73             //如果left的值 比rightbound還要大 那麼就說明 超過了右邊界,那我們只能返回給他右邊界的值
 74             return Math.min(Math.max(left, leftBound), rightBound);
 75         }
 76 
 77         //縱向的註釋就不寫了 自己體會
 78         @Override
 79         public int clampViewPositionVertical(View child, int top, int dy) {
 80             final int topBound = getPaddingTop();
 81             final int bottomBound = getHeight() - child.getHeight() - topBound;
 82             return Math.min(Math.max(top, topBound), bottomBound);
 83         }
 84 
 85         @Override
 86         public void onViewReleased(View releasedChild, float xvel, float yvel) {
 87             //鬆手的時候 判斷如果是這個view 就讓他回到起始位置
 88             if (releasedChild == iv1) {
 89                 //這邊程式碼你跟進去去看會發現最終呼叫的是startScroll這個方法 所以我們就明白還要在computeScroll方法裡重新整理
 90                 mDragger.settleCapturedViewAt(initPointPosition.x, initPointPosition.y);
 91                 invalidate();
 92             }
 93         }
 94     }
 95 
 96     @Override
 97     public void computeScroll() {
 98         if (mDragger.continueSettling(true)) {
 99             invalidate();
100         }
101     }
102 
103     @Override
104     protected void onLayout(boolean changed, int l, int t, int r, int b) {
105         super.onLayout(changed, l, t, r, b);
106         //佈局完成的時候就記錄一下位置
107         initPointPosition.x = iv1.getLeft();
108         initPointPosition.y = iv1.getTop();
109     }
110 
111     @Override
112     public boolean onInterceptTouchEvent(MotionEvent ev) {
113         //決定是否攔截當前事件
114         return mDragger.shouldInterceptTouchEvent(ev);
115     }
116 
117     @Override
118     public boolean onTouchEvent(MotionEvent event) {
119         //處理事件
120         mDragger.processTouchEvent(event);
121         return true;
122     }
123 
124 
125 }

看下效果:

到這裡有人會發現 這樣做的話imageview就無法響應點選事件了。繼續修改這個程式碼讓iv可以響應點選事件並且可以響應

滑動事件。

首先修改xml 把click屬性設定為true 這個程式碼就不上了,然後修改我們的程式碼 其實就是增加2個函式

1  @Override
2         public int getViewHorizontalDragRange(View child) {
3             return getMeasuredWidth() - child.getMeasuredWidth();
4         }
5 
6         @Override
7         public int getViewVerticalDragRange(View child) {
8             return getMeasuredHeight()-child.getMeasuredHeight();
9         }

然後看下效果:

這個地方 如果你學過android 事件傳遞的話很好理解,因為如果你子view可以響應點選事件的話,那說明你消費了這個事件。

如果你消費了這個事件話 就會先走dragger的 onInterceptTouchEvent這個方法。我們跟進去看看這個方法

 1   case MotionEvent.ACTION_MOVE: {
 2                 if (mInitialMotionX == null || mInitialMotionY == null) break;
 3 
 4                 // First to cross a touch slop over a draggable view wins. Also report edge drags.
 5                 final int pointerCount = MotionEventCompat.getPointerCount(ev);
 6                 for (int i = 0; i < pointerCount; i++) {
 7                     final int pointerId = MotionEventCompat.getPointerId(ev, i);
 8                     final float x = MotionEventCompat.getX(ev, i);
 9                     final float y = MotionEventCompat.getY(ev, i);
10                     final float dx = x - mInitialMotionX[pointerId];
11                     final float dy = y - mInitialMotionY[pointerId];
12 
13                     final View toCapture = findTopChildUnder((int) x, (int) y);
14                     final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
15                     if (pastSlop) {
16                         // check the callback's
17                         // getView[Horizontal|Vertical]DragRange methods to know
18                         // if you can move at all along an axis, then see if it
19                         // would clamp to the same value. If you can't move at
20                         // all in every dimension with a nonzero range, bail.
21                         final int oldLeft = toCapture.getLeft();
22                         final int targetLeft = oldLeft + (int) dx;
23                         final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
24                                 targetLeft, (int) dx);
25                         final int oldTop = toCapture.getTop();
26                         final int targetTop = oldTop + (int) dy;
27                         final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
28                                 (int) dy);
29                         final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
30                                 toCapture);
31                         final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
32                         if ((horizontalDragRange == 0 || horizontalDragRange > 0
33                                 && newLeft == oldLeft) && (verticalDragRange == 0
34                                 || verticalDragRange > 0 && newTop == oldTop)) {
35                             break;
36                         }
37                     }
38                     reportNewEdgeDrags(dx, dy, pointerId);
39                     if (mDragState == STATE_DRAGGING) {
40                         // Callback might have started an edge drag
41                         break;
42                     }
43 
44                     if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
45                         break;
46                     }
47                 }
48                 saveLastMotion(ev);
49                 break;
50             }

注意看29行到末尾 你會發現 只有當

horizontalDragRange 和verticalDragRange  

大於0的時候 對應的move事件才會捕獲。否則就是丟棄直接丟給子view自己處理了

 

另外還有一個效果就是 假如我們的 baby被拉倒了邊界處,

我們的手指不需要拖動baby這個iv,手指直接在邊界的其他地方拖動此時也能把這個iv拖走。

這個效果其實也可以實現,無非就是捕捉你手指在邊界處的動作 然後傳給你要拖動的view即可。

程式碼非常簡單 兩行即可

再重寫一個回撥函式 然後加個監聽

1   @Override
2         public void onEdgeDragStarted(int edgeFlags, int pointerId) {
3             mDragger.captureChildView(iv1, pointerId);
4         }
1         mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);

這個效果在模擬器上不知道為啥 滑鼠拖不動,GIF圖片我就不上了大家可以自己在手機裡跑一下就可以。

 

上面的那些效果實際上都是DrawerLayout 等類似抽屜效果裡經常用到的函式,有興趣的同學可以

看下原始碼。

 

相關文章