1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 
18 package android.support.v4.widget;
19 
20 import android.content.Context;
21 import android.support.v4.view.MotionEventCompat;
22 import android.support.v4.view.VelocityTrackerCompat;
23 import android.support.v4.view.ViewCompat;
24 import android.util.Log;
25 import android.view.MotionEvent;
26 import android.view.VelocityTracker;
27 import android.view.View;
28 import android.view.ViewConfiguration;
29 import android.view.ViewGroup;
30 import android.view.animation.Interpolator;
31 
32 import java.util.Arrays;
33 
34 /**
35  * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number
36  * of useful operations and state tracking for allowing a user to drag and reposition
37  * views within their parent ViewGroup.
38  */
39 public class ViewDragHelper {
40     private static final String TAG = "ViewDragHelper";
41 
42     /**
43      * A null/invalid pointer ID.
44      */
45     public static final int INVALID_POINTER = -1;
46 
47     /**
48      * A view is not currently being dragged or animating as a result of a fling/snap.
49      */
50     public static final int STATE_IDLE = 0;
51 
52     /**
53      * A view is currently being dragged. The position is currently changing as a result
54      * of user input or simulated user input.
55      */
56     public static final int STATE_DRAGGING = 1;
57 
58     /**
59      * A view is currently settling into place as a result of a fling or
60      * predefined non-interactive motion.
61      */
62     public static final int STATE_SETTLING = 2;
63 
64     /**
65      * Edge flag indicating that the left edge should be affected.
66      */
67     public static final int EDGE_LEFT = 1 << 0;
68 
69     /**
70      * Edge flag indicating that the right edge should be affected.
71      */
72     public static final int EDGE_RIGHT = 1 << 1;
73 
74     /**
75      * Edge flag indicating that the top edge should be affected.
76      */
77     public static final int EDGE_TOP = 1 << 2;
78 
79     /**
80      * Edge flag indicating that the bottom edge should be affected.
81      */
82     public static final int EDGE_BOTTOM = 1 << 3;
83 
84     /**
85      * Edge flag set indicating all edges should be affected.
86      */
87     public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;
88 
89     /**
90      * Indicates that a check should occur along the horizontal axis
91      */
92     public static final int DIRECTION_HORIZONTAL = 1 << 0;
93 
94     /**
95      * Indicates that a check should occur along the vertical axis
96      */
97     public static final int DIRECTION_VERTICAL = 1 << 1;
98 
99     /**
100      * Indicates that a check should occur along all axes
101      */
102     public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;
103 
104     private static final int EDGE_SIZE = 20; // dp
105 
106     private static final int BASE_SETTLE_DURATION = 256; // ms
107     private static final int MAX_SETTLE_DURATION = 600; // ms
108 
109     // Current drag state; idle, dragging or settling
110     private int mDragState;
111 
112     // Distance to travel before a drag may begin
113     private int mTouchSlop;
114 
115     // Last known position/pointer tracking
116     private int mActivePointerId = INVALID_POINTER;
117     private float[] mInitialMotionX;
118     private float[] mInitialMotionY;
119     private float[] mLastMotionX;
120     private float[] mLastMotionY;
121     private int[] mInitialEdgesTouched;
122     private int[] mEdgeDragsInProgress;
123     private int[] mEdgeDragsLocked;
124     private int mPointersDown;
125 
126     private VelocityTracker mVelocityTracker;
127     private float mMaxVelocity;
128     private float mMinVelocity;
129 
130     private int mEdgeSize;
131     private int mTrackingEdges;
132 
133     private ScrollerCompat mScroller;
134 
135     private final Callback mCallback;
136 
137     private View mCapturedView;
138     private boolean mReleaseInProgress;
139 
140     private final ViewGroup mParentView;
141 
142     /**
143      * A Callback is used as a communication channel with the ViewDragHelper back to the
144      * parent view using it. <code>on*</code>methods are invoked on siginficant events and several
145      * accessor methods are expected to provide the ViewDragHelper with more information
146      * about the state of the parent view upon request. The callback also makes decisions
147      * governing the range and draggability of child views.
148      */
149     public static abstract class Callback {
150         /**
151          * Called when the drag state changes. See the <code>STATE_*</code> constants
152          * for more information.
153          *
154          * @param state The new drag state
155          *
156          * @see #STATE_IDLE
157          * @see #STATE_DRAGGING
158          * @see #STATE_SETTLING
159          */
onViewDragStateChanged(int state)160         public void onViewDragStateChanged(int state) {}
161 
162         /**
163          * Called when the captured view's position changes as the result of a drag or settle.
164          *
165          * @param changedView View whose position changed
166          * @param left New X coordinate of the left edge of the view
167          * @param top New Y coordinate of the top edge of the view
168          * @param dx Change in X position from the last call
169          * @param dy Change in Y position from the last call
170          */
onViewPositionChanged(View changedView, int left, int top, int dx, int dy)171         public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}
172 
173         /**
174          * Called when a child view is captured for dragging or settling. The ID of the pointer
175          * currently dragging the captured view is supplied. If activePointerId is
176          * identified as {@link #INVALID_POINTER} the capture is programmatic instead of
177          * pointer-initiated.
178          *
179          * @param capturedChild Child view that was captured
180          * @param activePointerId Pointer id tracking the child capture
181          */
onViewCaptured(View capturedChild, int activePointerId)182         public void onViewCaptured(View capturedChild, int activePointerId) {}
183 
184         /**
185          * Called when the child view is no longer being actively dragged.
186          * The fling velocity is also supplied, if relevant. The velocity values may
187          * be clamped to system minimums or maximums.
188          *
189          * <p>Calling code may decide to fling or otherwise release the view to let it
190          * settle into place. It should do so using {@link #settleCapturedViewAt(int, int)}
191          * or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes
192          * one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING}
193          * and the view capture will not fully end until it comes to a complete stop.
194          * If neither of these methods is invoked before <code>onViewReleased</code> returns,
195          * the view will stop in place and the ViewDragHelper will return to
196          * {@link #STATE_IDLE}.</p>
197          *
198          * @param releasedChild The captured child view now being released
199          * @param xvel X velocity of the pointer as it left the screen in pixels per second.
200          * @param yvel Y velocity of the pointer as it left the screen in pixels per second.
201          */
onViewReleased(View releasedChild, float xvel, float yvel)202         public void onViewReleased(View releasedChild, float xvel, float yvel) {}
203 
204         /**
205          * Called when one of the subscribed edges in the parent view has been touched
206          * by the user while no child view is currently captured.
207          *
208          * @param edgeFlags A combination of edge flags describing the edge(s) currently touched
209          * @param pointerId ID of the pointer touching the described edge(s)
210          * @see #EDGE_LEFT
211          * @see #EDGE_TOP
212          * @see #EDGE_RIGHT
213          * @see #EDGE_BOTTOM
214          */
onEdgeTouched(int edgeFlags, int pointerId)215         public void onEdgeTouched(int edgeFlags, int pointerId) {}
216 
217         /**
218          * Called when the given edge may become locked. This can happen if an edge drag
219          * was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)}
220          * was called. This method should return true to lock this edge or false to leave it
221          * unlocked. The default behavior is to leave edges unlocked.
222          *
223          * @param edgeFlags A combination of edge flags describing the edge(s) locked
224          * @return true to lock the edge, false to leave it unlocked
225          */
onEdgeLock(int edgeFlags)226         public boolean onEdgeLock(int edgeFlags) {
227             return false;
228         }
229 
230         /**
231          * Called when the user has started a deliberate drag away from one
232          * of the subscribed edges in the parent view while no child view is currently captured.
233          *
234          * @param edgeFlags A combination of edge flags describing the edge(s) dragged
235          * @param pointerId ID of the pointer touching the described edge(s)
236          * @see #EDGE_LEFT
237          * @see #EDGE_TOP
238          * @see #EDGE_RIGHT
239          * @see #EDGE_BOTTOM
240          */
onEdgeDragStarted(int edgeFlags, int pointerId)241         public void onEdgeDragStarted(int edgeFlags, int pointerId) {}
242 
243         /**
244          * Called to determine the Z-order of child views.
245          *
246          * @param index the ordered position to query for
247          * @return index of the view that should be ordered at position <code>index</code>
248          */
getOrderedChildIndex(int index)249         public int getOrderedChildIndex(int index) {
250             return index;
251         }
252 
253         /**
254          * Return the magnitude of a draggable child view's horizontal range of motion in pixels.
255          * This method should return 0 for views that cannot move horizontally.
256          *
257          * @param child Child view to check
258          * @return range of horizontal motion in pixels
259          */
getViewHorizontalDragRange(View child)260         public int getViewHorizontalDragRange(View child) {
261             return 0;
262         }
263 
264         /**
265          * Return the magnitude of a draggable child view's vertical range of motion in pixels.
266          * This method should return 0 for views that cannot move vertically.
267          *
268          * @param child Child view to check
269          * @return range of vertical motion in pixels
270          */
getViewVerticalDragRange(View child)271         public int getViewVerticalDragRange(View child) {
272             return 0;
273         }
274 
275         /**
276          * Called when the user's input indicates that they want to capture the given child view
277          * with the pointer indicated by pointerId. The callback should return true if the user
278          * is permitted to drag the given view with the indicated pointer.
279          *
280          * <p>ViewDragHelper may call this method multiple times for the same view even if
281          * the view is already captured; this indicates that a new pointer is trying to take
282          * control of the view.</p>
283          *
284          * <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)}
285          * will follow if the capture is successful.</p>
286          *
287          * @param child Child the user is attempting to capture
288          * @param pointerId ID of the pointer attempting the capture
289          * @return true if capture should be allowed, false otherwise
290          */
tryCaptureView(View child, int pointerId)291         public abstract boolean tryCaptureView(View child, int pointerId);
292 
293         /**
294          * Restrict the motion of the dragged child view along the horizontal axis.
295          * The default implementation does not allow horizontal motion; the extending
296          * class must override this method and provide the desired clamping.
297          *
298          *
299          * @param child Child view being dragged
300          * @param left Attempted motion along the X axis
301          * @param dx Proposed change in position for left
302          * @return The new clamped position for left
303          */
clampViewPositionHorizontal(View child, int left, int dx)304         public int clampViewPositionHorizontal(View child, int left, int dx) {
305             return 0;
306         }
307 
308         /**
309          * Restrict the motion of the dragged child view along the vertical axis.
310          * The default implementation does not allow vertical motion; the extending
311          * class must override this method and provide the desired clamping.
312          *
313          *
314          * @param child Child view being dragged
315          * @param top Attempted motion along the Y axis
316          * @param dy Proposed change in position for top
317          * @return The new clamped position for top
318          */
clampViewPositionVertical(View child, int top, int dy)319         public int clampViewPositionVertical(View child, int top, int dy) {
320             return 0;
321         }
322     }
323 
324     /**
325      * Interpolator defining the animation curve for mScroller
326      */
327     private static final Interpolator sInterpolator = new Interpolator() {
328         public float getInterpolation(float t) {
329             t -= 1.0f;
330             return t * t * t * t * t + 1.0f;
331         }
332     };
333 
334     private final Runnable mSetIdleRunnable = new Runnable() {
335         public void run() {
336             setDragState(STATE_IDLE);
337         }
338     };
339 
340     /**
341      * Factory method to create a new ViewDragHelper.
342      *
343      * @param forParent Parent view to monitor
344      * @param cb Callback to provide information and receive events
345      * @return a new ViewDragHelper instance
346      */
create(ViewGroup forParent, Callback cb)347     public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
348         return new ViewDragHelper(forParent.getContext(), forParent, cb);
349     }
350 
351     /**
352      * Factory method to create a new ViewDragHelper.
353      *
354      * @param forParent Parent view to monitor
355      * @param sensitivity Multiplier for how sensitive the helper should be about detecting
356      *                    the start of a drag. Larger values are more sensitive. 1.0f is normal.
357      * @param cb Callback to provide information and receive events
358      * @return a new ViewDragHelper instance
359      */
create(ViewGroup forParent, float sensitivity, Callback cb)360     public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
361         final ViewDragHelper helper = create(forParent, cb);
362         helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
363         return helper;
364     }
365 
366     /**
367      * Apps should use ViewDragHelper.create() to get a new instance.
368      * This will allow VDH to use internal compatibility implementations for different
369      * platform versions.
370      *
371      * @param context Context to initialize config-dependent params from
372      * @param forParent Parent view to monitor
373      */
ViewDragHelper(Context context, ViewGroup forParent, Callback cb)374     private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
375         if (forParent == null) {
376             throw new IllegalArgumentException("Parent view may not be null");
377         }
378         if (cb == null) {
379             throw new IllegalArgumentException("Callback may not be null");
380         }
381 
382         mParentView = forParent;
383         mCallback = cb;
384 
385         final ViewConfiguration vc = ViewConfiguration.get(context);
386         final float density = context.getResources().getDisplayMetrics().density;
387         mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);
388 
389         mTouchSlop = vc.getScaledTouchSlop();
390         mMaxVelocity = vc.getScaledMaximumFlingVelocity();
391         mMinVelocity = vc.getScaledMinimumFlingVelocity();
392         mScroller = ScrollerCompat.create(context, sInterpolator);
393     }
394 
395     /**
396      * Set the minimum velocity that will be detected as having a magnitude greater than zero
397      * in pixels per second. Callback methods accepting a velocity will be clamped appropriately.
398      *
399      * @param minVel Minimum velocity to detect
400      */
setMinVelocity(float minVel)401     public void setMinVelocity(float minVel) {
402         mMinVelocity = minVel;
403     }
404 
405     /**
406      * Return the currently configured minimum velocity. Any flings with a magnitude less
407      * than this value in pixels per second. Callback methods accepting a velocity will receive
408      * zero as a velocity value if the real detected velocity was below this threshold.
409      *
410      * @return the minimum velocity that will be detected
411      */
getMinVelocity()412     public float getMinVelocity() {
413         return mMinVelocity;
414     }
415 
416     /**
417      * Retrieve the current drag state of this helper. This will return one of
418      * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
419      * @return The current drag state
420      */
getViewDragState()421     public int getViewDragState() {
422         return mDragState;
423     }
424 
425     /**
426      * Enable edge tracking for the selected edges of the parent view.
427      * The callback's {@link Callback#onEdgeTouched(int, int)} and
428      * {@link Callback#onEdgeDragStarted(int, int)} methods will only be invoked
429      * for edges for which edge tracking has been enabled.
430      *
431      * @param edgeFlags Combination of edge flags describing the edges to watch
432      * @see #EDGE_LEFT
433      * @see #EDGE_TOP
434      * @see #EDGE_RIGHT
435      * @see #EDGE_BOTTOM
436      */
setEdgeTrackingEnabled(int edgeFlags)437     public void setEdgeTrackingEnabled(int edgeFlags) {
438         mTrackingEdges = edgeFlags;
439     }
440 
441     /**
442      * Return the size of an edge. This is the range in pixels along the edges of this view
443      * that will actively detect edge touches or drags if edge tracking is enabled.
444      *
445      * @return The size of an edge in pixels
446      * @see #setEdgeTrackingEnabled(int)
447      */
getEdgeSize()448     public int getEdgeSize() {
449         return mEdgeSize;
450     }
451 
452     /**
453      * Capture a specific child view for dragging within the parent. The callback will be notified
454      * but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to
455      * capture this view.
456      *
457      * @param childView Child view to capture
458      * @param activePointerId ID of the pointer that is dragging the captured child view
459      */
captureChildView(View childView, int activePointerId)460     public void captureChildView(View childView, int activePointerId) {
461         if (childView.getParent() != mParentView) {
462             throw new IllegalArgumentException("captureChildView: parameter must be a descendant " +
463                     "of the ViewDragHelper's tracked parent view (" + mParentView + ")");
464         }
465 
466         mCapturedView = childView;
467         mActivePointerId = activePointerId;
468         mCallback.onViewCaptured(childView, activePointerId);
469         setDragState(STATE_DRAGGING);
470     }
471 
472     /**
473      * @return The currently captured view, or null if no view has been captured.
474      */
getCapturedView()475     public View getCapturedView() {
476         return mCapturedView;
477     }
478 
479     /**
480      * @return The ID of the pointer currently dragging the captured view,
481      *         or {@link #INVALID_POINTER}.
482      */
getActivePointerId()483     public int getActivePointerId() {
484         return mActivePointerId;
485     }
486 
487     /**
488      * @return The minimum distance in pixels that the user must travel to initiate a drag
489      */
getTouchSlop()490     public int getTouchSlop() {
491         return mTouchSlop;
492     }
493 
494     /**
495      * The result of a call to this method is equivalent to
496      * {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event.
497      */
cancel()498     public void cancel() {
499         mActivePointerId = INVALID_POINTER;
500         clearMotionHistory();
501 
502         if (mVelocityTracker != null) {
503             mVelocityTracker.recycle();
504             mVelocityTracker = null;
505         }
506     }
507 
508     /**
509      * {@link #cancel()}, but also abort all motion in progress and snap to the end of any
510      * animation.
511      */
abort()512     public void abort() {
513         cancel();
514         if (mDragState == STATE_SETTLING) {
515             final int oldX = mScroller.getCurrX();
516             final int oldY = mScroller.getCurrY();
517             mScroller.abortAnimation();
518             final int newX = mScroller.getCurrX();
519             final int newY = mScroller.getCurrY();
520             mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY);
521         }
522         setDragState(STATE_IDLE);
523     }
524 
525     /**
526      * Animate the view <code>child</code> to the given (left, top) position.
527      * If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
528      * on each subsequent frame to continue the motion until it returns false. If this method
529      * returns false there is no further work to do to complete the movement.
530      *
531      * <p>This operation does not count as a capture event, though {@link #getCapturedView()}
532      * will still report the sliding view while the slide is in progress.</p>
533      *
534      * @param child Child view to capture and animate
535      * @param finalLeft Final left position of child
536      * @param finalTop Final top position of child
537      * @return true if animation should continue through {@link #continueSettling(boolean)} calls
538      */
smoothSlideViewTo(View child, int finalLeft, int finalTop)539     public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
540         mCapturedView = child;
541         mActivePointerId = INVALID_POINTER;
542 
543         boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
544         if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) {
545             // If we're in an IDLE state to begin with and aren't moving anywhere, we
546             // end up having a non-null capturedView with an IDLE dragState
547             mCapturedView = null;
548         }
549 
550         return continueSliding;
551     }
552 
553     /**
554      * Settle the captured view at the given (left, top) position.
555      * The appropriate velocity from prior motion will be taken into account.
556      * If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
557      * on each subsequent frame to continue the motion until it returns false. If this method
558      * returns false there is no further work to do to complete the movement.
559      *
560      * @param finalLeft Settled left edge position for the captured view
561      * @param finalTop Settled top edge position for the captured view
562      * @return true if animation should continue through {@link #continueSettling(boolean)} calls
563      */
settleCapturedViewAt(int finalLeft, int finalTop)564     public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
565         if (!mReleaseInProgress) {
566             throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " +
567                     "Callback#onViewReleased");
568         }
569 
570         return forceSettleCapturedViewAt(finalLeft, finalTop,
571                 (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
572                 (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId));
573     }
574 
575     /**
576      * Settle the captured view at the given (left, top) position.
577      *
578      * @param finalLeft Target left position for the captured view
579      * @param finalTop Target top position for the captured view
580      * @param xvel Horizontal velocity
581      * @param yvel Vertical velocity
582      * @return true if animation should continue through {@link #continueSettling(boolean)} calls
583      */
forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel)584     private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
585         final int startLeft = mCapturedView.getLeft();
586         final int startTop = mCapturedView.getTop();
587         final int dx = finalLeft - startLeft;
588         final int dy = finalTop - startTop;
589 
590         if (dx == 0 && dy == 0) {
591             // Nothing to do. Send callbacks, be done.
592             mScroller.abortAnimation();
593             setDragState(STATE_IDLE);
594             return false;
595         }
596 
597         final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
598         mScroller.startScroll(startLeft, startTop, dx, dy, duration);
599 
600         setDragState(STATE_SETTLING);
601         return true;
602     }
603 
computeSettleDuration(View child, int dx, int dy, int xvel, int yvel)604     private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {
605         xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);
606         yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);
607         final int absDx = Math.abs(dx);
608         final int absDy = Math.abs(dy);
609         final int absXVel = Math.abs(xvel);
610         final int absYVel = Math.abs(yvel);
611         final int addedVel = absXVel + absYVel;
612         final int addedDistance = absDx + absDy;
613 
614         final float xweight = xvel != 0 ? (float) absXVel / addedVel :
615                 (float) absDx / addedDistance;
616         final float yweight = yvel != 0 ? (float) absYVel / addedVel :
617                 (float) absDy / addedDistance;
618 
619         int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));
620         int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child));
621 
622         return (int) (xduration * xweight + yduration * yweight);
623     }
624 
computeAxisDuration(int delta, int velocity, int motionRange)625     private int computeAxisDuration(int delta, int velocity, int motionRange) {
626         if (delta == 0) {
627             return 0;
628         }
629 
630         final int width = mParentView.getWidth();
631         final int halfWidth = width / 2;
632         final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);
633         final float distance = halfWidth + halfWidth *
634                 distanceInfluenceForSnapDuration(distanceRatio);
635 
636         int duration;
637         velocity = Math.abs(velocity);
638         if (velocity > 0) {
639             duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
640         } else {
641             final float range = (float) Math.abs(delta) / motionRange;
642             duration = (int) ((range + 1) * BASE_SETTLE_DURATION);
643         }
644         return Math.min(duration, MAX_SETTLE_DURATION);
645     }
646 
647     /**
648      * Clamp the magnitude of value for absMin and absMax.
649      * If the value is below the minimum, it will be clamped to zero.
650      * If the value is above the maximum, it will be clamped to the maximum.
651      *
652      * @param value Value to clamp
653      * @param absMin Absolute value of the minimum significant value to return
654      * @param absMax Absolute value of the maximum value to return
655      * @return The clamped value with the same sign as <code>value</code>
656      */
clampMag(int value, int absMin, int absMax)657     private int clampMag(int value, int absMin, int absMax) {
658         final int absValue = Math.abs(value);
659         if (absValue < absMin) return 0;
660         if (absValue > absMax) return value > 0 ? absMax : -absMax;
661         return value;
662     }
663 
664     /**
665      * Clamp the magnitude of value for absMin and absMax.
666      * If the value is below the minimum, it will be clamped to zero.
667      * If the value is above the maximum, it will be clamped to the maximum.
668      *
669      * @param value Value to clamp
670      * @param absMin Absolute value of the minimum significant value to return
671      * @param absMax Absolute value of the maximum value to return
672      * @return The clamped value with the same sign as <code>value</code>
673      */
clampMag(float value, float absMin, float absMax)674     private float clampMag(float value, float absMin, float absMax) {
675         final float absValue = Math.abs(value);
676         if (absValue < absMin) return 0;
677         if (absValue > absMax) return value > 0 ? absMax : -absMax;
678         return value;
679     }
680 
distanceInfluenceForSnapDuration(float f)681     private float distanceInfluenceForSnapDuration(float f) {
682         f -= 0.5f; // center the values about 0.
683         f *= 0.3f * Math.PI / 2.0f;
684         return (float) Math.sin(f);
685     }
686 
687     /**
688      * Settle the captured view based on standard free-moving fling behavior.
689      * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame
690      * to continue the motion until it returns false.
691      *
692      * @param minLeft Minimum X position for the view's left edge
693      * @param minTop Minimum Y position for the view's top edge
694      * @param maxLeft Maximum X position for the view's left edge
695      * @param maxTop Maximum Y position for the view's top edge
696      */
flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)697     public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {
698         if (!mReleaseInProgress) {
699             throw new IllegalStateException("Cannot flingCapturedView outside of a call to " +
700                     "Callback#onViewReleased");
701         }
702 
703         mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(),
704                 (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
705                 (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
706                 minLeft, maxLeft, minTop, maxTop);
707 
708         setDragState(STATE_SETTLING);
709     }
710 
711     /**
712      * Move the captured settling view by the appropriate amount for the current time.
713      * If <code>continueSettling</code> returns true, the caller should call it again
714      * on the next frame to continue.
715      *
716      * @param deferCallbacks true if state callbacks should be deferred via posted message.
717      *                       Set this to true if you are calling this method from
718      *                       {@link android.view.View#computeScroll()} or similar methods
719      *                       invoked as part of layout or drawing.
720      * @return true if settle is still in progress
721      */
continueSettling(boolean deferCallbacks)722     public boolean continueSettling(boolean deferCallbacks) {
723         if (mDragState == STATE_SETTLING) {
724             boolean keepGoing = mScroller.computeScrollOffset();
725             final int x = mScroller.getCurrX();
726             final int y = mScroller.getCurrY();
727             final int dx = x - mCapturedView.getLeft();
728             final int dy = y - mCapturedView.getTop();
729 
730             if (dx != 0) {
731                 ViewCompat.offsetLeftAndRight(mCapturedView, dx);
732             }
733             if (dy != 0) {
734                 ViewCompat.offsetTopAndBottom(mCapturedView, dy);
735             }
736 
737             if (dx != 0 || dy != 0) {
738                 mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
739             }
740 
741             if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
742                 // Close enough. The interpolator/scroller might think we're still moving
743                 // but the user sure doesn't.
744                 mScroller.abortAnimation();
745                 keepGoing = false;
746             }
747 
748             if (!keepGoing) {
749                 if (deferCallbacks) {
750                     mParentView.post(mSetIdleRunnable);
751                 } else {
752                     setDragState(STATE_IDLE);
753                 }
754             }
755         }
756 
757         return mDragState == STATE_SETTLING;
758     }
759 
760     /**
761      * Like all callback events this must happen on the UI thread, but release
762      * involves some extra semantics. During a release (mReleaseInProgress)
763      * is the only time it is valid to call {@link #settleCapturedViewAt(int, int)}
764      * or {@link #flingCapturedView(int, int, int, int)}.
765      */
dispatchViewReleased(float xvel, float yvel)766     private void dispatchViewReleased(float xvel, float yvel) {
767         mReleaseInProgress = true;
768         mCallback.onViewReleased(mCapturedView, xvel, yvel);
769         mReleaseInProgress = false;
770 
771         if (mDragState == STATE_DRAGGING) {
772             // onViewReleased didn't call a method that would have changed this. Go idle.
773             setDragState(STATE_IDLE);
774         }
775     }
776 
clearMotionHistory()777     private void clearMotionHistory() {
778         if (mInitialMotionX == null) {
779             return;
780         }
781         Arrays.fill(mInitialMotionX, 0);
782         Arrays.fill(mInitialMotionY, 0);
783         Arrays.fill(mLastMotionX, 0);
784         Arrays.fill(mLastMotionY, 0);
785         Arrays.fill(mInitialEdgesTouched, 0);
786         Arrays.fill(mEdgeDragsInProgress, 0);
787         Arrays.fill(mEdgeDragsLocked, 0);
788         mPointersDown = 0;
789     }
790 
clearMotionHistory(int pointerId)791     private void clearMotionHistory(int pointerId) {
792         if (mInitialMotionX == null || !isPointerDown(pointerId)) {
793             return;
794         }
795         mInitialMotionX[pointerId] = 0;
796         mInitialMotionY[pointerId] = 0;
797         mLastMotionX[pointerId] = 0;
798         mLastMotionY[pointerId] = 0;
799         mInitialEdgesTouched[pointerId] = 0;
800         mEdgeDragsInProgress[pointerId] = 0;
801         mEdgeDragsLocked[pointerId] = 0;
802         mPointersDown &= ~(1 << pointerId);
803     }
804 
ensureMotionHistorySizeForId(int pointerId)805     private void ensureMotionHistorySizeForId(int pointerId) {
806         if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) {
807             float[] imx = new float[pointerId + 1];
808             float[] imy = new float[pointerId + 1];
809             float[] lmx = new float[pointerId + 1];
810             float[] lmy = new float[pointerId + 1];
811             int[] iit = new int[pointerId + 1];
812             int[] edip = new int[pointerId + 1];
813             int[] edl = new int[pointerId + 1];
814 
815             if (mInitialMotionX != null) {
816                 System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length);
817                 System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length);
818                 System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length);
819                 System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length);
820                 System.arraycopy(mInitialEdgesTouched, 0, iit, 0, mInitialEdgesTouched.length);
821                 System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length);
822                 System.arraycopy(mEdgeDragsLocked, 0, edl, 0, mEdgeDragsLocked.length);
823             }
824 
825             mInitialMotionX = imx;
826             mInitialMotionY = imy;
827             mLastMotionX = lmx;
828             mLastMotionY = lmy;
829             mInitialEdgesTouched = iit;
830             mEdgeDragsInProgress = edip;
831             mEdgeDragsLocked = edl;
832         }
833     }
834 
saveInitialMotion(float x, float y, int pointerId)835     private void saveInitialMotion(float x, float y, int pointerId) {
836         ensureMotionHistorySizeForId(pointerId);
837         mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
838         mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
839         mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
840         mPointersDown |= 1 << pointerId;
841     }
842 
saveLastMotion(MotionEvent ev)843     private void saveLastMotion(MotionEvent ev) {
844         final int pointerCount = MotionEventCompat.getPointerCount(ev);
845         for (int i = 0; i < pointerCount; i++) {
846             final int pointerId = MotionEventCompat.getPointerId(ev, i);
847             // If pointer is invalid then skip saving on ACTION_MOVE.
848             if (!isValidPointerForActionMove(pointerId)) {
849                 continue;
850             }
851             final float x = MotionEventCompat.getX(ev, i);
852             final float y = MotionEventCompat.getY(ev, i);
853             mLastMotionX[pointerId] = x;
854             mLastMotionY[pointerId] = y;
855         }
856     }
857 
858     /**
859      * Check if the given pointer ID represents a pointer that is currently down (to the best
860      * of the ViewDragHelper's knowledge).
861      *
862      * <p>The state used to report this information is populated by the methods
863      * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or
864      * {@link #processTouchEvent(android.view.MotionEvent)}. If one of these methods has not
865      * been called for all relevant MotionEvents to track, the information reported
866      * by this method may be stale or incorrect.</p>
867      *
868      * @param pointerId pointer ID to check; corresponds to IDs provided by MotionEvent
869      * @return true if the pointer with the given ID is still down
870      */
isPointerDown(int pointerId)871     public boolean isPointerDown(int pointerId) {
872         return (mPointersDown & 1 << pointerId) != 0;
873     }
874 
setDragState(int state)875     void setDragState(int state) {
876         mParentView.removeCallbacks(mSetIdleRunnable);
877         if (mDragState != state) {
878             mDragState = state;
879             mCallback.onViewDragStateChanged(state);
880             if (mDragState == STATE_IDLE) {
881                 mCapturedView = null;
882             }
883         }
884     }
885 
886     /**
887      * Attempt to capture the view with the given pointer ID. The callback will be involved.
888      * This will put us into the "dragging" state. If we've already captured this view with
889      * this pointer this method will immediately return true without consulting the callback.
890      *
891      * @param toCapture View to capture
892      * @param pointerId Pointer to capture with
893      * @return true if capture was successful
894      */
tryCaptureViewForDrag(View toCapture, int pointerId)895     boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
896         if (toCapture == mCapturedView && mActivePointerId == pointerId) {
897             // Already done!
898             return true;
899         }
900         if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
901             mActivePointerId = pointerId;
902             captureChildView(toCapture, pointerId);
903             return true;
904         }
905         return false;
906     }
907 
908     /**
909      * Tests scrollability within child views of v given a delta of dx.
910      *
911      * @param v View to test for horizontal scrollability
912      * @param checkV Whether the view v passed should itself be checked for scrollability (true),
913      *               or just its children (false).
914      * @param dx Delta scrolled in pixels along the X axis
915      * @param dy Delta scrolled in pixels along the Y axis
916      * @param x X coordinate of the active touch point
917      * @param y Y coordinate of the active touch point
918      * @return true if child views of v can be scrolled by delta of dx.
919      */
canScroll(View v, boolean checkV, int dx, int dy, int x, int y)920     protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) {
921         if (v instanceof ViewGroup) {
922             final ViewGroup group = (ViewGroup) v;
923             final int scrollX = v.getScrollX();
924             final int scrollY = v.getScrollY();
925             final int count = group.getChildCount();
926             // Count backwards - let topmost views consume scroll distance first.
927             for (int i = count - 1; i >= 0; i--) {
928                 // TODO: Add versioned support here for transformed views.
929                 // This will not work for transformed views in Honeycomb+
930                 final View child = group.getChildAt(i);
931                 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
932                         y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
933                         canScroll(child, true, dx, dy, x + scrollX - child.getLeft(),
934                                 y + scrollY - child.getTop())) {
935                     return true;
936                 }
937             }
938         }
939 
940         return checkV && (ViewCompat.canScrollHorizontally(v, -dx) ||
941                 ViewCompat.canScrollVertically(v, -dy));
942     }
943 
944     /**
945      * Check if this event as provided to the parent view's onInterceptTouchEvent should
946      * cause the parent to intercept the touch event stream.
947      *
948      * @param ev MotionEvent provided to onInterceptTouchEvent
949      * @return true if the parent view should return true from onInterceptTouchEvent
950      */
shouldInterceptTouchEvent(MotionEvent ev)951     public boolean shouldInterceptTouchEvent(MotionEvent ev) {
952         final int action = MotionEventCompat.getActionMasked(ev);
953         final int actionIndex = MotionEventCompat.getActionIndex(ev);
954 
955         if (action == MotionEvent.ACTION_DOWN) {
956             // Reset things for a new event stream, just in case we didn't get
957             // the whole previous stream.
958             cancel();
959         }
960 
961         if (mVelocityTracker == null) {
962             mVelocityTracker = VelocityTracker.obtain();
963         }
964         mVelocityTracker.addMovement(ev);
965 
966         switch (action) {
967             case MotionEvent.ACTION_DOWN: {
968                 final float x = ev.getX();
969                 final float y = ev.getY();
970                 final int pointerId = MotionEventCompat.getPointerId(ev, 0);
971                 saveInitialMotion(x, y, pointerId);
972 
973                 final View toCapture = findTopChildUnder((int) x, (int) y);
974 
975                 // Catch a settling view if possible.
976                 if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
977                     tryCaptureViewForDrag(toCapture, pointerId);
978                 }
979 
980                 final int edgesTouched = mInitialEdgesTouched[pointerId];
981                 if ((edgesTouched & mTrackingEdges) != 0) {
982                     mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
983                 }
984                 break;
985             }
986 
987             case MotionEventCompat.ACTION_POINTER_DOWN: {
988                 final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
989                 final float x = MotionEventCompat.getX(ev, actionIndex);
990                 final float y = MotionEventCompat.getY(ev, actionIndex);
991 
992                 saveInitialMotion(x, y, pointerId);
993 
994                 // A ViewDragHelper can only manipulate one view at a time.
995                 if (mDragState == STATE_IDLE) {
996                     final int edgesTouched = mInitialEdgesTouched[pointerId];
997                     if ((edgesTouched & mTrackingEdges) != 0) {
998                         mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
999                     }
1000                 } else if (mDragState == STATE_SETTLING) {
1001                     // Catch a settling view if possible.
1002                     final View toCapture = findTopChildUnder((int) x, (int) y);
1003                     if (toCapture == mCapturedView) {
1004                         tryCaptureViewForDrag(toCapture, pointerId);
1005                     }
1006                 }
1007                 break;
1008             }
1009 
1010             case MotionEvent.ACTION_MOVE: {
1011                 if (mInitialMotionX == null || mInitialMotionY == null) break;
1012 
1013                 // First to cross a touch slop over a draggable view wins. Also report edge drags.
1014                 final int pointerCount = MotionEventCompat.getPointerCount(ev);
1015                 for (int i = 0; i < pointerCount; i++) {
1016                     final int pointerId = MotionEventCompat.getPointerId(ev, i);
1017 
1018                     // If pointer is invalid then skip the ACTION_MOVE.
1019                     if (!isValidPointerForActionMove(pointerId)) continue;
1020 
1021                     final float x = MotionEventCompat.getX(ev, i);
1022                     final float y = MotionEventCompat.getY(ev, i);
1023                     final float dx = x - mInitialMotionX[pointerId];
1024                     final float dy = y - mInitialMotionY[pointerId];
1025 
1026                     final View toCapture = findTopChildUnder((int) x, (int) y);
1027                     final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
1028                     if (pastSlop) {
1029                         // check the callback's
1030                         // getView[Horizontal|Vertical]DragRange methods to know
1031                         // if you can move at all along an axis, then see if it
1032                         // would clamp to the same value. If you can't move at
1033                         // all in every dimension with a nonzero range, bail.
1034                         final int oldLeft = toCapture.getLeft();
1035                         final int targetLeft = oldLeft + (int) dx;
1036                         final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
1037                                 targetLeft, (int) dx);
1038                         final int oldTop = toCapture.getTop();
1039                         final int targetTop = oldTop + (int) dy;
1040                         final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
1041                                 (int) dy);
1042                         final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
1043                                 toCapture);
1044                         final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
1045                         if ((horizontalDragRange == 0 || horizontalDragRange > 0
1046                                 && newLeft == oldLeft) && (verticalDragRange == 0
1047                                 || verticalDragRange > 0 && newTop == oldTop)) {
1048                             break;
1049                         }
1050                     }
1051                     reportNewEdgeDrags(dx, dy, pointerId);
1052                     if (mDragState == STATE_DRAGGING) {
1053                         // Callback might have started an edge drag
1054                         break;
1055                     }
1056 
1057                     if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
1058                         break;
1059                     }
1060                 }
1061                 saveLastMotion(ev);
1062                 break;
1063             }
1064 
1065             case MotionEventCompat.ACTION_POINTER_UP: {
1066                 final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
1067                 clearMotionHistory(pointerId);
1068                 break;
1069             }
1070 
1071             case MotionEvent.ACTION_UP:
1072             case MotionEvent.ACTION_CANCEL: {
1073                 cancel();
1074                 break;
1075             }
1076         }
1077 
1078         return mDragState == STATE_DRAGGING;
1079     }
1080 
1081     /**
1082      * Process a touch event received by the parent view. This method will dispatch callback events
1083      * as needed before returning. The parent view's onTouchEvent implementation should call this.
1084      *
1085      * @param ev The touch event received by the parent view
1086      */
processTouchEvent(MotionEvent ev)1087     public void processTouchEvent(MotionEvent ev) {
1088         final int action = MotionEventCompat.getActionMasked(ev);
1089         final int actionIndex = MotionEventCompat.getActionIndex(ev);
1090 
1091         if (action == MotionEvent.ACTION_DOWN) {
1092             // Reset things for a new event stream, just in case we didn't get
1093             // the whole previous stream.
1094             cancel();
1095         }
1096 
1097         if (mVelocityTracker == null) {
1098             mVelocityTracker = VelocityTracker.obtain();
1099         }
1100         mVelocityTracker.addMovement(ev);
1101 
1102         switch (action) {
1103             case MotionEvent.ACTION_DOWN: {
1104                 final float x = ev.getX();
1105                 final float y = ev.getY();
1106                 final int pointerId = MotionEventCompat.getPointerId(ev, 0);
1107                 final View toCapture = findTopChildUnder((int) x, (int) y);
1108 
1109                 saveInitialMotion(x, y, pointerId);
1110 
1111                 // Since the parent is already directly processing this touch event,
1112                 // there is no reason to delay for a slop before dragging.
1113                 // Start immediately if possible.
1114                 tryCaptureViewForDrag(toCapture, pointerId);
1115 
1116                 final int edgesTouched = mInitialEdgesTouched[pointerId];
1117                 if ((edgesTouched & mTrackingEdges) != 0) {
1118                     mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
1119                 }
1120                 break;
1121             }
1122 
1123             case MotionEventCompat.ACTION_POINTER_DOWN: {
1124                 final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
1125                 final float x = MotionEventCompat.getX(ev, actionIndex);
1126                 final float y = MotionEventCompat.getY(ev, actionIndex);
1127 
1128                 saveInitialMotion(x, y, pointerId);
1129 
1130                 // A ViewDragHelper can only manipulate one view at a time.
1131                 if (mDragState == STATE_IDLE) {
1132                     // If we're idle we can do anything! Treat it like a normal down event.
1133 
1134                     final View toCapture = findTopChildUnder((int) x, (int) y);
1135                     tryCaptureViewForDrag(toCapture, pointerId);
1136 
1137                     final int edgesTouched = mInitialEdgesTouched[pointerId];
1138                     if ((edgesTouched & mTrackingEdges) != 0) {
1139                         mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
1140                     }
1141                 } else if (isCapturedViewUnder((int) x, (int) y)) {
1142                     // We're still tracking a captured view. If the same view is under this
1143                     // point, we'll swap to controlling it with this pointer instead.
1144                     // (This will still work if we're "catching" a settling view.)
1145 
1146                     tryCaptureViewForDrag(mCapturedView, pointerId);
1147                 }
1148                 break;
1149             }
1150 
1151             case MotionEvent.ACTION_MOVE: {
1152                 if (mDragState == STATE_DRAGGING) {
1153                     // If pointer is invalid then skip the ACTION_MOVE.
1154                     if (!isValidPointerForActionMove(mActivePointerId)) break;
1155 
1156                     final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
1157                     final float x = MotionEventCompat.getX(ev, index);
1158                     final float y = MotionEventCompat.getY(ev, index);
1159                     final int idx = (int) (x - mLastMotionX[mActivePointerId]);
1160                     final int idy = (int) (y - mLastMotionY[mActivePointerId]);
1161 
1162                     dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
1163 
1164                     saveLastMotion(ev);
1165                 } else {
1166                     // Check to see if any pointer is now over a draggable view.
1167                     final int pointerCount = MotionEventCompat.getPointerCount(ev);
1168                     for (int i = 0; i < pointerCount; i++) {
1169                         final int pointerId = MotionEventCompat.getPointerId(ev, i);
1170 
1171                         // If pointer is invalid then skip the ACTION_MOVE.
1172                         if (!isValidPointerForActionMove(pointerId)) continue;
1173 
1174                         final float x = MotionEventCompat.getX(ev, i);
1175                         final float y = MotionEventCompat.getY(ev, i);
1176                         final float dx = x - mInitialMotionX[pointerId];
1177                         final float dy = y - mInitialMotionY[pointerId];
1178 
1179                         reportNewEdgeDrags(dx, dy, pointerId);
1180                         if (mDragState == STATE_DRAGGING) {
1181                             // Callback might have started an edge drag.
1182                             break;
1183                         }
1184 
1185                         final View toCapture = findTopChildUnder((int) x, (int) y);
1186                         if (checkTouchSlop(toCapture, dx, dy) &&
1187                                 tryCaptureViewForDrag(toCapture, pointerId)) {
1188                             break;
1189                         }
1190                     }
1191                     saveLastMotion(ev);
1192                 }
1193                 break;
1194             }
1195 
1196             case MotionEventCompat.ACTION_POINTER_UP: {
1197                 final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex);
1198                 if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
1199                     // Try to find another pointer that's still holding on to the captured view.
1200                     int newActivePointer = INVALID_POINTER;
1201                     final int pointerCount = MotionEventCompat.getPointerCount(ev);
1202                     for (int i = 0; i < pointerCount; i++) {
1203                         final int id = MotionEventCompat.getPointerId(ev, i);
1204                         if (id == mActivePointerId) {
1205                             // This one's going away, skip.
1206                             continue;
1207                         }
1208 
1209                         final float x = MotionEventCompat.getX(ev, i);
1210                         final float y = MotionEventCompat.getY(ev, i);
1211                         if (findTopChildUnder((int) x, (int) y) == mCapturedView &&
1212                                 tryCaptureViewForDrag(mCapturedView, id)) {
1213                             newActivePointer = mActivePointerId;
1214                             break;
1215                         }
1216                     }
1217 
1218                     if (newActivePointer == INVALID_POINTER) {
1219                         // We didn't find another pointer still touching the view, release it.
1220                         releaseViewForPointerUp();
1221                     }
1222                 }
1223                 clearMotionHistory(pointerId);
1224                 break;
1225             }
1226 
1227             case MotionEvent.ACTION_UP: {
1228                 if (mDragState == STATE_DRAGGING) {
1229                     releaseViewForPointerUp();
1230                 }
1231                 cancel();
1232                 break;
1233             }
1234 
1235             case MotionEvent.ACTION_CANCEL: {
1236                 if (mDragState == STATE_DRAGGING) {
1237                     dispatchViewReleased(0, 0);
1238                 }
1239                 cancel();
1240                 break;
1241             }
1242         }
1243     }
1244 
reportNewEdgeDrags(float dx, float dy, int pointerId)1245     private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
1246         int dragsStarted = 0;
1247         if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
1248             dragsStarted |= EDGE_LEFT;
1249         }
1250         if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
1251             dragsStarted |= EDGE_TOP;
1252         }
1253         if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) {
1254             dragsStarted |= EDGE_RIGHT;
1255         }
1256         if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {
1257             dragsStarted |= EDGE_BOTTOM;
1258         }
1259 
1260         if (dragsStarted != 0) {
1261             mEdgeDragsInProgress[pointerId] |= dragsStarted;
1262             mCallback.onEdgeDragStarted(dragsStarted, pointerId);
1263         }
1264     }
1265 
checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge)1266     private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
1267         final float absDelta = Math.abs(delta);
1268         final float absODelta = Math.abs(odelta);
1269 
1270         if ((mInitialEdgesTouched[pointerId] & edge) != edge  || (mTrackingEdges & edge) == 0 ||
1271                 (mEdgeDragsLocked[pointerId] & edge) == edge ||
1272                 (mEdgeDragsInProgress[pointerId] & edge) == edge ||
1273                 (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
1274             return false;
1275         }
1276         if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {
1277             mEdgeDragsLocked[pointerId] |= edge;
1278             return false;
1279         }
1280         return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
1281     }
1282 
1283     /**
1284      * Check if we've crossed a reasonable touch slop for the given child view.
1285      * If the child cannot be dragged along the horizontal or vertical axis, motion
1286      * along that axis will not count toward the slop check.
1287      *
1288      * @param child Child to check
1289      * @param dx Motion since initial position along X axis
1290      * @param dy Motion since initial position along Y axis
1291      * @return true if the touch slop has been crossed
1292      */
checkTouchSlop(View child, float dx, float dy)1293     private boolean checkTouchSlop(View child, float dx, float dy) {
1294         if (child == null) {
1295             return false;
1296         }
1297         final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
1298         final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;
1299 
1300         if (checkHorizontal && checkVertical) {
1301             return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
1302         } else if (checkHorizontal) {
1303             return Math.abs(dx) > mTouchSlop;
1304         } else if (checkVertical) {
1305             return Math.abs(dy) > mTouchSlop;
1306         }
1307         return false;
1308     }
1309 
1310     /**
1311      * Check if any pointer tracked in the current gesture has crossed
1312      * the required slop threshold.
1313      *
1314      * <p>This depends on internal state populated by
1315      * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or
1316      * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on
1317      * the results of this method after all currently available touch data
1318      * has been provided to one of these two methods.</p>
1319      *
1320      * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL},
1321      *                   {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL}
1322      * @return true if the slop threshold has been crossed, false otherwise
1323      */
checkTouchSlop(int directions)1324     public boolean checkTouchSlop(int directions) {
1325         final int count = mInitialMotionX.length;
1326         for (int i = 0; i < count; i++) {
1327             if (checkTouchSlop(directions, i)) {
1328                 return true;
1329             }
1330         }
1331         return false;
1332     }
1333 
1334     /**
1335      * Check if the specified pointer tracked in the current gesture has crossed
1336      * the required slop threshold.
1337      *
1338      * <p>This depends on internal state populated by
1339      * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or
1340      * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on
1341      * the results of this method after all currently available touch data
1342      * has been provided to one of these two methods.</p>
1343      *
1344      * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL},
1345      *                   {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL}
1346      * @param pointerId ID of the pointer to slop check as specified by MotionEvent
1347      * @return true if the slop threshold has been crossed, false otherwise
1348      */
checkTouchSlop(int directions, int pointerId)1349     public boolean checkTouchSlop(int directions, int pointerId) {
1350         if (!isPointerDown(pointerId)) {
1351             return false;
1352         }
1353 
1354         final boolean checkHorizontal = (directions & DIRECTION_HORIZONTAL) == DIRECTION_HORIZONTAL;
1355         final boolean checkVertical = (directions & DIRECTION_VERTICAL) == DIRECTION_VERTICAL;
1356 
1357         final float dx = mLastMotionX[pointerId] - mInitialMotionX[pointerId];
1358         final float dy = mLastMotionY[pointerId] - mInitialMotionY[pointerId];
1359 
1360         if (checkHorizontal && checkVertical) {
1361             return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
1362         } else if (checkHorizontal) {
1363             return Math.abs(dx) > mTouchSlop;
1364         } else if (checkVertical) {
1365             return Math.abs(dy) > mTouchSlop;
1366         }
1367         return false;
1368     }
1369 
1370     /**
1371      * Check if any of the edges specified were initially touched in the currently active gesture.
1372      * If there is no currently active gesture this method will return false.
1373      *
1374      * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT},
1375      *              {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and
1376      *              {@link #EDGE_ALL}
1377      * @return true if any of the edges specified were initially touched in the current gesture
1378      */
isEdgeTouched(int edges)1379     public boolean isEdgeTouched(int edges) {
1380         final int count = mInitialEdgesTouched.length;
1381         for (int i = 0; i < count; i++) {
1382             if (isEdgeTouched(edges, i)) {
1383                 return true;
1384             }
1385         }
1386         return false;
1387     }
1388 
1389     /**
1390      * Check if any of the edges specified were initially touched by the pointer with
1391      * the specified ID. If there is no currently active gesture or if there is no pointer with
1392      * the given ID currently down this method will return false.
1393      *
1394      * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT},
1395      *              {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and
1396      *              {@link #EDGE_ALL}
1397      * @return true if any of the edges specified were initially touched in the current gesture
1398      */
isEdgeTouched(int edges, int pointerId)1399     public boolean isEdgeTouched(int edges, int pointerId) {
1400         return isPointerDown(pointerId) && (mInitialEdgesTouched[pointerId] & edges) != 0;
1401     }
1402 
releaseViewForPointerUp()1403     private void releaseViewForPointerUp() {
1404         mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
1405         final float xvel = clampMag(
1406                 VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
1407                 mMinVelocity, mMaxVelocity);
1408         final float yvel = clampMag(
1409                 VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
1410                 mMinVelocity, mMaxVelocity);
1411         dispatchViewReleased(xvel, yvel);
1412     }
1413 
dragTo(int left, int top, int dx, int dy)1414     private void dragTo(int left, int top, int dx, int dy) {
1415         int clampedX = left;
1416         int clampedY = top;
1417         final int oldLeft = mCapturedView.getLeft();
1418         final int oldTop = mCapturedView.getTop();
1419         if (dx != 0) {
1420             clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
1421             ViewCompat.offsetLeftAndRight(mCapturedView, clampedX - oldLeft);
1422         }
1423         if (dy != 0) {
1424             clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
1425             ViewCompat.offsetTopAndBottom(mCapturedView, clampedY - oldTop);
1426         }
1427 
1428         if (dx != 0 || dy != 0) {
1429             final int clampedDx = clampedX - oldLeft;
1430             final int clampedDy = clampedY - oldTop;
1431             mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
1432                     clampedDx, clampedDy);
1433         }
1434     }
1435 
1436     /**
1437      * Determine if the currently captured view is under the given point in the
1438      * parent view's coordinate system. If there is no captured view this method
1439      * will return false.
1440      *
1441      * @param x X position to test in the parent's coordinate system
1442      * @param y Y position to test in the parent's coordinate system
1443      * @return true if the captured view is under the given point, false otherwise
1444      */
isCapturedViewUnder(int x, int y)1445     public boolean isCapturedViewUnder(int x, int y) {
1446         return isViewUnder(mCapturedView, x, y);
1447     }
1448 
1449     /**
1450      * Determine if the supplied view is under the given point in the
1451      * parent view's coordinate system.
1452      *
1453      * @param view Child view of the parent to hit test
1454      * @param x X position to test in the parent's coordinate system
1455      * @param y Y position to test in the parent's coordinate system
1456      * @return true if the supplied view is under the given point, false otherwise
1457      */
isViewUnder(View view, int x, int y)1458     public boolean isViewUnder(View view, int x, int y) {
1459         if (view == null) {
1460             return false;
1461         }
1462         return x >= view.getLeft() &&
1463                 x < view.getRight() &&
1464                 y >= view.getTop() &&
1465                 y < view.getBottom();
1466     }
1467 
1468     /**
1469      * Find the topmost child under the given point within the parent view's coordinate system.
1470      * The child order is determined using {@link Callback#getOrderedChildIndex(int)}.
1471      *
1472      * @param x X position to test in the parent's coordinate system
1473      * @param y Y position to test in the parent's coordinate system
1474      * @return The topmost child view under (x, y) or null if none found.
1475      */
findTopChildUnder(int x, int y)1476     public View findTopChildUnder(int x, int y) {
1477         final int childCount = mParentView.getChildCount();
1478         for (int i = childCount - 1; i >= 0; i--) {
1479             final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
1480             if (x >= child.getLeft() && x < child.getRight() &&
1481                     y >= child.getTop() && y < child.getBottom()) {
1482                 return child;
1483             }
1484         }
1485         return null;
1486     }
1487 
getEdgesTouched(int x, int y)1488     private int getEdgesTouched(int x, int y) {
1489         int result = 0;
1490 
1491         if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT;
1492         if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP;
1493         if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT;
1494         if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM;
1495 
1496         return result;
1497     }
1498 
isValidPointerForActionMove(int pointerId)1499     private boolean isValidPointerForActionMove(int pointerId) {
1500         if (!isPointerDown(pointerId)) {
1501             Log.e(TAG, "Ignoring pointerId=" + pointerId + " because ACTION_DOWN was not received "
1502                     + "for this pointer before ACTION_MOVE. It likely happened because "
1503                     + " ViewDragHelper did not receive all the events in the event stream.");
1504             return false;
1505         }
1506         return true;
1507     }
1508 }