ViewDragHelper源码解析

  1. 1. ViewDragHelper
    1. 1.1. 特点
    2. 1.2. 使用
  2. 2. 源码分析
    1. 2.1. ViewDragHelper
    2. 2.2. Callback
    3. 2.3. shouldInterceptTouchEvent
    4. 2.4. processTouchEvent
    5. 2.5. settleCapturedViewAt && flingCapturedView
    6. 2.6. smoothSlideViewTo

分析版本:support-v4-23.3.0

ViewDragHelper 封装了许多手势拖动操作,在某些条件下使用 ViewDragHelper 开发可提高效率。

ViewDragHelper

大家应该都知道 DrawerLayout 吧,该控件有一个手势操作就是将 Menu 从屏幕边缘位置滑动出来,而这部分的手势操作是交给 ViewDragHelper 去处理的。ViewDragHelper 并不是第一个用于分析手势处理的类,Gesturedetector 也是,但是在和拖动相关的手势分析方面 ViewDragHelper 更胜一筹。

特点

  • ViewDragHelper 可以检测到是否触及到边缘
  • ViewDragHelper 并不是直接作用于要被拖动的 View,而是使其控制的视图容器中的子 View 可以被拖动
  • ViewDragHelper.Callback 是连接 ViewDragHelper 与 View 之间的桥梁
  • ViewDragHelper 的本质其实是分析 onInterceptTouchEvent 和 onTouchEvent 的 MotionEvent 参数,然后根据分析的结果去改变一个容器中被拖动子 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
public class VDHLayout extends LinearLayout{
private ViewDragHelper mDragger;

public VDHLayout(Context context, AttributeSet attrs){
super(context, attrs);
//第二个参数就是滑动灵敏度的意思
mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback(){
//这个地方实际上函数返回值为true就代表可以滑动 为false 则不能滑动
@Override
public boolean tryCaptureView(View child, int pointerId){
return true;
}

//这个地方实际上left就代表 你将要移动到的位置的坐标。返回值就是最终确定的移动的位置。
//我们要让view滑动的范围在我们的layout之内
//实际上就是判断如果这个坐标在layout之内 那我们就返回这个坐标值。
//如果这个坐标在layout的边界处 那我们就只能返回边界的坐标给他。不能让他超出这个范围
//除此之外就是如果你的layout设置了padding的话,也可以让子view的活动范围在padding之内的.
@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 boolean onInterceptTouchEvent(MotionEvent event){
return mDragger.shouldInterceptTouchEvent(event);
}

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

onInterceptTouchEvent 中通过使用 mDragger.shouldInterceptTouchEvent(event) 来决定我们是否应该拦截当前的事件。onTouchEvent 中通过 mDragger.processTouchEvent(event) 处理事件。

源码分析

ViewDragHelper

从创建开始:

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
public class ViewDragHelper {
private final ViewGroup mParentView;
private final Callback mCallback;

private int mEdgeSize;//边缘距离,当在边缘的这个距离内算触发边缘
private int mTouchSlop;//最小移动距离
private float mMaxVelocity;//最大移动速度
private float mMinVelocity;//最小移动速度
private ScrollerCompat mScroller;

/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/

public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
return new ViewDragHelper(forParent.getContext(), forParent, cb);
}

/**
* 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;
}

/**
* Apps should use ViewDragHelper.create() to get a new instance.
* This will allow VDH to use internal compatibility implementations for different
* platform versions.
*
* @param context Context to initialize config-dependent params from
* @param forParent Parent view to monitor
*/

private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
if (forParent == null) {
throw new IllegalArgumentException("Parent view may not be null");
}
if (cb == null) {
throw new IllegalArgumentException("Callback may not be null");
}

mParentView = forParent;
mCallback = cb;

final ViewConfiguration vc = ViewConfiguration.get(context);
final float density = context.getResources().getDisplayMetrics().density;
mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);

mTouchSlop = vc.getScaledTouchSlop();
mMaxVelocity = vc.getScaledMaximumFlingVelocity();
mMinVelocity = vc.getScaledMinimumFlingVelocity();
mScroller = ScrollerCompat.create(context, sInterpolator);
}

/**
* Interpolator defining the animation curve for mScroller
*/

private static final Interpolator sInterpolator = new Interpolator() {
public float getInterpolation(float t) {
t -= 1.0f;
return t * t * t * t * t + 1.0f;
}
};
}

这个构造函数是私有的,也是仅有的构造函数,所以外部只能通过 create() 工厂方法来创建 ViewDragHelper 实例了。 这里要求了我们传递的自定义 ViewGroup 和回调对象不能为空,否则会直接抛出异常中断程序。在这里也初始化了一些触摸滑动需要的参考值和辅助类。

  • mParentView 和 mCallback 分别保存传递过来的对应参数
  • ViewConfiguration 类里定义了 View 相关的一系列时间、大小、距离等常量
  • mEdgeSize 表示边缘触摸的范围。例如 mEdgeSize 为 20dp 并且用户注册监听了左侧边缘触摸时,触摸点的 x 坐标小于 mParentView.getLeft() + mEdgeSize 时(即触摸点在容器左边界往右 20dp 内)就算做是左侧的边缘触摸
  • mTouchSlop 是一个很小的距离值,只有在前后两次触摸点的距离超过 mTouchSlop 的值时,我们才把这两次触摸算作是『滑动』,我们只在此时进行滑动处理,否则任何微小的距离的变化我们都要处理的话会显得太频繁,如果处理过程又比较复杂耗时就会使界面产生卡顿
  • mMaxVelocity 、mMinVelocity 是 fling 时的最大、最小速率,单位是像素每秒
  • mScroller 是 View 滚动的辅助类
  • 还有个 sensitivity ,是用来调节 mTouchSlop 的值。sensitivity 越大,mTouchSlop 越小,对滑动的检测就越敏感。 例如 sensitivity 为 1 时,前后触摸点距离超过 20dp 才进行滑动处理,现在 sensitivity 为 2 的话,前后触摸点距离超过 10dp 就进行处理了

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
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
public class ViewDragHelper {
/**
* A Callback is used as a communication channel with the ViewDragHelper back to the
* parent view using it. <code>on*</code>methods are invoked on siginficant events and several
* accessor methods are expected to provide the ViewDragHelper with more information
* about the state of the parent view upon request. The callback also makes decisions
* governing the range and draggability of child views.
*/

public static abstract class Callback {
/**
* Called when the drag state changes. See the <code>STATE_*</code> constants
* for more information.
*
* @param state The new drag state
*
* @see #STATE_IDLE
* @see #STATE_DRAGGING
* @see #STATE_SETTLING
*/

public void onViewDragStateChanged(int state) {}

/**
* Called when the captured view's position changes as the result of a drag or settle.
*
* @param changedView View whose position changed
* @param left New X coordinate of the left edge of the view
* @param top New Y coordinate of the top edge of the view
* @param dx Change in X position from the last call
* @param dy Change in Y position from the last call
*/

public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}

/**
* Called when a child view is captured for dragging or settling. The ID of the pointer
* currently dragging the captured view is supplied. If activePointerId is
* identified as {@link #INVALID_POINTER} the capture is programmatic instead of
* pointer-initiated.
*
* @param capturedChild Child view that was captured
* @param activePointerId Pointer id tracking the child capture
*/

public void onViewCaptured(View capturedChild, int activePointerId) {}

/**
* Called when the child view is no longer being actively dragged.
* The fling velocity is also supplied, if relevant. The velocity values may
* be clamped to system minimums or maximums.
*
* <p>Calling code may decide to fling or otherwise release the view to let it
* settle into place. It should do so using {@link #settleCapturedViewAt(int, int)}
* or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes
* one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING}
* and the view capture will not fully end until it comes to a complete stop.
* If neither of these methods is invoked before <code>onViewReleased</code> returns,
* the view will stop in place and the ViewDragHelper will return to
* {@link #STATE_IDLE}.</p>
*
* @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.
*/

public void onViewReleased(View releasedChild, float xvel, float yvel) {}

/**
* Called when one of the subscribed edges in the parent view has been touched
* by the user while no child view is currently captured.
*
* @param edgeFlags A combination of edge flags describing the edge(s) currently touched
* @param pointerId ID of the pointer touching the described edge(s)
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/

public void onEdgeTouched(int edgeFlags, int pointerId) {}

/**
* Called when the given edge may become locked. This can happen if an edge drag
* was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)}
* was called. This method should return true to lock this edge or false to leave it
* unlocked. The default behavior is to leave edges unlocked.
*
* @param edgeFlags A combination of edge flags describing the edge(s) locked
* @return true to lock the edge, false to leave it unlocked
*/

public boolean onEdgeLock(int edgeFlags) {
return false;
}

/**
* Called when the user has started a deliberate drag away from one
* of the subscribed edges in the parent view while no child view is currently captured.
*
* @param edgeFlags A combination of edge flags describing the edge(s) dragged
* @param pointerId ID of the pointer touching the described edge(s)
* @see #EDGE_LEFT
* @see #EDGE_TOP
* @see #EDGE_RIGHT
* @see #EDGE_BOTTOM
*/

public void onEdgeDragStarted(int edgeFlags, int pointerId) {}

/**
* Called to determine the Z-order of child views.
*
* @param index the ordered position to query for
* @return index of the view that should be ordered at position <code>index</code>
*/

public int getOrderedChildIndex(int index) {
return index;
}

/**
* Return the magnitude of a draggable child view's horizontal range of motion in pixels.
* This method should return 0 for views that cannot move horizontally.
*
* @param child Child view to check
* @return range of horizontal motion in pixels
*/

public int getViewHorizontalDragRange(View child) {
return 0;
}

/**
* Return the magnitude of a draggable child view's vertical range of motion in pixels.
* This method should return 0 for views that cannot move vertically.
*
* @param child Child view to check
* @return range of vertical motion in pixels
*/

public int getViewVerticalDragRange(View child) {
return 0;
}

/**
* Called when the user's input indicates that they want to capture the given child view
* with the pointer indicated by pointerId. The callback should return true if the user
* is permitted to drag the given view with the indicated pointer.
*
* <p>ViewDragHelper may call this method multiple times for the same view even if
* the view is already captured; this indicates that a new pointer is trying to take
* control of the view.</p>
*
* <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)}
* will follow if the capture is successful.</p>
*
* @param child Child the user is attempting to capture
* @param pointerId ID of the pointer attempting the capture
* @return true if capture should be allowed, false otherwise
*/

public abstract boolean tryCaptureView(View child, int pointerId);

/**
* Restrict the motion of the dragged child view along the horizontal axis.
* The default implementation does not allow horizontal motion; the extending
* class must override this method and provide the desired clamping.
*
*
* @param child Child view being dragged
* @param left Attempted motion along the X axis
* @param dx Proposed change in position for left
* @return The new clamped position for left
*/

public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;
}

/**
* Restrict the motion of the dragged child view along the vertical axis.
* The default implementation does not allow vertical motion; the extending
* class must override this method and provide the desired clamping.
*
*
* @param child Child view being dragged
* @param top Attempted motion along the Y axis
* @param dy Proposed change in position for top
* @return The new clamped position for top
*/

public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
}
}
  • onViewCaptured (当 captureView 被捕获时回调)
  • tryCaptureView (是否需要 capture 这个 View)
  • clampViewPositionHorizontal (横向移动的时候回调)
  • clampViewPositionVertical (纵向移动的时候回调)
  • onViewDragStateChanged (当ViewDragHelper状态发生变化时回调(IDLE,DRAGGING,SETTING[自动滚动时]))
  • onViewPositionChanged (当 captureView 的位置发生改变时回调)
  • onEdgeTouched (当触摸到边界时回调)
  • onEdgeLock (true 的时候会锁住当前的边界,false 则 unLock )
  • onEdgeDragStarted (边界拖动开始的时候回调)
  • getOrderedChildIndex (改变同一个坐标( x , y )去寻找 captureView 位置的方法)
  • getViewHorizontalDragRange (最大横滑动的滑动距离)
  • getViewVerticalDragRange (最大纵向滑动的距离)
  • onViewReleased (当 captureView 被释放的时候回调)

shouldInterceptTouchEvent

onInterceptTouchEvent 中的处理:

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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
public class ViewDragHelper {
/**
* A null/invalid pointer ID.
*/

public static final int INVALID_POINTER = -1;

/**
* Edge flag indicating that the left edge should be affected.
*/

public static final int EDGE_LEFT = 1 << 0;

/**
* Edge flag indicating that the right edge should be affected.
*/

public static final int EDGE_RIGHT = 1 << 1;

/**
* Edge flag indicating that the top edge should be affected.
*/

public static final int EDGE_TOP = 1 << 2;

/**
* Edge flag indicating that the bottom edge should be affected.
*/

public static final int EDGE_BOTTOM = 1 << 3;

/**
* Edge flag set indicating all edges should be affected.
*/

public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;

/**
* A view is not currently being dragged or animating as a result of a fling/snap.
*/

public static final int STATE_IDLE = 0;//静止空闲状态

/**
* A view is currently being dragged. The position is currently changing as a result
* of user input or simulated user input.
*/

public static final int STATE_DRAGGING = 1;//正在被拖动

/**
* A view is currently settling into place as a result of a fling or
* predefined non-interactive motion.
*/

public static final int STATE_SETTLING = 2;//正在安置状态中(用户并没有交互操作),就是自动滚动的过程中

private int mActivePointerId = INVALID_POINTER;

// Current drag state; idle, dragging or settling
private int mDragState;//当前drag状态
private View mCapturedView;//正在被drag的view

// Last known position/pointer tracking
private int mActivePointerId = INVALID_POINTER;
private float[] mInitialMotionX;//一开始的X坐标,数组代表多手指的情况,index就是手指id
private float[] mInitialMotionY;//一开始的Y坐标,数组代表多手指的情况,index就是手指id
private float[] mLastMotionX;//随时更新的的X坐标,数组代表多手指的情况,index就是手指id
private float[] mLastMotionY;//随时更新的的Y坐标,数组代表多手指的情况,index就是手指id
private int[] mInitialEdgesTouched;//刚触摸的时候触摸的边缘位置
private int[] mEdgeDragsInProgress;//记录边缘拖动
private int[] mEdgeDragsLocked;//记录锁定的边缘
private int mPointersDown;

private VelocityTracker mVelocityTracker;
private int mTrackingEdges;

/**
* Check if this event as provided to the parent view's onInterceptTouchEvent should
* cause the parent to intercept the touch event stream.
*
* @param ev MotionEvent provided to onInterceptTouchEvent
* @return true if the parent view should return true from onInterceptTouchEvent
*/

public boolean shouldInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);//当前action
final int actionIndex = MotionEventCompat.getActionIndex(ev);//当前actionIndex

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);

switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = MotionEventCompat.getPointerId(ev, 0);//手指id
saveInitialMotion(x, y, pointerId);//保存手指id对应的信息

final View toCapture = findTopChildUnder((int) x, (int) y);//找到该坐标上的最上面的那个view

// Catch a settling view if possible.
if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {//释放后正在进行动画的时候马上又去drag
tryCaptureViewForDrag(toCapture, pointerId);//去drag
}

final int edgesTouched = mInitialEdgesTouched[pointerId];//得到当前边缘触摸
if ((edgesTouched & mTrackingEdges) != 0) {//mTrackingEdges为是否开启某边缘的触摸drag
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);//保存手指id对应的信息

// A ViewDragHelper can only manipulate one view at a time.
if (mDragState == STATE_IDLE) {//如果当前状态是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);//去drag
}
}
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);//得到id

// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) continue;

final float x = MotionEventCompat.getX(ev, i);//得到X
final float y = MotionEventCompat.getY(ev, i);//得到Y
final float dx = x - mInitialMotionX[pointerId];//算出移动的X方向距离
final float dy = y - mInitialMotionY[pointerId];//算出移动的Y方向距离

final View toCapture = findTopChildUnder((int) x, (int) y);
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);//是否可以滑动
if (pastSlop) {
// check the callback's
// getView[Horizontal|Vertical]DragRange methods to know
// if you can move at all along an axis, then see if it
// would clamp to the same value. If you can't move at
// all in every dimension with a nonzero range, bail.
final int oldLeft = toCapture.getLeft();
final int targetLeft = oldLeft + (int) dx;
final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
targetLeft, (int) dx);//通过回调得到新的left的值
final int oldTop = toCapture.getTop();
final int targetTop = oldTop + (int) dy;
final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,//通过回调得到新的top的值
(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;
}

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;
}
}

return mDragState == STATE_DRAGGING;//如果是dragging状态,就拦截手势事件
}

/**
* The result of a call to this method is equivalent to
* {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event.
*/

public void cancel() {
mActivePointerId = INVALID_POINTER;
clearMotionHistory();

if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}

private void clearMotionHistory() {
if (mInitialMotionX == null) {
return;
}
Arrays.fill(mInitialMotionX, 0);
Arrays.fill(mInitialMotionY, 0);
Arrays.fill(mLastMotionX, 0);
Arrays.fill(mLastMotionY, 0);
Arrays.fill(mInitialEdgesTouched, 0);
Arrays.fill(mEdgeDragsInProgress, 0);
Arrays.fill(mEdgeDragsLocked, 0);
mPointersDown = 0;
}

private void saveInitialMotion(float x, float y, int pointerId) {
ensureMotionHistorySizeForId(pointerId);
mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;//赋最开始的值
mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;//赋最开始的值
mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);//赋值边缘值
mPointersDown |= 1 << pointerId;
}

private void ensureMotionHistorySizeForId(int pointerId) {
if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) {
float[] imx = new float[pointerId + 1];
float[] imy = new float[pointerId + 1];
float[] lmx = new float[pointerId + 1];
float[] lmy = new float[pointerId + 1];
int[] iit = new int[pointerId + 1];
int[] edip = new int[pointerId + 1];
int[] edl = new int[pointerId + 1];

if (mInitialMotionX != null) {
System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length);
System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length);
System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length);
System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length);
System.arraycopy(mInitialEdgesTouched, 0, iit, 0, mInitialEdgesTouched.length);
System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length);
System.arraycopy(mEdgeDragsLocked, 0, edl, 0, mEdgeDragsLocked.length);
}

mInitialMotionX = imx;
mInitialMotionY = imy;
mLastMotionX = lmx;
mLastMotionY = lmy;
mInitialEdgesTouched = iit;
mEdgeDragsInProgress = edip;
mEdgeDragsLocked = edl;
}
}

/**
* 判断是否在边缘,且返回是哪里的边缘
*/

private int getEdgesTouched(int x, int y) {
int result = 0;

if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT;
if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP;
if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT;
if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM;

return result;
}

/**
* Find the topmost child under the given point within the parent view's coordinate system.
* The child order is determined using {@link Callback#getOrderedChildIndex(int)}.
* 找到该坐标上的最上面的那个view
*
* @param x X position to test in the parent's coordinate system
* @param y Y position to test in the parent's coordinate system
* @return The topmost child view under (x, y) or null if none found.
*/

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;
}

/**
* Attempt to capture the view with the given pointer ID. The callback will be involved.
* This will put us into the "dragging" state. If we've already captured this view with
* this pointer this method will immediately return true without consulting the callback.
*
* @param toCapture View to capture
* @param pointerId Pointer to capture with
* @return true if capture was successful
*/

boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
if (toCapture == mCapturedView && mActivePointerId == pointerId) {//当前有正在被drag的View且是同一个且还是同一个手指
// Already done!
return true;
}
if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {//回调返回TRUE,表示要drag这个toCapture这个View
mActivePointerId = pointerId;
captureChildView(toCapture, pointerId);
return true;
}
return false;
}

/**
* Capture a specific child view for dragging within the parent. The callback will be notified
* but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to
* capture this view.
*
* @param childView Child view to capture
* @param activePointerId ID of the pointer that is dragging the captured child view
*/

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 = childView;//记录mCaptureView
mActivePointerId = activePointerId;//记录手指id
mCallback.onViewCaptured(childView, activePointerId);//回调
setDragState(STATE_DRAGGING);//设置drag状态
}

void setDragState(int state) {
mParentView.removeCallbacks(mSetIdleRunnable);
if (mDragState != state) {
mDragState = state;//设置新的状态
mCallback.onViewDragStateChanged(state);//回调
if (mDragState == STATE_IDLE) {
mCapturedView = null;
}
}
}

private final Runnable mSetIdleRunnable = new Runnable() {
public void run() {
setDragState(STATE_IDLE);
}
};

private boolean isValidPointerForActionMove(int pointerId) {
if (!isPointerDown(pointerId)) {
Log.e(TAG, "Ignoring pointerId=" + pointerId + " because ACTION_DOWN was not received "
+ "for this pointer before ACTION_MOVE. It likely happened because "
+ " ViewDragHelper did not receive all the events in the event stream.");
return false;
}
return true;
}

public boolean isPointerDown(int pointerId) {
return (mPointersDown & 1 << pointerId) != 0;
}

/**
* Check if we've crossed a reasonable touch slop for the given child view.
* If the child cannot be dragged along the horizontal or vertical axis, motion
* along that axis will not count toward the slop check.
* 检查手指移动的距离有没有超过触发处理移动事件的最短距离
*
* @param child Child to check
* @param dx Motion since initial position along X axis
* @param dy Motion since initial position along Y axis
* @return true if the touch slop has been crossed
*/

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;
}

/**
* 对四个边缘都做了一次检查,检查是否在某些边缘产生拖动了,如果有拖动,就将有拖动的边缘记录在mEdgeDragsInProgress中,再调用Callback的onEdgeDragStarted(int edgeFlags, int pointerId)通知某个边缘开始产生拖动了。虽然reportNewEdgeDrags()会被调用很多次
*/

private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
int dragsStarted = 0;
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
dragsStarted |= EDGE_LEFT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
dragsStarted |= EDGE_TOP;
}
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) {
dragsStarted |= EDGE_RIGHT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {
dragsStarted |= EDGE_BOTTOM;
}

if (dragsStarted != 0) {
mEdgeDragsInProgress[pointerId] |= dragsStarted;
mCallback.onEdgeDragStarted(dragsStarted, pointerId);
}
}

/**
* 返回true表示在指定的边缘开始产生拖动了
*/

private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
final float absDelta = Math.abs(delta);//主要监测的方向上的变化
final float absODelta = Math.abs(odelta);//另外一个方向上的变化

//1.如果ACTION_DOWN发生时没有触摸到边缘,或者触摸到的边缘不是指定的edge,就直接返回false
//2.mTrackingEdges是由setEdgeTrackingEnabled(int edgeFlags)设置的
//3.锁定的边缘
//4.mEdgeDragsInProgress是保存已发生过拖动事件的边缘的
//5.检查本次移动的距离是不是太小了,太小就不处理了
if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 ||
(mEdgeDragsLocked[pointerId] & edge) == edge ||
(mEdgeDragsInProgress[pointerId] & edge) == edge ||
(absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
return false;
}
if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {//次要方向上移动的距离是否远超过主要方向上移动的距离
mEdgeDragsLocked[pointerId] |= edge;
return false;
}
return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
}

private void saveLastMotion(MotionEvent ev) {
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);
mLastMotionX[pointerId] = x;
mLastMotionY[pointerId] = y;
}
}
}

findTopChildUnder() 方法是获得 View,如果在同一个位置有两个子 View 重叠,想要让下层的子 View 被选中, 那么就要实现 Callback 里的 getOrderedChildIndex(int index) 方法来改变查找子View的顺序;例如 topView( 上层View )的 index 是 4, bottomView(下层View)的 index 是 3,按照正常的遍历查找方式( getOrderedChildIndex() 默认直接返回 index ),会选择到 topView , 要想让 bottomView 被选中就得这么写:

1
2
3
4
5
6
7
8
public int getOrderedChildIndex(int index) {
int indexTop = mParentView.indexOfChild(topView);
int indexBottom = mParentView.indexOfChild(bottomView);
if (index == indexTop) {
return indexBottom;
}
return index;
}

shouldInterceptTouchEvent() 方法中,一开始进入的是 ACTION_DOWN ,mCapturedView 默认为 null ,所以一开始会进到 tryCaptureViewForDrag() 里面,当条件 tryCaptureView() 满足的时候会回调 onViewCaptured() 出去,同时设置状态为 STATE_DRAGGING ,之后再回调 onEdgeTouched()ACTION_DOWN 部分处理完了,跳过 switch 语句块,剩下的代码就只有 return mDragState == STATE_DRAGGING; ,此时如果状态为 dragging 的话,就反会的 true ,那么表示手势事件被拦截了;在 ACTION_DOWN 部分没有对 mDragState 进行赋值,其默认值为 STATE_IDLE ,所以此处返回 false ,此时会将手势事件传递给子 View 。如果子 View 没有消耗手势事件,那么会进到 ViewDragHelper#processTouchEvent() 中;如果子 View 消耗了手势事件,那么父 View 的 onTouchEvent() 收不到 ACTION_DOWN 事件,不过只要子 View 没有调用过 requestDisallowInterceptTouchEvent(true),父 View 的 onInterceptTouchEvent()ACTION_MOVE 部分还是会执行的, 如果在此时返回了 true 拦截了ACTION_MOVE 事件,processTouchEvent() 里的 ACTION_MOVE 部分也就会正常执行,拖动也就没问题了。

如果有多个手指触摸到屏幕上了,对每个触摸点都检查一下,看当前触摸的地方是否需要捕获某个 View。这里先用 findTopChildUnder(int x, int y) 寻找触摸点处的子 View,再用 checkTouchSlop(View child, float dx, float dy) 检查当前触摸点到 ACTION_DOWN 触摸点的距离是否达到了 mTouchSlop,达到了才会去捕获 View。

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
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
public class ViewDragHelper {

/**
* Process a touch event received by the parent view. This method will dispatch callback events
* as needed before returning. The parent view's onTouchEvent implementation should call this.
*
* @param ev The touch event received by the parent view
*/

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

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);

switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = MotionEventCompat.getPointerId(ev, 0);//手指id
final View toCapture = findTopChildUnder((int) x, (int) y);

saveInitialMotion(x, y, pointerId);//保存手指id对应的信息

// Since the parent is already directly processing this touch event,
// there is no reason to delay for a slop before dragging.
// Start immediately if possible.
tryCaptureViewForDrag(toCapture, pointerId);//去drag

final int edgesTouched = mInitialEdgesTouched[pointerId];//得到当前边缘触摸
if ((edgesTouched & mTrackingEdges) != 0) {//mTrackingEdges为是否开启某边缘的触摸drag
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);;//保存手指id对应的信息

// A ViewDragHelper can only manipulate one view at a time.
if (mDragState == STATE_IDLE) {
// If we're idle we can do anything! Treat it like a normal down event.

final View toCapture = findTopChildUnder((int) x, (int) y);//找到View
tryCaptureViewForDrag(toCapture, pointerId);//回调

final int edgesTouched = mInitialEdgesTouched[pointerId];//得到边缘
if ((edgesTouched & mTrackingEdges) != 0) {//是否能够触发边缘drag
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);//回调
}
} else if (isCapturedViewUnder((int) x, (int) y)) {
// We're still tracking a captured view. If the same view is under this
// point, we'll swap to controlling it with this pointer instead.
// (This will still work if we're "catching" a settling view.)

tryCaptureViewForDrag(mCapturedView, pointerId);
}
break;
}

case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_DRAGGING) {//正在拖动
// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(mActivePointerId)) break;

final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, index);//得到X
final float y = MotionEventCompat.getY(ev, index);//得到Y
final int idx = (int) (x - mLastMotionX[mActivePointerId]);//算出移动的X方向距离
final int idy = (int) (y - mLastMotionY[mActivePointerId]);//算出移动的Y方向距离

dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);//移动

saveLastMotion(ev);
} else {
// Check to see if any pointer is now over a draggable view.
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int pointerId = MotionEventCompat.getPointerId(ev, i);

// If pointer is invalid then skip the ACTION_MOVE.
if (!isValidPointerForActionMove(pointerId)) continue;

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);
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;//更新手指id
break;
}
}

if (newActivePointer == INVALID_POINTER) {
// We didn't find another pointer still touching the view, release it.
releaseViewForPointerUp();//释放
}
}
clearMotionHistory(pointerId);//清除数据
break;
}

case MotionEvent.ACTION_UP: {
if (mDragState == STATE_DRAGGING) {
releaseViewForPointerUp();//释放
}
cancel();
break;
}

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

private void dragTo(int left, int top, int dx, int dy) {、、参数dx和dy是前后两次ACTION_MOVE移动的距离
int clampedX = left;
int clampedY = top;
final int oldLeft = mCapturedView.getLeft();
final int oldTop = mCapturedView.getTop();
if (dx != 0) {
clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);//回调
ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);//移动
}
if (dy != 0) {
clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);//回调
ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);//移动
}

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

private void releaseViewForPointerUp() {
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);
}

/**
* Clamp the magnitude of value for absMin and absMax.
* If the value is below the minimum, it will be clamped to zero.
* If the value is above the maximum, it will be clamped to the maximum.
*
* @param value Value to clamp
* @param absMin Absolute value of the minimum significant value to return
* @param absMax Absolute value of the maximum value to return
* @return The clamped value with the same sign as <code>value</code>
*/

private float clampMag(float value, float absMin, float absMax) {
final float absValue = Math.abs(value);
if (absValue < absMin) return 0;
if (absValue > absMax) return value > 0 ? absMax : -absMax;
return value;
}

/**
* Like all callback events this must happen on the UI thread, but release
* involves some extra semantics. During a release (mReleaseInProgress)
* is the only time it is valid to call {@link #settleCapturedViewAt(int, int)}
* or {@link #flingCapturedView(int, int, int, int)}.
*/

private void dispatchViewReleased(float xvel, float yvel) {
mReleaseInProgress = true;
mCallback.onViewReleased(mCapturedView, xvel, yvel);
mReleaseInProgress = false;

if (mDragState == STATE_DRAGGING) {
// onViewReleased didn't call a method that would have changed this. Go idle.
setDragState(STATE_IDLE);
}
}

}

ACTION_DOWNshouldInterceptTouchEvent 中的差不多。当进入到 ACTION_MOVE 的时候,先判断 mDragState 是否为 STATE_DRAGGING ,而唯一调用 setDragState(STATE_DRAGGING) 的地方就是 tryCaptureViewForDrag() 了, 刚才在 ACTION_DOWN 里调用过 tryCaptureViewForDrag() 。当为 STATE_DRAGGING 的时候,开始不停的调用 dragTo() 对 mCaptureView 进行真正拖动了。而 dragTo() 通过调用 offsetLeftAndRight()offsetTopAndBottom() 来完成对 mCapturedView 移动。在 dispatchViewReleased() 的注释里面有介绍了两个方法,settleCapturedViewAt(int, int)flingCapturedView(int, int, int, int)

settleCapturedViewAt && flingCapturedView

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
public class ViewDragHelper {

/**
* Settle the captured view at the given (left, top) position.
* The appropriate velocity from prior motion will be taken into account.
* If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
* on each subsequent frame to continue the motion until it returns false. If this method
* returns false there is no further work to do to complete the movement.
*
* @param finalLeft Settled left edge position for the captured view
* @param finalTop Settled top edge position for the captured view
* @return true if animation should continue through {@link #continueSettling(boolean)} calls
*/

public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
if (!mReleaseInProgress) {
throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " +
"Callback#onViewReleased");
}

return forceSettleCapturedViewAt(finalLeft, finalTop,
(int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
(int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId));
}

/**
* Settle the captured view at the given (left, top) position.
*
* @param finalLeft Target left position for the captured view
* @param finalTop Target top position for the captured view
* @param xvel Horizontal velocity
* @param yvel Vertical velocity
* @return true if animation should continue through {@link #continueSettling(boolean)} calls
*/

private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
final int startLeft = mCapturedView.getLeft();
final int startTop = mCapturedView.getTop();
final int dx = finalLeft - startLeft;
final int dy = finalTop - startTop;

if (dx == 0 && dy == 0) {
// Nothing to do. Send callbacks, be done.
mScroller.abortAnimation();
setDragState(STATE_IDLE);
return false;
}

final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
mScroller.startScroll(startLeft, startTop, dx, dy, duration);//靠Scroll类完成

setDragState(STATE_SETTLING);
return true;
}

private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {
xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);
yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);
final int absDx = Math.abs(dx);
final int absDy = Math.abs(dy);
final int absXVel = Math.abs(xvel);
final int absYVel = Math.abs(yvel);
final int addedVel = absXVel + absYVel;
final int addedDistance = absDx + absDy;

final float xweight = xvel != 0 ? (float) absXVel / addedVel :
(float) absDx / addedDistance;
final float yweight = yvel != 0 ? (float) absYVel / addedVel :
(float) absDy / addedDistance;

int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));
int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child));

return (int) (xduration * xweight + yduration * yweight);
}

private int computeAxisDuration(int delta, int velocity, int motionRange) {
if (delta == 0) {
return 0;
}
//如果给定的速率velocity不为0,就通过距离除以速率来算出时间
final int width = mParentView.getWidth();
final int halfWidth = width / 2;
final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);
final float distance = halfWidth + halfWidth *
distanceInfluenceForSnapDuration(distanceRatio);

int duration;
velocity = Math.abs(velocity);
if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
final float range = (float) Math.abs(delta) / motionRange;
duration = (int) ((range + 1) * BASE_SETTLE_DURATION);
}
return Math.min(duration, MAX_SETTLE_DURATION);
}

/**
* Clamp the magnitude of value for absMin and absMax.
* If the value is below the minimum, it will be clamped to zero.
* If the value is above the maximum, it will be clamped to the maximum.
* 确保参数中给定的速率在正常范围之内
*
* @param value Value to clamp
* @param absMin Absolute value of the minimum significant value to return
* @param absMax Absolute value of the maximum value to return
* @return The clamped value with the same sign as <code>value</code>
*/

private int clampMag(int value, int absMin, int absMax) {
final int absValue = Math.abs(value);
if (absValue < absMin) return 0;
if (absValue > absMax) return value > 0 ? absMax : -absMax;
return value;
}

/**
* Settle the captured view based on standard free-moving fling behavior.
* The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame
* to continue the motion until it returns false.
*
* @param minLeft Minimum X position for the view's left edge
* @param minTop Minimum Y position for the view's top edge
* @param maxLeft Maximum X position for the view's left edge
* @param maxTop Maximum Y position for the view's top edge
*/

public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {
if (!mReleaseInProgress) {
throw new IllegalStateException("Cannot flingCapturedView outside of a call to " +
"Callback#onViewReleased");
}

mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(),
(int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
(int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
minLeft, maxLeft, minTop, maxTop);

setDragState(STATE_SETTLING);
}
}

这两个方法里一开始都会判断 mReleaseInProgress 为 false ,如果为 false 就会抛一个 IllegalStateException 异常, 而 mReleaseInProgress 唯一为true的时候就是在 dispatchViewReleased() 里调用 onViewReleased() 的时候。settleCapturedViewAt(int finalLeft, int finalTop) 以松手前的滑动速度为初速动,让捕获到的 View 自动滚动到指定位置。只能在 Callback 的 onViewReleased() 中调用。flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) 以松手前的滑动速度为初速动,让捕获到的 View 在指定范围内 fling ,也只能在 Callback 的 onViewReleased() 中调用。

smoothSlideViewTo

ViewDragHelper 还有一个移动 View 的方法是 smoothSlideViewTo(View child, int finalLeft, int finalTop):

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
public class ViewDragHelper {
/**
* Animate the view <code>child</code> to the given (left, top) position.
* If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
* on each subsequent frame to continue the motion until it returns false. If this method
* returns false there is no further work to do to complete the movement.
*
* <p>This operation does not count as a capture event, though {@link #getCapturedView()}
* will still report the sliding view while the slide is in progress.</p>
*
* @param child Child view to capture and animate
* @param finalLeft Final left position of child
* @param finalTop Final top position of child
* @return true if animation should continue through {@link #continueSettling(boolean)} calls
*/

public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
mCapturedView = child;
mActivePointerId = INVALID_POINTER;

boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) {
// If we're in an IDLE state to begin with and aren't moving anywhere, we
// end up having a non-null capturedView with an IDLE dragState
mCapturedView = null;
}

return continueSliding;
}
}

可以看到它不受 mReleaseInProgress 的限制,所以可以在任何地方调用,效果和 settleCapturedViewAt() 类似,因为它们最终都调用了 forceSettleCapturedViewAt() 来启动自动滚动,区别在于 settleCapturedViewAt() 会以最后松手前的滑动速率为初速度将 View 滚动到最终位置,而 smoothSlideViewTo() 滚动的初速度是0。 smoothSlideViewTo(View child, int finalLeft, int finalTop) 指定某个 View 自动滚动到指定的位置,初速度为 0 ,可在任何地方调用。