Android 一步一步教你使用ViewDragHelper
在自定义viewgroup的时候 要重写onInterceptTouchEvent和onTouchEvent 这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 等类似抽屉效果里经常用到的函数,有兴趣的同学可以
看下源码。


浙公网安备 33010602011771号