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