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