ViewDragHelper源代码分析

1.简介

在上一篇文章SwipeBackLayout源代码分析中,我们了解了ViewDragHelper是可以帮助我们处理各种拖拽事件的类.使用好ViewDragHelper能帮助我们做出各种酷炫的交互,今天我们就来分析一下ViewDragHelper的使用与实现:

2.使用方法

我们这里就以翔总的这篇文章中的例子来介绍一下ViewDragHelper的使用.另外,本文中的demo可以在
这里找到

首先我们创建一个DragLayout类并继承自LinearLayout,然后我们准备在DragLayout放置三个View第一个用来被我们拖动然后停止在松手的位置,第二个可以被我们拖动,松手的时候滑动到指定位置,第三个只可以通过触摸边缘来进行拖动,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

public class DragLayout extends LinearLayout {

private ViewDragHelper mDragger;
private View mDragView;
private View mAutoBackView;
private View mEdgeTrackerView;

private Point mAutoBackOriginPos = new Point();

public DragLayout(Context context) {
this(context, null);
}

public DragLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initViewDragHelper();
}

private void initViewDragHelper() {
mDragger = ViewDragHelper.create(this,myCallback);
mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);
}

ViewDragHelper.Callback myCallback = new ViewDragHelper.Callback() {
@Override
//child为当前触摸区域下的View,如果返回true,就可以拖拽.
public boolean tryCaptureView(View child, int pointerId) {
return child == mDragView || child == mAutoBackView;
}

//松手时的回调
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (releasedChild == mAutoBackView) {
mDragger.settleCapturedViewAt(mAutoBackOriginPos.x, mAutoBackOriginPos.y);
invalidate();
}
}

//边缘触摸开始时的回调
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
mDragger.captureChildView(mEdgeTrackerView, pointerId);
}

//获取水平方向允许拖拽的区域,这里是父布局的宽-子控件的宽
@Override
public int getViewHorizontalDragRange(View child) {
return getMeasuredWidth() - child.getMeasuredWidth();
}

//获取垂直方向允许拖拽的范围
@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight() - child.getMeasuredHeight();
}

//left为child即将移动到的水平位置的值,但是返回值会最终决定移动到的值
//这里直接返回了left
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
//同上只是这里是垂直方向
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
};

@Override
public void computeScroll() {
if (mDragger.continueSettling(true)) {
invalidate();
}
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragger.shouldInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
mDragger.processTouchEvent(event);
return true;
}

@Override
protected void onFinishInflate() {
super.onFinishInflate();
mDragView = getChildAt(0);
mAutoBackView = getChildAt(1);
mEdgeTrackerView = getChildAt(2);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mAutoBackOriginPos.x = mAutoBackView.getLeft();
mAutoBackOriginPos.y = mAutoBackView.getTop();
}
}
  1. 我们首先在构造方法里传入了当前类的对象和我们定义的ViewDragHelper.Callback对象初始化了我们的ViewDragHelper,然后我们希望所有的边缘触摸都能触发mEdgeTrackerView的拖动,所以我们紧接着调用了mDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_ALL);方法.
  2. 在我们定义的Callback中,有多个回调方法,每个回调方法都有它的作用,在代码里注释比较清楚了,我们下面也会解析每一个Callback中回调方法的作用.
  3. 第三步我们需要在onInterceptTouchEvent()方法和onTouchEvent()将事件委托给ViewDragHelper去处理,这样ViewDragHelper才能根据响应的事件并回调我们自己编写的Callback接口来进行响应的处理,
  4. 由于ViewDragHelper中的滑动是交给Srcoller类来处理的所以这里我们要重写computeScroll()方法,配合Scroller完成滚动动画.
  5. 最后在onFinishInflate()里获取到我们的View对象即可.

3.类关系图

由于就一个类类图我们就不画了,但是作为一个强迫症患者,这个标题必须有…

4.源码分析

1.ViewDragHelper.Callback的实现

在分析ViewDragHelper之前,我们先来分析一下Callback的定义,看看Callback都定义了哪些方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

public static abstract class Callback {

//当View的拖拽状态改变时回调,state为STATE_IDLE,STATE_DRAGGING,STATE_SETTLING的一种
//STATE_IDLE: 当前未被拖拽
//STATE_DRAGGING:正在被拖拽
//STATE_SETTLING: 被拖拽后需要被安放到一个位置中的状态
public void onViewDragStateChanged(int state) {}

//当View被拖拽位置发生改变时回调
//changedView :被拖拽的View
//left : 被拖拽后View的left边缘坐标
//top : 被拖拽后View的top边缘坐标
//dx : 拖动的x偏移量
//dy : 拖动的y偏移量
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}

//当一个View被捕获到准备开始拖动时回调,
//capturedChild : 捕获的View
//activePointerId : 对应的PointerId
public void onViewCaptured(View capturedChild, int activePointerId) {}

//当被捕获拖拽的View被释放是回调
//releasedChild : 被释放的View
//xvel : 释放View的x方向上的加速度
//yvel : 释放View的y方向上的加速度
public void onViewReleased(View releasedChild, float xvel, float yvel) {}

//如果parentView订阅了边缘触摸,则如果有边缘触摸就回调的接口
//edgeFlags : 当前触摸的flag 有: EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM
//pointerId : 用来描述边缘触摸操作的id
public void onEdgeTouched(int edgeFlags, int pointerId) {}

//是否锁定该边缘的触摸,默认返回false,返回true表示锁定
public boolean onEdgeLock(int edgeFlags) {
return false;
}

//边缘触摸开始时回调
//edgeFlags : 当前触摸的flag 有: EDGE_LEFT,EDGE_TOP,EDGE_RIGHT,EDGE_BOTTOM
//pointerId : 用来描述边缘触摸操作的id
public void onEdgeDragStarted(int edgeFlags, int pointerId) {}

//在寻找当前触摸点下的子View时会调用此方法,寻找到的View会提供给tryCaptureViewForDrag()来尝试捕获。
//如果需要改变子View的遍历查询顺序可改写此方法,例如让下层的View优先于上层的View被选中。
public int getOrderedChildIndex(int index) {
return index;
}

//获取被拖拽View child 的水平拖拽范围,返回0表示无法被水平拖拽
public int getViewHorizontalDragRange(View child) {
return 0;
}

//获取被拖拽View child 的垂直拖拽范围,返回0表示无法被水平拖拽
public int getViewVerticalDragRange(View child) {
return 0;
}

//尝试捕获被拖拽的View
public abstract boolean tryCaptureView(View child, int pointerId);

//决定拖拽View在水平方向上应该移动到的位置
//child : 被拖拽的View
//left : 期望移动到位置的View的left值
//dx : 移动的水平距离
//返回值 : 直接决定View在水平方向的位置
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}

//决定拖拽View在垂直方向上应该移动到的位置
//child : 被拖拽的View
//top : 期望移动到位置的View的top值
//dy : 移动的垂直距离
//返回值 : 直接决定View在垂直方向的位置
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
}

想必注释已经很清楚了,正是这些回调方法,再结合ViewDragHelper中的各种方法,来帮助我们实现各种各样的拖拽的效果。

2.shouldInterceptTouchEvent()方法的实现

在这里我们假设大家都清楚了Android的事件分发机制,如果不清楚请看这里,要想处理触摸事件,我们需要在onInterceptTouchEvent(MotionEvent ev)方法里判断是否需要拦截这次触摸事件,如果此方法返回true则触摸事件将会交给onTouchEvent(MotionEvent event)处理,这样我们就能处理触摸事件了,所以我们在上面的使用方法里会这样写:

1
2
3
4
5
6
7
8
9
10
11

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragger.shouldInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
mDragger.processTouchEvent(event);
return true;
}

这样就将是否拦截触摸事件,以及处理触摸事件委托给ViewDragHelper来处理了,所以我们先来看看ViewDragHelpershouldInterceptTouchEvent();方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150

public boolean shouldInterceptTouchEvent(MotionEvent ev) {
//获取action
final int action = MotionEventCompat.getActionMasked(ev);
//获取action对应的index
final int actionIndex = MotionEventCompat.getActionIndex(ev);

//如果是按下的action则重置一些信息,包括各种事件点的数组
if (action == MotionEvent.ACTION_DOWN) {
// Reset things for a new event stream, just in case we didn't get
// the whole previous stream.
cancel();
}
//初始化mVelocityTracker
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);

//根据action来做相应的处理
switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
//获取这个事件对应的pointerId,一般情况下只有一个手指触摸时为0
//两个手指触摸时第二个手指触摸返回的pointerId为1,以此类推
final int pointerId = MotionEventCompat.getPointerId(ev, 0);
//保存点的数据
//TODO (1)
saveInitialMotion(x, y, pointerId);
//获取当前触摸点下最顶层的子View
//TODO (2)
final View toCapture = findTopChildUnder((int) x, (int) y);

//如果toCapture是已经捕获的View,而且正在处于被释放状态
//那么就重新捕获
if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
tryCaptureViewForDrag(toCapture, pointerId);
}

//如果触摸了边缘,回调callback的onEdgeTouched()方法
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}

//当又有一个手指触摸时
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
final float x = MotionEventCompat.getX(ev, actionIndex);
final float y = MotionEventCompat.getY(ev, actionIndex);

//保存触摸信息
saveInitialMotion(x, y, pointerId);

//因为同一时间ViewDragHelper只能操控一个View,所以当有新的手指触摸时
//只讨论当无触摸发生时,回调边缘触摸的callback
//或者正在处于释放状态时重新捕获View
if (mDragState == STATE_IDLE) {
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
} else if (mDragState == STATE_SETTLING) {
// Catch a settling view if possible.
final View toCapture = findTopChildUnder((int) x, (int) y);
if (toCapture == mCapturedView) {
tryCaptureViewForDrag(toCapture, pointerId);
}
}
break;
}

//当手指移动时
case MotionEvent.ACTION_MOVE: {
if (mInitialMotionX == null || mInitialMotionY == null) break;

// First to cross a touch slop over a draggable view wins. Also report edge drags.
//得到触摸点的数量,并循环处理,只处理第一个发生了拖拽的事件
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int pointerId = MotionEventCompat.getPointerId(ev, i);
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
//获得拖拽偏移量
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
//获取当前触摸点下最顶层的子View
final View toCapture = findTopChildUnder((int) x, (int) y);
//如果找到了最顶层View,并且产生了拖动(checkTouchSlop()返回true)
//TODO (3)
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
if (pastSlop) {
//根据callback的四个方法getView[Horizontal|Vertical]DragRange和
//clampViewPosition[Horizontal|Vertical]来检查是否可以拖动
final int oldLeft = toCapture.getLeft();
final int targetLeft = oldLeft + (int) dx;
final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
targetLeft, (int) dx);
final int oldTop = toCapture.getTop();
final int targetTop = oldTop + (int) dy;
final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
(int) dy);
final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
toCapture);
final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
//如果都不允许移动则跳出循环
if ((horizontalDragRange == 0 || horizontalDragRange > 0
&& newLeft == oldLeft) && (verticalDragRange == 0
|| verticalDragRange > 0 && newTop == oldTop)) {
break;
}
}
//记录并回调是否有边缘触摸
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag
break;
}
//如果产生了拖动则调用tryCaptureViewForDrag()
//TODO (4)
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
//保存触摸点的信息
saveLastMotion(ev);
break;
}

//当有一个手指抬起时,清除这个手指的触摸数据
case MotionEventCompat.ACTION_POINTER_UP: {
final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
clearMotionHistory(pointerId);
break;
}

//清除所有触摸数据
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
cancel();
break;
}
}

//如果mDragState等于正在拖拽则返回true
return mDragState == STATE_DRAGGING;
}

上面就是整个shouldInterceptTouchEvent()的实现,上面的注释也足够清楚了,我们这里就先不分析某一种触摸事件,大家可以看到我上面留了几个TODO,下文会一起分析,这里我假设大家都已经对触摸事件分发处理都有充分的理解了,我们下面就直接看ViewDragHelperprocessTouchEvent()方法的实现.

3.processTouchEvent()方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

public void processTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
final int actionIndex = MotionEventCompat.getActionIndex(ev);

...(省去部分代码)
switch (action) {
case MotionEvent.ACTION_DOWN: {
...(省去部分代码)
break;
}

case MotionEventCompat.ACTION_POINTER_DOWN: {
...(省去部分代码)
break;
}

case MotionEvent.ACTION_MOVE: {
//如果现在已经是拖拽状态
if (mDragState == STATE_DRAGGING) {
final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, index);
final float y = MotionEventCompat.getY(ev, index);
final int idx = (int) (x - mLastMotionX[mActivePointerId]);
final int idy = (int) (y - mLastMotionY[mActivePointerId]);

//拖拽至指定位置
//TODO (5)
dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);

saveLastMotion(ev);
} else {
// Check to see if any pointer is now over a draggable view.
//如果还不是拖拽状态,就检测是否经过了一个View
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int pointerId = MotionEventCompat.getPointerId(ev, i);
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];

reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag.
break;
}

final View toCapture = findTopChildUnder((int) x, (int) y);
if (checkTouchSlop(toCapture, dx, dy) &&
tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
}
break;
}
//当多个手指中的一个手机松开时
case MotionEventCompat.ACTION_POINTER_UP: {
final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
//如果当前点正在被拖拽,则再剩余还在触摸的点钟寻找是否正在View上
if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
// Try to find another pointer that's still holding on to the captured view.
int newActivePointer = INVALID_POINTER;
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int id = MotionEventCompat.getPointerId(ev, i);
if (id == mActivePointerId) {
// This one's going away, skip.
continue;
}

final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
if (findTopChildUnder((int) x, (int) y) == mCapturedView &&
tryCaptureViewForDrag(mCapturedView, id)) {
newActivePointer = mActivePointerId;
break;
}
}

if (newActivePointer == INVALID_POINTER) {
// We didn't find another pointer still touching the view, release it.
//如果没找到则释放View
//TODO (6)
releaseViewForPointerUp();
}
}
clearMotionHistory(pointerId);
break;
}

case MotionEvent.ACTION_UP: {
//如果是拖拽状态的释放则调用
//releaseViewForPointerUp()
if (mDragState == STATE_DRAGGING) {
releaseViewForPointerUp();
}
cancel();
break;
}

case MotionEvent.ACTION_CANCEL: {
if (mDragState == STATE_DRAGGING) {
dispatchViewReleased(0, 0);
}
cancel();
break;
}
}
}

上面就是processTouchEvent()方法的实现,我们省去了部分大致与shouldInterceptTouchEvent()相同的逻辑代码,通过事件传递机制我们知道,如果程序已经进入到processTouchEvent()中,也就意味着触摸事件就不会再向下传递,都会交给此方法处理,所以在这里我们就需要处理拖拽事件了,通过上面的注释,我们也看到了在MotionEvent.ACTION_MOVE,MotionEventCompat.ACTION_POINTER_UP,MotionEvent.ACTION_UPMotionEvent.ACTION_CANCEL都分别进行了处理 ,我们知道触摸事件大致的流程是:

ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP

再配合事件的分发机制,我们就能很清晰的分析出一次完整的事件调用过程,所以整个ViewDragHelper的拖拽过程也能很清晰的分为三个步骤:

捕获拖拽目标View -> 拖拽目标View -> 处理目标View释放操作

最后我们再分析上面两段代码的6个TODO:

4.saveInitialMotion()方法

1
2
3
4
5
6
7
8
9
10
11
12
private void saveInitialMotion(float x, float y, int pointerId) {
//确保各个数组的大小足够存放数据
ensureMotionHistorySizeForId(pointerId);
//保存x坐标
mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
//保存y坐标
mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
//保存是否触摸到边缘
mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
//保存当前id是否在触摸,用于后续验证
mPointersDown |= 1 << pointerId;
}

5.findTopChildUnder()方法

1
2
3
4
5
6
7
8
9
10
11
public View findTopChildUnder(int x, int y) {
final int childCount = mParentView.getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
if (x >= child.getLeft() && x < child.getRight() &&
y >= child.getTop() && y < child.getBottom()) {
return child;
}
}
return null;
}

代码很简单就是根据xy坐标和来找到指定View,注意这里回调了callback中的getOrderedChildIndex()方法,所以我们可以在这里返回指定的Viewindex.

6.checkTouchSlop()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private boolean checkTouchSlop(View child, float dx, float dy) {
if (child == null) {
return false;
}
final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;

if (checkHorizontal && checkVertical) {
return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
} else if (checkHorizontal) {
return Math.abs(dx) > mTouchSlop;
} else if (checkVertical) {
return Math.abs(dy) > mTouchSlop;
}
return false;
}

用来根据mTouchSlop最小拖动的距离来判断是否属于拖动,mTouchSlop根据我们设定的灵敏度决定.

7.tryCaptureViewForDrag()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
//如果已经捕获该View 直接返回true
if (toCapture == mCapturedView && mActivePointerId == pointerId) {
// Already done!
return true;
}
//根据mCallback.tryCaptureView()方法来最终决定是否可以捕获View
if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
mActivePointerId = pointerId;
//如果可以则调用captureChildView(),并返回true
captureChildView(toCapture, pointerId);
return true;
}
return false;
}

可以看到如果可以捕获View则调用了captureChildView()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

public void captureChildView(View childView, int activePointerId) {
if (childView.getParent() != mParentView) {
throw new IllegalArgumentException("captureChildView: parameter must be a descendant " +
"of the ViewDragHelper's tracked parent view (" + mParentView + ")");
}
//赋值mCapturedView
mCapturedView = childView;
mActivePointerId = activePointerId;
//回调callback
mCallback.onViewCaptured(childView, activePointerId);
//设定mDragState的状态为STATE_DRAGGING
setDragState(STATE_DRAGGING);
}

如果程序执行到这里,就证明View已经处于拖拽状态了,后续的触摸操作,将直接根据mDragStateSTATE_DRAGGING的状态处理.

8.dragTo()方法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

private void dragTo(int left, int top, int dx, int dy) {
int clampedX = left;
int clampedY = top;
final int oldLeft = mCapturedView.getLeft();
final int oldTop = mCapturedView.getTop();
if (dx != 0) {
//回调callback来决定View最终被拖拽的x方向上的偏移量
clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
//移动View
ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
}
if (dy != 0) {
//回调callback来决定View最终被拖拽的y方向上的偏移量
clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
//移动View
ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
}

if (dx != 0 || dy != 0) {
final int clampedDx = clampedX - oldLeft;
final int clampedDy = clampedY - oldTop;
//回调callback
mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
clampedDx, clampedDy);
}
}

因为dragTo()方法是在processTouchEvent()中的MotionEvent.ACTION_MOVE case被调用所以当程序运行到这里时View就会不断的被拖动了。如果一旦手指释放则最终会调用releaseViewForPointerUp()方法

8.releaseViewForPointerUp()方法的实现

1
2
3
4
5
6
7
8
9
10
11
12

private void releaseViewForPointerUp() {
//计算出当前x和y方向上的加速度
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final float xvel = clampMag(
VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
mMinVelocity, mMaxVelocity);
final float yvel = clampMag(
VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
mMinVelocity, mMaxVelocity);
dispatchViewReleased(xvel, yvel);
}

计算完加速度后就调用了dispatchViewReleased():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

private void dispatchViewReleased(float xvel, float yvel) {
//设定当前正处于释放阶段
mReleaseInProgress = true;
//回调callback的onViewReleased()方法
mCallback.onViewReleased(mCapturedView, xvel, yvel);
mReleaseInProgress = false;

//设定状态
if (mDragState == STATE_DRAGGING) {
// onViewReleased didn't call a method that would have changed this. Go idle.
//如果onViewReleased()中没有调用任何方法,则状态设定为STATE_IDLE
setDragState(STATE_IDLE);
}
}

所以最后释放后的处理交给了callback中的onViewReleased()方法,如果我们什么都不做,那么这个被拖拽的View就是停止在当前位置,或者我们可以调用ViewDragHelper提供给我们的这几个方法:

  • settleCapturedViewAt(int finalLeft, int finalTop)
    以松手前的滑动速度为初速动,让捕获到的View自动滚动到指定位置。只能在Callback的onViewReleased()中调用。
  • flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)
    以松手前的滑动速度为初速动,让捕获到的View在指定范围内fling。只能在Callback的onViewReleased()中调用。
  • smoothSlideViewTo(View child, int finalLeft, int finalTop)
    指定某个View自动滚动到指定的位置,初速度为0,可在任何地方调用。

引用自这篇文章,具体释放后的原理我们就不分析了,其实就是配合Scroller这个类来实现,具体也可以参照上面这篇文章。好,我们关于ViewDragHelper的源码分析就到这里.

5.开源项目中的使用

ViewDragHelper在各种关于拖拽和各种手势动画的开源库中使用广泛,我这里就简要列出一些,大家可以多去看看是如何使用ViewDragHelper的:

6.个人评价

ViewDragHelper的出现,大大简化了我们开发相关触摸和拖拽功能的复杂度和代码量,帮助我们比较容易的实现各种效果,让我们开发酷炫的交互更加容易了。但是从一些开源项目中发现,ViewDragHelper中还是有一些不足之处,比如给Scroller提供了一个固定的Interpolator,导致如果我们想实现例如反弹效果的话,还要把ViewDragHelper的代码拷贝一份并修改Interpolator,这样做肯定是不太好的.当然建议我们自己修改一个ViewDragHelper后如果项目里有多处使用,可以包装成一个提供给我们自己项目的模块使用,防止出现更多的多余代码.