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