Android ViewDragHelper详解

ViewDragHelper这个类。非常方便的帮我们实现了自定义View(ViewGroup)滑动相关的功能.

下面一步一步学习:

先看一个简单效果 一个layout里有2个图片 其中有一个可以滑动 一个不能滑

代码:

xml文件:

 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     <app.com.drag.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/p1"/>
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/p2"/>
25 
26 
27     </app.com.drag.DragLayout>
28 
29 </LinearLayout>
View Code

然后就是自定义的layout 如何实现2个子view 一个可以滑动 一个不能滑动的

 1 public class DragLayout extends LinearLayout {
 2 
 3     private ViewDragHelper mDragger;
 4 
 5     private ViewDragHelper.Callback callback;
 6 
 7     private ImageView iv1;
 8     private ImageView iv2;
 9 
10     @Override
11     protected void onFinishInflate() {
12         iv1 = (ImageView) this.findViewById(R.id.iv1);
13         iv2 = (ImageView) this.findViewById(R.id.iv2);
14         super.onFinishInflate();
15 
16     }
17 
18     public DragLayout(Context context) {
19         super(context);
20 
21     }
22 
23     public DragLayout(Context context, AttributeSet attrs) {
24         super(context, attrs);
25         callback = new DraggerCallBack();
26         //第二个参数就是滑动灵敏度的意思 可以随意设置
27         mDragger = ViewDragHelper.create(this, 1.0f, callback);
28     }
29 
30     class DraggerCallBack extends ViewDragHelper.Callback {
31 
32         //这个地方实际上函数返回值为true就代表可以滑动 为false 则不能滑动
33         @Override
34         public boolean tryCaptureView(View child, int pointerId) {
35             if (child == iv2) {
36                 return false;
37             }
38             return true;
39         }
40 
41         @Override
42         public int clampViewPositionHorizontal(View child, int left, int dx) {
43             return left;
44         }
45 
46         @Override
47         public int clampViewPositionVertical(View child, int top, int dy) {
48             return top;
49         }
50     }
51 
52 
53     @Override
54     public boolean onInterceptTouchEvent(MotionEvent ev) {
55         //决定是否拦截当前事件
56         return mDragger.shouldInterceptTouchEvent(ev);
57     }
58 
59     @Override
60     public boolean onTouchEvent(MotionEvent event) {
61         //处理事件
62         mDragger.processTouchEvent(event);
63         return true;
64     }
65 
66 
67 }
View Code

然后再完善一下这个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         }
View Code

看下效果

然后我们可以再加上一个回弹的效果,就是你把babay拉倒一个位置 然后松手他会自动回弹到初始位置

其实思路很简单 就是你松手的时候 回到初始的坐标位置即可。

  1 public class DragLayout extends LinearLayout {
  2 
  3     private ViewDragHelper mDragger;
  4 
  5     private ViewDragHelper.Callback callback;
  6 
  7     private ImageView iv1;
  8     private ImageView iv2;
  9 
 10     private Point initPointPosition = new Point();
 11 
 12     @Override
 13     protected void onFinishInflate() {
 14         iv1 = (ImageView) this.findViewById(R.id.iv1);
 15         iv2 = (ImageView) this.findViewById(R.id.iv2);
 16         super.onFinishInflate();
 17 
 18     }
 19 
 20     public DragLayout(Context context) {
 21         super(context);
 22 
 23     }
 24 
 25     public DragLayout(Context context, AttributeSet attrs) {
 26         super(context, attrs);
 27         callback = new DraggerCallBack();
 28         //第二个参数就是滑动灵敏度的意思 可以随意设置
 29         mDragger = ViewDragHelper.create(this, 1.0f, callback);
 30     }
 31 
 32     class DraggerCallBack extends ViewDragHelper.Callback {
 33 
 34         //这个地方实际上函数返回值为true就代表可以滑动 为false 则不能滑动
 35         @Override
 36         public boolean tryCaptureView(View child, int pointerId) {
 37             if (child == iv2) {
 38                 return false;
 39             }
 40             return true;
 41         }
 42 
 43 
 44         //这个地方实际上left就代表 你将要移动到的位置的坐标。返回值就是最终确定的移动的位置。
 45         // 我们要让view滑动的范围在我们的layout之内
 46         //实际上就是判断如果这个坐标在layout之内 那我们就返回这个坐标值。
 47         //如果这个坐标在layout的边界处 那我们就只能返回边界的坐标给他。不能让他超出这个范围
 48         //除此之外就是如果你的layout设置了padding的话,也可以让子view的活动范围在padding之内的.
 49 
 50         @Override
 51         public int clampViewPositionHorizontal(View child, int left, int dx) {
 52             //取得左边界的坐标
 53             final int leftBound = getPaddingLeft();
 54             //取得右边界的坐标
 55             final int rightBound = getWidth() - child.getWidth() - leftBound;
 56             //这个地方的含义就是 如果left的值 在leftbound和rightBound之间 那么就返回left
 57             //如果left的值 比 leftbound还要小 那么就说明 超过了左边界 那我们只能返回给他左边界的值
 58             //如果left的值 比rightbound还要大 那么就说明 超过了右边界,那我们只能返回给他右边界的值
 59             return Math.min(Math.max(left, leftBound), rightBound);
 60         }
 61 
 62         //纵向的注释就不写了 自己体会
 63         @Override
 64         public int clampViewPositionVertical(View child, int top, int dy) {
 65             final int topBound = getPaddingTop();
 66             final int bottomBound = getHeight() - child.getHeight() - topBound;
 67             return Math.min(Math.max(top, topBound), bottomBound);
 68         }
 69 
 70         @Override
 71         public void onViewReleased(View releasedChild, float xvel, float yvel) {
 72             //松手的时候 判断如果是这个view 就让他回到起始位置
 73             if (releasedChild == iv1) {
 74                 //这边代码你跟进去去看会发现最终调用的是startScroll这个方法 所以我们就明白还要在computeScroll方法里刷新
 75                 mDragger.settleCapturedViewAt(initPointPosition.x, initPointPosition.y);
 76                 invalidate();
 77             }
 78         }
 79     }
 80 
 81     @Override
 82     public void computeScroll() {
 83         if (mDragger.continueSettling(true)) {
 84             invalidate();
 85         }
 86     }
 87 
 88     @Override
 89     protected void onLayout(boolean changed, int l, int t, int r, int b) {
 90         super.onLayout(changed, l, t, r, b);
 91         //布局完成的时候就记录一下位置
 92         initPointPosition.x = iv1.getLeft();
 93         initPointPosition.y = iv1.getTop();
 94     }
 95 
 96     @Override
 97     public boolean onInterceptTouchEvent(MotionEvent ev) {
 98         //决定是否拦截当前事件
 99         return mDragger.shouldInterceptTouchEvent(ev);
100     }
101 
102     @Override
103     public boolean onTouchEvent(MotionEvent event) {
104         //处理事件
105         mDragger.processTouchEvent(event);
106         return true;
107     }
108 
109 
110 }
View Code

看下效果:

到这里有人会发现 这样做的话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 }
View Code

然后看下效果:

这个地方 如果你学过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             }
View Code

注意看29行到末尾 你会发现 只有当

horizontalDragRange 和verticalDragRange  

大于0的时候 对应的move事件才会捕获。否则就是丢弃直接丢给子view自己处理了

另外还有一个效果就是 假如我们的 baby被拉倒了边界处,

我们的手指不需要拖动baby这个iv,手指直接在边界的其他地方拖动此时也能把这个iv拖走。

这个效果其实也可以实现,无非就是捕捉你手指在边界处的动作 然后传给你要拖动的view即可。

代码非常简单 两行即可

再重写一个回调函数 然后加个监听

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

这个效果在模拟器上不知道为啥 鼠标拖不动,GIF图片我就不上了大家可以自己在手机里跑一下就可以。

上面的那些效果实际上都是DrawerLayout 等类似抽屉效果里经常用到的函数,有兴趣的同学可以看下源码。

上面的博客内容是参考http://www.cnblogs.com/punkisnotdead/p/4724825.html 写的.只是记录下,自己日后方便看,有问题大家一起交流

下面在总结下ViewDragHelper的调用时机:

shouldInterceptTouchEvent:

DOWN:
    getOrderedChildIndex(findTopChildUnder)
    ->onEdgeTouched

MOVE:
    getOrderedChildIndex(findTopChildUnder)
    ->getViewHorizontalDragRange & 
      getViewVerticalDragRange(checkTouchSlop)(MOVE中可能不止一次)
    ->clampViewPositionHorizontal&
      clampViewPositionVertical
    ->onEdgeDragStarted
    ->tryCaptureView
    ->onViewCaptured
    ->onViewDragStateChanged

processTouchEvent:

DOWN:
    getOrderedChildIndex(findTopChildUnder)
    ->tryCaptureView
    ->onViewCaptured
    ->onViewDragStateChanged
    ->onEdgeTouched
MOVE:
    ->STATE==DRAGGING:dragTo
    ->STATE!=DRAGGING:
        onEdgeDragStarted
        ->getOrderedChildIndex(findTopChildUnder)
        ->getViewHorizontalDragRange&
          getViewVerticalDragRange(checkTouchSlop)
        ->tryCaptureView
        ->onViewCaptured
        ->onViewDragStateChanged

当然整个过程可能会存在很多判断不成立的情况.

这个总结参考的http://blog.csdn.net/lmj623565791/article/details/46858663 博客

原文地址:https://www.cnblogs.com/bokezhilu/p/7528096.html