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