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