• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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 package android.support.v7.widget.helper;
18 
19 import android.content.res.Resources;
20 import android.graphics.Canvas;
21 import android.graphics.Rect;
22 import android.os.Build;
23 import android.support.annotation.Nullable;
24 import android.support.v4.animation.AnimatorCompatHelper;
25 import android.support.v4.animation.AnimatorListenerCompat;
26 import android.support.v4.animation.AnimatorUpdateListenerCompat;
27 import android.support.v4.animation.ValueAnimatorCompat;
28 import android.support.v4.view.GestureDetectorCompat;
29 import android.support.v4.view.MotionEventCompat;
30 import android.support.v4.view.VelocityTrackerCompat;
31 import android.support.v4.view.ViewCompat;
32 import android.support.v7.recyclerview.R;
33 import android.support.v7.widget.LinearLayoutManager;
34 import android.support.v7.widget.RecyclerView;
35 import android.support.v7.widget.RecyclerView.OnItemTouchListener;
36 import android.support.v7.widget.RecyclerView.ViewHolder;
37 import android.util.Log;
38 import android.view.GestureDetector;
39 import android.view.HapticFeedbackConstants;
40 import android.view.MotionEvent;
41 import android.view.VelocityTracker;
42 import android.view.View;
43 import android.view.ViewConfiguration;
44 import android.view.ViewParent;
45 import android.view.animation.Interpolator;
46 
47 import java.util.ArrayList;
48 import java.util.List;
49 
50 /**
51  * This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView.
52  * <p>
53  * It works with a RecyclerView and a Callback class, which configures what type of interactions
54  * are enabled and also receives events when user performs these actions.
55  * <p>
56  * Depending on which functionality you support, you should override
57  * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder)} and / or
58  * {@link Callback#onSwiped(ViewHolder, int)}.
59  * <p>
60  * This class is designed to work with any LayoutManager but for certain situations, it can be
61  * optimized for your custom LayoutManager by extending methods in the
62  * {@link ItemTouchHelper.Callback} class or implementing {@link ItemTouchHelper.ViewDropHandler}
63  * interface in your LayoutManager.
64  * <p>
65  * By default, ItemTouchHelper moves the items' translateX/Y properties to reposition them. On
66  * platforms older than Honeycomb, ItemTouchHelper uses canvas translations and View's visibility
67  * property to move items in response to touch events. You can customize these behaviors by
68  * overriding {@link Callback#onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int,
69  * boolean)}
70  * or {@link Callback#onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int,
71  * boolean)}.
72  * <p/>
73  * Most of the time, you only need to override <code>onChildDraw</code> but due to limitations of
74  * platform prior to Honeycomb, you may need to implement <code>onChildDrawOver</code> as well.
75  */
76 public class ItemTouchHelper extends RecyclerView.ItemDecoration
77         implements RecyclerView.OnChildAttachStateChangeListener {
78 
79     /**
80      * Up direction, used for swipe & drag control.
81      */
82     public static final int UP = 1;
83 
84     /**
85      * Down direction, used for swipe & drag control.
86      */
87     public static final int DOWN = 1 << 1;
88 
89     /**
90      * Left direction, used for swipe & drag control.
91      */
92     public static final int LEFT = 1 << 2;
93 
94     /**
95      * Right direction, used for swipe & drag control.
96      */
97     public static final int RIGHT = 1 << 3;
98 
99     // If you change these relative direction values, update Callback#convertToAbsoluteDirection,
100     // Callback#convertToRelativeDirection.
101     /**
102      * Horizontal start direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout
103      * direction. Used for swipe & drag control.
104      */
105     public static final int START = LEFT << 2;
106 
107     /**
108      * Horizontal end direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout
109      * direction. Used for swipe & drag control.
110      */
111     public static final int END = RIGHT << 2;
112 
113     /**
114      * ItemTouchHelper is in idle state. At this state, either there is no related motion event by
115      * the user or latest motion events have not yet triggered a swipe or drag.
116      */
117     public static final int ACTION_STATE_IDLE = 0;
118 
119     /**
120      * A View is currently being swiped.
121      */
122     public static final int ACTION_STATE_SWIPE = 1;
123 
124     /**
125      * A View is currently being dragged.
126      */
127     public static final int ACTION_STATE_DRAG = 2;
128 
129     /**
130      * Animation type for views which are swiped successfully.
131      */
132     public static final int ANIMATION_TYPE_SWIPE_SUCCESS = 1 << 1;
133 
134     /**
135      * Animation type for views which are not completely swiped thus will animate back to their
136      * original position.
137      */
138     public static final int ANIMATION_TYPE_SWIPE_CANCEL = 1 << 2;
139 
140     /**
141      * Animation type for views that were dragged and now will animate to their final position.
142      */
143     public static final int ANIMATION_TYPE_DRAG = 1 << 3;
144 
145     private static final String TAG = "ItemTouchHelper";
146 
147     private static final boolean DEBUG = false;
148 
149     private static final int ACTIVE_POINTER_ID_NONE = -1;
150 
151     private static final int DIRECTION_FLAG_COUNT = 8;
152 
153     private static final int ACTION_MODE_IDLE_MASK = (1 << DIRECTION_FLAG_COUNT) - 1;
154 
155     private static final int ACTION_MODE_SWIPE_MASK = ACTION_MODE_IDLE_MASK << DIRECTION_FLAG_COUNT;
156 
157     private static final int ACTION_MODE_DRAG_MASK = ACTION_MODE_SWIPE_MASK << DIRECTION_FLAG_COUNT;
158 
159     /**
160      * The unit we are using to track velocity
161      */
162     private static final int PIXELS_PER_SECOND = 1000;
163 
164     /**
165      * Views, whose state should be cleared after they are detached from RecyclerView.
166      * This is necessary after swipe dismissing an item. We wait until animator finishes its job
167      * to clean these views.
168      */
169     final List<View> mPendingCleanup = new ArrayList<View>();
170 
171     /**
172      * Re-use array to calculate dx dy for a ViewHolder
173      */
174     private final float[] mTmpPosition = new float[2];
175 
176     /**
177      * Currently selected view holder
178      */
179     ViewHolder mSelected = null;
180 
181     /**
182      * The reference coordinates for the action start. For drag & drop, this is the time long
183      * press is completed vs for swipe, this is the initial touch point.
184      */
185     float mInitialTouchX;
186 
187     float mInitialTouchY;
188 
189     /**
190      * Set when ItemTouchHelper is assigned to a RecyclerView.
191      */
192     float mSwipeEscapeVelocity;
193 
194     /**
195      * Set when ItemTouchHelper is assigned to a RecyclerView.
196      */
197     float mMaxSwipeVelocity;
198 
199     /**
200      * The diff between the last event and initial touch.
201      */
202     float mDx;
203 
204     float mDy;
205 
206     /**
207      * The coordinates of the selected view at the time it is selected. We record these values
208      * when action starts so that we can consistently position it even if LayoutManager moves the
209      * View.
210      */
211     float mSelectedStartX;
212 
213     float mSelectedStartY;
214 
215     /**
216      * The pointer we are tracking.
217      */
218     int mActivePointerId = ACTIVE_POINTER_ID_NONE;
219 
220     /**
221      * Developer callback which controls the behavior of ItemTouchHelper.
222      */
223     Callback mCallback;
224 
225     /**
226      * Current mode.
227      */
228     int mActionState = ACTION_STATE_IDLE;
229 
230     /**
231      * The direction flags obtained from unmasking
232      * {@link Callback#getAbsoluteMovementFlags(RecyclerView, ViewHolder)} for the current
233      * action state.
234      */
235     int mSelectedFlags;
236 
237     /**
238      * When a View is dragged or swiped and needs to go back to where it was, we create a Recover
239      * Animation and animate it to its location using this custom Animator, instead of using
240      * framework Animators.
241      * Using framework animators has the side effect of clashing with ItemAnimator, creating
242      * jumpy UIs.
243      */
244     List<RecoverAnimation> mRecoverAnimations = new ArrayList<RecoverAnimation>();
245 
246     private int mSlop;
247 
248     private RecyclerView mRecyclerView;
249 
250     /**
251      * When user drags a view to the edge, we start scrolling the LayoutManager as long as View
252      * is partially out of bounds.
253      */
254     private final Runnable mScrollRunnable = new Runnable() {
255         @Override
256         public void run() {
257             if (mSelected != null && scrollIfNecessary()) {
258                 if (mSelected != null) { //it might be lost during scrolling
259                     moveIfNecessary(mSelected);
260                 }
261                 mRecyclerView.removeCallbacks(mScrollRunnable);
262                 ViewCompat.postOnAnimation(mRecyclerView, this);
263             }
264         }
265     };
266 
267     /**
268      * Used for detecting fling swipe
269      */
270     private VelocityTracker mVelocityTracker;
271 
272     //re-used list for selecting a swap target
273     private List<ViewHolder> mSwapTargets;
274 
275     //re used for for sorting swap targets
276     private List<Integer> mDistances;
277 
278     /**
279      * If drag & drop is supported, we use child drawing order to bring them to front.
280      */
281     private RecyclerView.ChildDrawingOrderCallback mChildDrawingOrderCallback = null;
282 
283     /**
284      * This keeps a reference to the child dragged by the user. Even after user stops dragging,
285      * until view reaches its final position (end of recover animation), we keep a reference so
286      * that it can be drawn above other children.
287      */
288     private View mOverdrawChild = null;
289 
290     /**
291      * We cache the position of the overdraw child to avoid recalculating it each time child
292      * position callback is called. This value is invalidated whenever a child is attached or
293      * detached.
294      */
295     private int mOverdrawChildPosition = -1;
296 
297     /**
298      * Used to detect long press.
299      */
300     private GestureDetectorCompat mGestureDetector;
301 
302     private final OnItemTouchListener mOnItemTouchListener
303             = new OnItemTouchListener() {
304         @Override
305         public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
306             mGestureDetector.onTouchEvent(event);
307             if (DEBUG) {
308                 Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event);
309             }
310             final int action = MotionEventCompat.getActionMasked(event);
311             if (action == MotionEvent.ACTION_DOWN) {
312                 mActivePointerId = event.getPointerId(0);
313                 mInitialTouchX = event.getX();
314                 mInitialTouchY = event.getY();
315                 obtainVelocityTracker();
316                 if (mSelected == null) {
317                     final RecoverAnimation animation = findAnimation(event);
318                     if (animation != null) {
319                         mInitialTouchX -= animation.mX;
320                         mInitialTouchY -= animation.mY;
321                         endRecoverAnimation(animation.mViewHolder, true);
322                         if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
323                             mCallback.clearView(mRecyclerView, animation.mViewHolder);
324                         }
325                         select(animation.mViewHolder, animation.mActionState);
326                         updateDxDy(event, mSelectedFlags, 0);
327                     }
328                 }
329             } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
330                 mActivePointerId = ACTIVE_POINTER_ID_NONE;
331                 select(null, ACTION_STATE_IDLE);
332             } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
333                 // in a non scroll orientation, if distance change is above threshold, we
334                 // can select the item
335                 final int index = event.findPointerIndex(mActivePointerId);
336                 if (DEBUG) {
337                     Log.d(TAG, "pointer index " + index);
338                 }
339                 if (index >= 0) {
340                     checkSelectForSwipe(action, event, index);
341                 }
342             }
343             if (mVelocityTracker != null) {
344                 mVelocityTracker.addMovement(event);
345             }
346             return mSelected != null;
347         }
348 
349         @Override
350         public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
351             mGestureDetector.onTouchEvent(event);
352             if (DEBUG) {
353                 Log.d(TAG,
354                         "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event);
355             }
356             if (mVelocityTracker != null) {
357                 mVelocityTracker.addMovement(event);
358             }
359             if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
360                 return;
361             }
362             final int action = MotionEventCompat.getActionMasked(event);
363             final int activePointerIndex = event.findPointerIndex(mActivePointerId);
364             if (activePointerIndex >= 0) {
365                 checkSelectForSwipe(action, event, activePointerIndex);
366             }
367             ViewHolder viewHolder = mSelected;
368             if (viewHolder == null) {
369                 return;
370             }
371             switch (action) {
372                 case MotionEvent.ACTION_MOVE: {
373                     // Find the index of the active pointer and fetch its position
374                     if (activePointerIndex >= 0) {
375                         updateDxDy(event, mSelectedFlags, activePointerIndex);
376                         moveIfNecessary(viewHolder);
377                         mRecyclerView.removeCallbacks(mScrollRunnable);
378                         mScrollRunnable.run();
379                         mRecyclerView.invalidate();
380                     }
381                     break;
382                 }
383                 case MotionEvent.ACTION_CANCEL:
384                     if (mVelocityTracker != null) {
385                         mVelocityTracker.clear();
386                     }
387                     // fall through
388                 case MotionEvent.ACTION_UP:
389                     select(null, ACTION_STATE_IDLE);
390                     mActivePointerId = ACTIVE_POINTER_ID_NONE;
391                     break;
392                 case MotionEvent.ACTION_POINTER_UP: {
393                     final int pointerIndex = MotionEventCompat.getActionIndex(event);
394                     final int pointerId = event.getPointerId(pointerIndex);
395                     if (pointerId == mActivePointerId) {
396                         // This was our active pointer going up. Choose a new
397                         // active pointer and adjust accordingly.
398                         final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
399                         mActivePointerId = event.getPointerId(newPointerIndex);
400                         updateDxDy(event, mSelectedFlags, pointerIndex);
401                     }
402                     break;
403                 }
404             }
405         }
406 
407         @Override
408         public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
409             if (!disallowIntercept) {
410                 return;
411             }
412             select(null, ACTION_STATE_IDLE);
413         }
414     };
415 
416     /**
417      * Temporary rect instance that is used when we need to lookup Item decorations.
418      */
419     private Rect mTmpRect;
420 
421     /**
422      * When user started to drag scroll. Reset when we don't scroll
423      */
424     private long mDragScrollStartTimeInMs;
425 
426     /**
427      * Creates an ItemTouchHelper that will work with the given Callback.
428      * <p>
429      * You can attach ItemTouchHelper to a RecyclerView via
430      * {@link #attachToRecyclerView(RecyclerView)}. Upon attaching, it will add an item decoration,
431      * an onItemTouchListener and a Child attach / detach listener to the RecyclerView.
432      *
433      * @param callback The Callback which controls the behavior of this touch helper.
434      */
ItemTouchHelper(Callback callback)435     public ItemTouchHelper(Callback callback) {
436         mCallback = callback;
437     }
438 
hitTest(View child, float x, float y, float left, float top)439     private static boolean hitTest(View child, float x, float y, float left, float top) {
440         return x >= left &&
441                 x <= left + child.getWidth() &&
442                 y >= top &&
443                 y <= top + child.getHeight();
444     }
445 
446     /**
447      * Attaches the ItemTouchHelper to the provided RecyclerView. If TouchHelper is already
448      * attached to a RecyclerView, it will first detach from the previous one. You can call this
449      * method with {@code null} to detach it from the current RecyclerView.
450      *
451      * @param recyclerView The RecyclerView instance to which you want to add this helper or
452      *                     {@code null} if you want to remove ItemTouchHelper from the current
453      *                     RecyclerView.
454      */
attachToRecyclerView(@ullable RecyclerView recyclerView)455     public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
456         if (mRecyclerView == recyclerView) {
457             return; // nothing to do
458         }
459         if (mRecyclerView != null) {
460             destroyCallbacks();
461         }
462         mRecyclerView = recyclerView;
463         if (mRecyclerView != null) {
464             final Resources resources = recyclerView.getResources();
465             mSwipeEscapeVelocity = resources
466                     .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
467             mMaxSwipeVelocity = resources
468                     .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
469             setupCallbacks();
470         }
471     }
472 
setupCallbacks()473     private void setupCallbacks() {
474         ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
475         mSlop = vc.getScaledTouchSlop();
476         mRecyclerView.addItemDecoration(this);
477         mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
478         mRecyclerView.addOnChildAttachStateChangeListener(this);
479         initGestureDetector();
480     }
481 
destroyCallbacks()482     private void destroyCallbacks() {
483         mRecyclerView.removeItemDecoration(this);
484         mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener);
485         mRecyclerView.removeOnChildAttachStateChangeListener(this);
486         // clean all attached
487         final int recoverAnimSize = mRecoverAnimations.size();
488         for (int i = recoverAnimSize - 1; i >= 0; i--) {
489             final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0);
490             mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder);
491         }
492         mRecoverAnimations.clear();
493         mOverdrawChild = null;
494         mOverdrawChildPosition = -1;
495         releaseVelocityTracker();
496     }
497 
initGestureDetector()498     private void initGestureDetector() {
499         if (mGestureDetector != null) {
500             return;
501         }
502         mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
503                 new ItemTouchHelperGestureListener());
504     }
505 
getSelectedDxDy(float[] outPosition)506     private void getSelectedDxDy(float[] outPosition) {
507         if ((mSelectedFlags & (LEFT | RIGHT)) != 0) {
508             outPosition[0] = mSelectedStartX + mDx - mSelected.itemView.getLeft();
509         } else {
510             outPosition[0] = ViewCompat.getTranslationX(mSelected.itemView);
511         }
512         if ((mSelectedFlags & (UP | DOWN)) != 0) {
513             outPosition[1] = mSelectedStartY + mDy - mSelected.itemView.getTop();
514         } else {
515             outPosition[1] = ViewCompat.getTranslationY(mSelected.itemView);
516         }
517     }
518 
519     @Override
onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)520     public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
521         float dx = 0, dy = 0;
522         if (mSelected != null) {
523             getSelectedDxDy(mTmpPosition);
524             dx = mTmpPosition[0];
525             dy = mTmpPosition[1];
526         }
527         mCallback.onDrawOver(c, parent, mSelected,
528                 mRecoverAnimations, mActionState, dx, dy);
529     }
530 
531     @Override
onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)532     public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
533         // we don't know if RV changed something so we should invalidate this index.
534         mOverdrawChildPosition = -1;
535         float dx = 0, dy = 0;
536         if (mSelected != null) {
537             getSelectedDxDy(mTmpPosition);
538             dx = mTmpPosition[0];
539             dy = mTmpPosition[1];
540         }
541         mCallback.onDraw(c, parent, mSelected,
542                 mRecoverAnimations, mActionState, dx, dy);
543     }
544 
545     /**
546      * Starts dragging or swiping the given View. Call with null if you want to clear it.
547      *
548      * @param selected    The ViewHolder to drag or swipe. Can be null if you want to cancel the
549      *                    current action
550      * @param actionState The type of action
551      */
select(ViewHolder selected, int actionState)552     private void select(ViewHolder selected, int actionState) {
553         if (selected == mSelected && actionState == mActionState) {
554             return;
555         }
556         mDragScrollStartTimeInMs = Long.MIN_VALUE;
557         final int prevActionState = mActionState;
558         // prevent duplicate animations
559         endRecoverAnimation(selected, true);
560         mActionState = actionState;
561         if (actionState == ACTION_STATE_DRAG) {
562             // we remove after animation is complete. this means we only elevate the last drag
563             // child but that should perform good enough as it is very hard to start dragging a
564             // new child before the previous one settles.
565             mOverdrawChild = selected.itemView;
566             addChildDrawingOrderCallback();
567         }
568         int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState))
569                 - 1;
570         boolean preventLayout = false;
571 
572         if (mSelected != null) {
573             final ViewHolder prevSelected = mSelected;
574             if (prevSelected.itemView.getParent() != null) {
575                 final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0
576                         : swipeIfNecessary(prevSelected);
577                 releaseVelocityTracker();
578                 // find where we should animate to
579                 final float targetTranslateX, targetTranslateY;
580                 int animationType;
581                 switch (swipeDir) {
582                     case LEFT:
583                     case RIGHT:
584                     case START:
585                     case END:
586                         targetTranslateY = 0;
587                         targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();
588                         break;
589                     case UP:
590                     case DOWN:
591                         targetTranslateX = 0;
592                         targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight();
593                         break;
594                     default:
595                         targetTranslateX = 0;
596                         targetTranslateY = 0;
597                 }
598                 if (prevActionState == ACTION_STATE_DRAG) {
599                     animationType = ANIMATION_TYPE_DRAG;
600                 } else if (swipeDir > 0) {
601                     animationType = ANIMATION_TYPE_SWIPE_SUCCESS;
602                 } else {
603                     animationType = ANIMATION_TYPE_SWIPE_CANCEL;
604                 }
605                 getSelectedDxDy(mTmpPosition);
606                 final float currentTranslateX = mTmpPosition[0];
607                 final float currentTranslateY = mTmpPosition[1];
608                 final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType,
609                         prevActionState, currentTranslateX, currentTranslateY,
610                         targetTranslateX, targetTranslateY) {
611                     @Override
612                     public void onAnimationEnd(ValueAnimatorCompat animation) {
613                         super.onAnimationEnd(animation);
614                         if (this.mOverridden) {
615                             return;
616                         }
617                         if (swipeDir <= 0) {
618                             // this is a drag or failed swipe. recover immediately
619                             mCallback.clearView(mRecyclerView, prevSelected);
620                             // full cleanup will happen on onDrawOver
621                         } else {
622                             // wait until remove animation is complete.
623                             mPendingCleanup.add(prevSelected.itemView);
624                             mIsPendingCleanup = true;
625                             if (swipeDir > 0) {
626                                 // Animation might be ended by other animators during a layout.
627                                 // We defer callback to avoid editing adapter during a layout.
628                                 postDispatchSwipe(this, swipeDir);
629                             }
630                         }
631                         // removed from the list after it is drawn for the last time
632                         if (mOverdrawChild == prevSelected.itemView) {
633                             removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
634                         }
635                     }
636                 };
637                 final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType,
638                         targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY);
639                 rv.setDuration(duration);
640                 mRecoverAnimations.add(rv);
641                 rv.start();
642                 preventLayout = true;
643             } else {
644                 removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
645                 mCallback.clearView(mRecyclerView, prevSelected);
646             }
647             mSelected = null;
648         }
649         if (selected != null) {
650             mSelectedFlags =
651                     (mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask)
652                             >> (mActionState * DIRECTION_FLAG_COUNT);
653             mSelectedStartX = selected.itemView.getLeft();
654             mSelectedStartY = selected.itemView.getTop();
655             mSelected = selected;
656 
657             if (actionState == ACTION_STATE_DRAG) {
658                 mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
659             }
660         }
661         final ViewParent rvParent = mRecyclerView.getParent();
662         if (rvParent != null) {
663             rvParent.requestDisallowInterceptTouchEvent(mSelected != null);
664         }
665         if (!preventLayout) {
666             mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();
667         }
668         mCallback.onSelectedChanged(mSelected, mActionState);
669         mRecyclerView.invalidate();
670     }
671 
postDispatchSwipe(final RecoverAnimation anim, final int swipeDir)672     private void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir) {
673         // wait until animations are complete.
674         mRecyclerView.post(new Runnable() {
675             @Override
676             public void run() {
677                 if (mRecyclerView != null && mRecyclerView.isAttachedToWindow() &&
678                         !anim.mOverridden &&
679                         anim.mViewHolder.getAdapterPosition() != RecyclerView.NO_POSITION) {
680                     final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator();
681                     // if animator is running or we have other active recover animations, we try
682                     // not to call onSwiped because DefaultItemAnimator is not good at merging
683                     // animations. Instead, we wait and batch.
684                     if ((animator == null || !animator.isRunning(null))
685                             && !hasRunningRecoverAnim()) {
686                         mCallback.onSwiped(anim.mViewHolder, swipeDir);
687                     } else {
688                         mRecyclerView.post(this);
689                     }
690                 }
691             }
692         });
693     }
694 
hasRunningRecoverAnim()695     private boolean hasRunningRecoverAnim() {
696         final int size = mRecoverAnimations.size();
697         for (int i = 0; i < size; i++) {
698             if (!mRecoverAnimations.get(i).mEnded) {
699                 return true;
700             }
701         }
702         return false;
703     }
704 
705     /**
706      * If user drags the view to the edge, trigger a scroll if necessary.
707      */
scrollIfNecessary()708     private boolean scrollIfNecessary() {
709         if (mSelected == null) {
710             mDragScrollStartTimeInMs = Long.MIN_VALUE;
711             return false;
712         }
713         final long now = System.currentTimeMillis();
714         final long scrollDuration = mDragScrollStartTimeInMs
715                 == Long.MIN_VALUE ? 0 : now - mDragScrollStartTimeInMs;
716         RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
717         if (mTmpRect == null) {
718             mTmpRect = new Rect();
719         }
720         int scrollX = 0;
721         int scrollY = 0;
722         lm.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect);
723         if (lm.canScrollHorizontally()) {
724             int curX = (int) (mSelectedStartX + mDx);
725             final int leftDiff = curX - mTmpRect.left - mRecyclerView.getPaddingLeft();
726             if (mDx < 0 && leftDiff < 0) {
727                 scrollX = leftDiff;
728             } else if (mDx > 0) {
729                 final int rightDiff =
730                         curX + mSelected.itemView.getWidth() + mTmpRect.right
731                                 - (mRecyclerView.getWidth() - mRecyclerView.getPaddingRight());
732                 if (rightDiff > 0) {
733                     scrollX = rightDiff;
734                 }
735             }
736         }
737         if (lm.canScrollVertically()) {
738             int curY = (int) (mSelectedStartY + mDy);
739             final int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop();
740             if (mDy < 0 && topDiff < 0) {
741                 scrollY = topDiff;
742             } else if (mDy > 0) {
743                 final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom -
744                         (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom());
745                 if (bottomDiff > 0) {
746                     scrollY = bottomDiff;
747                 }
748             }
749         }
750         if (scrollX != 0) {
751             scrollX = mCallback.interpolateOutOfBoundsScroll(mRecyclerView,
752                     mSelected.itemView.getWidth(), scrollX,
753                     mRecyclerView.getWidth(), scrollDuration);
754         }
755         if (scrollY != 0) {
756             scrollY = mCallback.interpolateOutOfBoundsScroll(mRecyclerView,
757                     mSelected.itemView.getHeight(), scrollY,
758                     mRecyclerView.getHeight(), scrollDuration);
759         }
760         if (scrollX != 0 || scrollY != 0) {
761             if (mDragScrollStartTimeInMs == Long.MIN_VALUE) {
762                 mDragScrollStartTimeInMs = now;
763             }
764             mRecyclerView.scrollBy(scrollX, scrollY);
765             return true;
766         }
767         mDragScrollStartTimeInMs = Long.MIN_VALUE;
768         return false;
769     }
770 
findSwapTargets(ViewHolder viewHolder)771     private List<ViewHolder> findSwapTargets(ViewHolder viewHolder) {
772         if (mSwapTargets == null) {
773             mSwapTargets = new ArrayList<ViewHolder>();
774             mDistances = new ArrayList<Integer>();
775         } else {
776             mSwapTargets.clear();
777             mDistances.clear();
778         }
779         final int margin = mCallback.getBoundingBoxMargin();
780         final int left = Math.round(mSelectedStartX + mDx) - margin;
781         final int top = Math.round(mSelectedStartY + mDy) - margin;
782         final int right = left + viewHolder.itemView.getWidth() + 2 * margin;
783         final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin;
784         final int centerX = (left + right) / 2;
785         final int centerY = (top + bottom) / 2;
786         final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
787         final int childCount = lm.getChildCount();
788         for (int i = 0; i < childCount; i++) {
789             View other = lm.getChildAt(i);
790             if (other == viewHolder.itemView) {
791                 continue;//myself!
792             }
793             if (other.getBottom() < top || other.getTop() > bottom
794                     || other.getRight() < left || other.getLeft() > right) {
795                 continue;
796             }
797             final ViewHolder otherVh = mRecyclerView.getChildViewHolder(other);
798             if (mCallback.canDropOver(mRecyclerView, mSelected, otherVh)) {
799                 // find the index to add
800                 final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2);
801                 final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2);
802                 final int dist = dx * dx + dy * dy;
803 
804                 int pos = 0;
805                 final int cnt = mSwapTargets.size();
806                 for (int j = 0; j < cnt; j++) {
807                     if (dist > mDistances.get(j)) {
808                         pos++;
809                     } else {
810                         break;
811                     }
812                 }
813                 mSwapTargets.add(pos, otherVh);
814                 mDistances.add(pos, dist);
815             }
816         }
817         return mSwapTargets;
818     }
819 
820     /**
821      * Checks if we should swap w/ another view holder.
822      */
moveIfNecessary(ViewHolder viewHolder)823     private void moveIfNecessary(ViewHolder viewHolder) {
824         if (mRecyclerView.isLayoutRequested()) {
825             return;
826         }
827         if (mActionState != ACTION_STATE_DRAG) {
828             return;
829         }
830 
831         final float threshold = mCallback.getMoveThreshold(viewHolder);
832         final int x = (int) (mSelectedStartX + mDx);
833         final int y = (int) (mSelectedStartY + mDy);
834         if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold
835                 && Math.abs(x - viewHolder.itemView.getLeft())
836                 < viewHolder.itemView.getWidth() * threshold) {
837             return;
838         }
839         List<ViewHolder> swapTargets = findSwapTargets(viewHolder);
840         if (swapTargets.size() == 0) {
841             return;
842         }
843         // may swap.
844         ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y);
845         if (target == null) {
846             mSwapTargets.clear();
847             mDistances.clear();
848             return;
849         }
850         final int toPosition = target.getAdapterPosition();
851         final int fromPosition = viewHolder.getAdapterPosition();
852         if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
853             // keep target visible
854             mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
855                     target, toPosition, x, y);
856         }
857     }
858 
859     @Override
onChildViewAttachedToWindow(View view)860     public void onChildViewAttachedToWindow(View view) {
861     }
862 
863     @Override
onChildViewDetachedFromWindow(View view)864     public void onChildViewDetachedFromWindow(View view) {
865         removeChildDrawingOrderCallbackIfNecessary(view);
866         final ViewHolder holder = mRecyclerView.getChildViewHolder(view);
867         if (holder == null) {
868             return;
869         }
870         if (mSelected != null && holder == mSelected) {
871             select(null, ACTION_STATE_IDLE);
872         } else {
873             endRecoverAnimation(holder, false); // this may push it into pending cleanup list.
874             if (mPendingCleanup.remove(holder.itemView)) {
875                 mCallback.clearView(mRecyclerView, holder);
876             }
877         }
878     }
879 
880     /**
881      * Returns the animation type or 0 if cannot be found.
882      */
endRecoverAnimation(ViewHolder viewHolder, boolean override)883     private int endRecoverAnimation(ViewHolder viewHolder, boolean override) {
884         final int recoverAnimSize = mRecoverAnimations.size();
885         for (int i = recoverAnimSize - 1; i >= 0; i--) {
886             final RecoverAnimation anim = mRecoverAnimations.get(i);
887             if (anim.mViewHolder == viewHolder) {
888                 anim.mOverridden |= override;
889                 if (!anim.mEnded) {
890                     anim.cancel();
891                 }
892                 mRecoverAnimations.remove(i);
893                 return anim.mAnimationType;
894             }
895         }
896         return 0;
897     }
898 
899     @Override
getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)900     public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
901             RecyclerView.State state) {
902         outRect.setEmpty();
903     }
904 
obtainVelocityTracker()905     private void obtainVelocityTracker() {
906         if (mVelocityTracker != null) {
907             mVelocityTracker.recycle();
908         }
909         mVelocityTracker = VelocityTracker.obtain();
910     }
911 
releaseVelocityTracker()912     private void releaseVelocityTracker() {
913         if (mVelocityTracker != null) {
914             mVelocityTracker.recycle();
915             mVelocityTracker = null;
916         }
917     }
918 
findSwipedView(MotionEvent motionEvent)919     private ViewHolder findSwipedView(MotionEvent motionEvent) {
920         final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
921         if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
922             return null;
923         }
924         final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId);
925         final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX;
926         final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY;
927         final float absDx = Math.abs(dx);
928         final float absDy = Math.abs(dy);
929 
930         if (absDx < mSlop && absDy < mSlop) {
931             return null;
932         }
933         if (absDx > absDy && lm.canScrollHorizontally()) {
934             return null;
935         } else if (absDy > absDx && lm.canScrollVertically()) {
936             return null;
937         }
938         View child = findChildView(motionEvent);
939         if (child == null) {
940             return null;
941         }
942         return mRecyclerView.getChildViewHolder(child);
943     }
944 
945     /**
946      * Checks whether we should select a View for swiping.
947      */
checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex)948     private boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
949         if (mSelected != null || action != MotionEvent.ACTION_MOVE
950                 || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) {
951             return false;
952         }
953         if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
954             return false;
955         }
956         final ViewHolder vh = findSwipedView(motionEvent);
957         if (vh == null) {
958             return false;
959         }
960         final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);
961 
962         final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK)
963                 >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);
964 
965         if (swipeFlags == 0) {
966             return false;
967         }
968 
969         // mDx and mDy are only set in allowed directions. We use custom x/y here instead of
970         // updateDxDy to avoid swiping if user moves more in the other direction
971         final float x = motionEvent.getX(pointerIndex);
972         final float y = motionEvent.getY(pointerIndex);
973 
974         // Calculate the distance moved
975         final float dx = x - mInitialTouchX;
976         final float dy = y - mInitialTouchY;
977         // swipe target is chose w/o applying flags so it does not really check if swiping in that
978         // direction is allowed. This why here, we use mDx mDy to check slope value again.
979         final float absDx = Math.abs(dx);
980         final float absDy = Math.abs(dy);
981 
982         if (absDx < mSlop && absDy < mSlop) {
983             return false;
984         }
985         if (absDx > absDy) {
986             if (dx < 0 && (swipeFlags & LEFT) == 0) {
987                 return false;
988             }
989             if (dx > 0 && (swipeFlags & RIGHT) == 0) {
990                 return false;
991             }
992         } else {
993             if (dy < 0 && (swipeFlags & UP) == 0) {
994                 return false;
995             }
996             if (dy > 0 && (swipeFlags & DOWN) == 0) {
997                 return false;
998             }
999         }
1000         mDx = mDy = 0f;
1001         mActivePointerId = motionEvent.getPointerId(0);
1002         select(vh, ACTION_STATE_SWIPE);
1003         return true;
1004     }
1005 
findChildView(MotionEvent event)1006     private View findChildView(MotionEvent event) {
1007         // first check elevated views, if none, then call RV
1008         final float x = event.getX();
1009         final float y = event.getY();
1010         if (mSelected != null) {
1011             final View selectedView = mSelected.itemView;
1012             if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) {
1013                 return selectedView;
1014             }
1015         }
1016         for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) {
1017             final RecoverAnimation anim = mRecoverAnimations.get(i);
1018             final View view = anim.mViewHolder.itemView;
1019             if (hitTest(view, x, y, anim.mX, anim.mY)) {
1020                 return view;
1021             }
1022         }
1023         return mRecyclerView.findChildViewUnder(x, y);
1024     }
1025 
1026     /**
1027      * Starts dragging the provided ViewHolder. By default, ItemTouchHelper starts a drag when a
1028      * View is long pressed. You can disable that behavior by overriding
1029      * {@link ItemTouchHelper.Callback#isLongPressDragEnabled()}.
1030      * <p>
1031      * For this method to work:
1032      * <ul>
1033      * <li>The provided ViewHolder must be a child of the RecyclerView to which this
1034      * ItemTouchHelper
1035      * is attached.</li>
1036      * <li>{@link ItemTouchHelper.Callback} must have dragging enabled.</li>
1037      * <li>There must be a previous touch event that was reported to the ItemTouchHelper
1038      * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener
1039      * grabs previous events, this should work as expected.</li>
1040      * </ul>
1041      *
1042      * For example, if you would like to let your user to be able to drag an Item by touching one
1043      * of its descendants, you may implement it as follows:
1044      * <pre>
1045      *     viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() {
1046      *         public boolean onTouch(View v, MotionEvent event) {
1047      *             if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
1048      *                 mItemTouchHelper.startDrag(viewHolder);
1049      *             }
1050      *             return false;
1051      *         }
1052      *     });
1053      * </pre>
1054      * <p>
1055      *
1056      * @param viewHolder The ViewHolder to start dragging. It must be a direct child of
1057      *                   RecyclerView.
1058      * @see ItemTouchHelper.Callback#isItemViewSwipeEnabled()
1059      */
startDrag(ViewHolder viewHolder)1060     public void startDrag(ViewHolder viewHolder) {
1061         if (!mCallback.hasDragFlag(mRecyclerView, viewHolder)) {
1062             Log.e(TAG, "Start drag has been called but swiping is not enabled");
1063             return;
1064         }
1065         if (viewHolder.itemView.getParent() != mRecyclerView) {
1066             Log.e(TAG, "Start drag has been called with a view holder which is not a child of "
1067                     + "the RecyclerView which is controlled by this ItemTouchHelper.");
1068             return;
1069         }
1070         obtainVelocityTracker();
1071         mDx = mDy = 0f;
1072         select(viewHolder, ACTION_STATE_DRAG);
1073     }
1074 
1075     /**
1076      * Starts swiping the provided ViewHolder. By default, ItemTouchHelper starts swiping a View
1077      * when user swipes their finger (or mouse pointer) over the View. You can disable this
1078      * behavior
1079      * by overriding {@link ItemTouchHelper.Callback}
1080      * <p>
1081      * For this method to work:
1082      * <ul>
1083      * <li>The provided ViewHolder must be a child of the RecyclerView to which this
1084      * ItemTouchHelper is attached.</li>
1085      * <li>{@link ItemTouchHelper.Callback} must have swiping enabled.</li>
1086      * <li>There must be a previous touch event that was reported to the ItemTouchHelper
1087      * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener
1088      * grabs previous events, this should work as expected.</li>
1089      * </ul>
1090      *
1091      * For example, if you would like to let your user to be able to swipe an Item by touching one
1092      * of its descendants, you may implement it as follows:
1093      * <pre>
1094      *     viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() {
1095      *         public boolean onTouch(View v, MotionEvent event) {
1096      *             if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
1097      *                 mItemTouchHelper.startSwipe(viewHolder);
1098      *             }
1099      *             return false;
1100      *         }
1101      *     });
1102      * </pre>
1103      *
1104      * @param viewHolder The ViewHolder to start swiping. It must be a direct child of
1105      *                   RecyclerView.
1106      */
startSwipe(ViewHolder viewHolder)1107     public void startSwipe(ViewHolder viewHolder) {
1108         if (!mCallback.hasSwipeFlag(mRecyclerView, viewHolder)) {
1109             Log.e(TAG, "Start swipe has been called but dragging is not enabled");
1110             return;
1111         }
1112         if (viewHolder.itemView.getParent() != mRecyclerView) {
1113             Log.e(TAG, "Start swipe has been called with a view holder which is not a child of "
1114                     + "the RecyclerView controlled by this ItemTouchHelper.");
1115             return;
1116         }
1117         obtainVelocityTracker();
1118         mDx = mDy = 0f;
1119         select(viewHolder, ACTION_STATE_SWIPE);
1120     }
1121 
findAnimation(MotionEvent event)1122     private RecoverAnimation findAnimation(MotionEvent event) {
1123         if (mRecoverAnimations.isEmpty()) {
1124             return null;
1125         }
1126         View target = findChildView(event);
1127         for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) {
1128             final RecoverAnimation anim = mRecoverAnimations.get(i);
1129             if (anim.mViewHolder.itemView == target) {
1130                 return anim;
1131             }
1132         }
1133         return null;
1134     }
1135 
updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex)1136     private void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) {
1137         final float x = ev.getX(pointerIndex);
1138         final float y = ev.getY(pointerIndex);
1139 
1140         // Calculate the distance moved
1141         mDx = x - mInitialTouchX;
1142         mDy = y - mInitialTouchY;
1143         if ((directionFlags & LEFT) == 0) {
1144             mDx = Math.max(0, mDx);
1145         }
1146         if ((directionFlags & RIGHT) == 0) {
1147             mDx = Math.min(0, mDx);
1148         }
1149         if ((directionFlags & UP) == 0) {
1150             mDy = Math.max(0, mDy);
1151         }
1152         if ((directionFlags & DOWN) == 0) {
1153             mDy = Math.min(0, mDy);
1154         }
1155     }
1156 
swipeIfNecessary(ViewHolder viewHolder)1157     private int swipeIfNecessary(ViewHolder viewHolder) {
1158         if (mActionState == ACTION_STATE_DRAG) {
1159             return 0;
1160         }
1161         final int originalMovementFlags = mCallback.getMovementFlags(mRecyclerView, viewHolder);
1162         final int absoluteMovementFlags = mCallback.convertToAbsoluteDirection(
1163                 originalMovementFlags,
1164                 ViewCompat.getLayoutDirection(mRecyclerView));
1165         final int flags = (absoluteMovementFlags
1166                 & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT);
1167         if (flags == 0) {
1168             return 0;
1169         }
1170         final int originalFlags = (originalMovementFlags
1171                 & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT);
1172         int swipeDir;
1173         if (Math.abs(mDx) > Math.abs(mDy)) {
1174             if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) {
1175                 // if swipe dir is not in original flags, it should be the relative direction
1176                 if ((originalFlags & swipeDir) == 0) {
1177                     // convert to relative
1178                     return Callback.convertToRelativeDirection(swipeDir,
1179                             ViewCompat.getLayoutDirection(mRecyclerView));
1180                 }
1181                 return swipeDir;
1182             }
1183             if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) {
1184                 return swipeDir;
1185             }
1186         } else {
1187             if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) {
1188                 return swipeDir;
1189             }
1190             if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) {
1191                 // if swipe dir is not in original flags, it should be the relative direction
1192                 if ((originalFlags & swipeDir) == 0) {
1193                     // convert to relative
1194                     return Callback.convertToRelativeDirection(swipeDir,
1195                             ViewCompat.getLayoutDirection(mRecyclerView));
1196                 }
1197                 return swipeDir;
1198             }
1199         }
1200         return 0;
1201     }
1202 
checkHorizontalSwipe(ViewHolder viewHolder, int flags)1203     private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) {
1204         if ((flags & (LEFT | RIGHT)) != 0) {
1205             final int dirFlag = mDx > 0 ? RIGHT : LEFT;
1206             if (mVelocityTracker != null && mActivePointerId > -1) {
1207                 mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND,
1208                         mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity));
1209                 final float xVelocity = VelocityTrackerCompat
1210                         .getXVelocity(mVelocityTracker, mActivePointerId);
1211                 final float yVelocity = VelocityTrackerCompat
1212                         .getYVelocity(mVelocityTracker, mActivePointerId);
1213                 final int velDirFlag = xVelocity > 0f ? RIGHT : LEFT;
1214                 final float absXVelocity = Math.abs(xVelocity);
1215                 if ((velDirFlag & flags) != 0 && dirFlag == velDirFlag &&
1216                         absXVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) &&
1217                         absXVelocity > Math.abs(yVelocity)) {
1218                     return velDirFlag;
1219                 }
1220             }
1221 
1222             final float threshold = mRecyclerView.getWidth() * mCallback
1223                     .getSwipeThreshold(viewHolder);
1224 
1225             if ((flags & dirFlag) != 0 && Math.abs(mDx) > threshold) {
1226                 return dirFlag;
1227             }
1228         }
1229         return 0;
1230     }
1231 
checkVerticalSwipe(ViewHolder viewHolder, int flags)1232     private int checkVerticalSwipe(ViewHolder viewHolder, int flags) {
1233         if ((flags & (UP | DOWN)) != 0) {
1234             final int dirFlag = mDy > 0 ? DOWN : UP;
1235             if (mVelocityTracker != null && mActivePointerId > -1) {
1236                 mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND,
1237                         mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity));
1238                 final float xVelocity = VelocityTrackerCompat
1239                         .getXVelocity(mVelocityTracker, mActivePointerId);
1240                 final float yVelocity = VelocityTrackerCompat
1241                         .getYVelocity(mVelocityTracker, mActivePointerId);
1242                 final int velDirFlag = yVelocity > 0f ? DOWN : UP;
1243                 final float absYVelocity = Math.abs(yVelocity);
1244                 if ((velDirFlag & flags) != 0 && velDirFlag == dirFlag &&
1245                         absYVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) &&
1246                         absYVelocity > Math.abs(xVelocity)) {
1247                     return velDirFlag;
1248                 }
1249             }
1250 
1251             final float threshold = mRecyclerView.getHeight() * mCallback
1252                     .getSwipeThreshold(viewHolder);
1253             if ((flags & dirFlag) != 0 && Math.abs(mDy) > threshold) {
1254                 return dirFlag;
1255             }
1256         }
1257         return 0;
1258     }
1259 
addChildDrawingOrderCallback()1260     private void addChildDrawingOrderCallback() {
1261         if (Build.VERSION.SDK_INT >= 21) {
1262             return;// we use elevation on Lollipop
1263         }
1264         if (mChildDrawingOrderCallback == null) {
1265             mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() {
1266                 @Override
1267                 public int onGetChildDrawingOrder(int childCount, int i) {
1268                     if (mOverdrawChild == null) {
1269                         return i;
1270                     }
1271                     int childPosition = mOverdrawChildPosition;
1272                     if (childPosition == -1) {
1273                         childPosition = mRecyclerView.indexOfChild(mOverdrawChild);
1274                         mOverdrawChildPosition = childPosition;
1275                     }
1276                     if (i == childCount - 1) {
1277                         return childPosition;
1278                     }
1279                     return i < childPosition ? i : i + 1;
1280                 }
1281             };
1282         }
1283         mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback);
1284     }
1285 
removeChildDrawingOrderCallbackIfNecessary(View view)1286     private void removeChildDrawingOrderCallbackIfNecessary(View view) {
1287         if (view == mOverdrawChild) {
1288             mOverdrawChild = null;
1289             // only remove if we've added
1290             if (mChildDrawingOrderCallback != null) {
1291                 mRecyclerView.setChildDrawingOrderCallback(null);
1292             }
1293         }
1294     }
1295 
1296     /**
1297      * An interface which can be implemented by LayoutManager for better integration with
1298      * {@link ItemTouchHelper}.
1299      */
1300     public static interface ViewDropHandler {
1301 
1302         /**
1303          * Called by the {@link ItemTouchHelper} after a View is dropped over another View.
1304          * <p>
1305          * A LayoutManager should implement this interface to get ready for the upcoming move
1306          * operation.
1307          * <p>
1308          * For example, LinearLayoutManager sets up a "scrollToPositionWithOffset" calls so that
1309          * the View under drag will be used as an anchor View while calculating the next layout,
1310          * making layout stay consistent.
1311          *
1312          * @param view   The View which is being dragged. It is very likely that user is still
1313          *               dragging this View so there might be other
1314          *               {@link #prepareForDrop(View, View, int, int)} after this one.
1315          * @param target The target view which is being dropped on.
1316          * @param x      The <code>left</code> offset of the View that is being dragged. This value
1317          *               includes the movement caused by the user.
1318          * @param y      The <code>top</code> offset of the View that is being dragged. This value
1319          *               includes the movement caused by the user.
1320          */
prepareForDrop(View view, View target, int x, int y)1321         public void prepareForDrop(View view, View target, int x, int y);
1322     }
1323 
1324     /**
1325      * This class is the contract between ItemTouchHelper and your application. It lets you control
1326      * which touch behaviors are enabled per each ViewHolder and also receive callbacks when user
1327      * performs these actions.
1328      * <p>
1329      * To control which actions user can take on each view, you should override
1330      * {@link #getMovementFlags(RecyclerView, ViewHolder)} and return appropriate set
1331      * of direction flags. ({@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link #END},
1332      * {@link #UP}, {@link #DOWN}). You can use
1333      * {@link #makeMovementFlags(int, int)} to easily construct it. Alternatively, you can use
1334      * {@link SimpleCallback}.
1335      * <p>
1336      * If user drags an item, ItemTouchHelper will call
1337      * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder)
1338      * onMove(recyclerView, dragged, target)}.
1339      * Upon receiving this callback, you should move the item from the old position
1340      * ({@code dragged.getAdapterPosition()}) to new position ({@code target.getAdapterPosition()})
1341      * in your adapter and also call {@link RecyclerView.Adapter#notifyItemMoved(int, int)}.
1342      * To control where a View can be dropped, you can override
1343      * {@link #canDropOver(RecyclerView, ViewHolder, ViewHolder)}. When a
1344      * dragging View overlaps multiple other views, Callback chooses the closest View with which
1345      * dragged View might have changed positions. Although this approach works for many use cases,
1346      * if you have a custom LayoutManager, you can override
1347      * {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)} to select a
1348      * custom drop target.
1349      * <p>
1350      * When a View is swiped, ItemTouchHelper animates it until it goes out of bounds, then calls
1351      * {@link #onSwiped(ViewHolder, int)}. At this point, you should update your
1352      * adapter (e.g. remove the item) and call related Adapter#notify event.
1353      */
1354     @SuppressWarnings("UnusedParameters")
1355     public abstract static class Callback {
1356 
1357         public static final int DEFAULT_DRAG_ANIMATION_DURATION = 200;
1358 
1359         public static final int DEFAULT_SWIPE_ANIMATION_DURATION = 250;
1360 
1361         static final int RELATIVE_DIR_FLAGS = START | END |
1362                 ((START | END) << DIRECTION_FLAG_COUNT) |
1363                 ((START | END) << (2 * DIRECTION_FLAG_COUNT));
1364 
1365         private static final ItemTouchUIUtil sUICallback;
1366 
1367         private static final int ABS_HORIZONTAL_DIR_FLAGS = LEFT | RIGHT |
1368                 ((LEFT | RIGHT) << DIRECTION_FLAG_COUNT) |
1369                 ((LEFT | RIGHT) << (2 * DIRECTION_FLAG_COUNT));
1370 
1371         private static final Interpolator sDragScrollInterpolator = new Interpolator() {
1372             @Override
1373             public float getInterpolation(float t) {
1374                 return t * t * t * t * t;
1375             }
1376         };
1377 
1378         private static final Interpolator sDragViewScrollCapInterpolator = new Interpolator() {
1379             @Override
1380             public float getInterpolation(float t) {
1381                 t -= 1.0f;
1382                 return t * t * t * t * t + 1.0f;
1383             }
1384         };
1385 
1386         /**
1387          * Drag scroll speed keeps accelerating until this many milliseconds before being capped.
1388          */
1389         private static final long DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;
1390 
1391         private int mCachedMaxScrollSpeed = -1;
1392 
1393         static {
1394             if (Build.VERSION.SDK_INT >= 21) {
1395                 sUICallback = new ItemTouchUIUtilImpl.Lollipop();
1396             } else if (Build.VERSION.SDK_INT >= 11) {
1397                 sUICallback = new ItemTouchUIUtilImpl.Honeycomb();
1398             } else {
1399                 sUICallback = new ItemTouchUIUtilImpl.Gingerbread();
1400             }
1401         }
1402 
1403         /**
1404          * Returns the {@link ItemTouchUIUtil} that is used by the {@link Callback} class for
1405          * visual
1406          * changes on Views in response to user interactions. {@link ItemTouchUIUtil} has different
1407          * implementations for different platform versions.
1408          * <p>
1409          * By default, {@link Callback} applies these changes on
1410          * {@link RecyclerView.ViewHolder#itemView}.
1411          * <p>
1412          * For example, if you have a use case where you only want the text to move when user
1413          * swipes over the view, you can do the following:
1414          * <pre>
1415          *     public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder){
1416          *         getDefaultUIUtil().clearView(((ItemTouchViewHolder) viewHolder).textView);
1417          *     }
1418          *     public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
1419          *         if (viewHolder != null){
1420          *             getDefaultUIUtil().onSelected(((ItemTouchViewHolder) viewHolder).textView);
1421          *         }
1422          *     }
1423          *     public void onChildDraw(Canvas c, RecyclerView recyclerView,
1424          *             RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState,
1425          *             boolean isCurrentlyActive) {
1426          *         getDefaultUIUtil().onDraw(c, recyclerView,
1427          *                 ((ItemTouchViewHolder) viewHolder).textView, dX, dY,
1428          *                 actionState, isCurrentlyActive);
1429          *         return true;
1430          *     }
1431          *     public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
1432          *             RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState,
1433          *             boolean isCurrentlyActive) {
1434          *         getDefaultUIUtil().onDrawOver(c, recyclerView,
1435          *                 ((ItemTouchViewHolder) viewHolder).textView, dX, dY,
1436          *                 actionState, isCurrentlyActive);
1437          *         return true;
1438          *     }
1439          * </pre>
1440          *
1441          * @return The {@link ItemTouchUIUtil} instance that is used by the {@link Callback}
1442          */
getDefaultUIUtil()1443         public static ItemTouchUIUtil getDefaultUIUtil() {
1444             return sUICallback;
1445         }
1446 
1447         /**
1448          * Replaces a movement direction with its relative version by taking layout direction into
1449          * account.
1450          *
1451          * @param flags           The flag value that include any number of movement flags.
1452          * @param layoutDirection The layout direction of the View. Can be obtained from
1453          *                        {@link ViewCompat#getLayoutDirection(android.view.View)}.
1454          * @return Updated flags which uses relative flags ({@link #START}, {@link #END}) instead
1455          * of {@link #LEFT}, {@link #RIGHT}.
1456          * @see #convertToAbsoluteDirection(int, int)
1457          */
convertToRelativeDirection(int flags, int layoutDirection)1458         public static int convertToRelativeDirection(int flags, int layoutDirection) {
1459             int masked = flags & ABS_HORIZONTAL_DIR_FLAGS;
1460             if (masked == 0) {
1461                 return flags;// does not have any abs flags, good.
1462             }
1463             flags &= ~masked; //remove left / right.
1464             if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) {
1465                 // no change. just OR with 2 bits shifted mask and return
1466                 flags |= masked << 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT.
1467                 return flags;
1468             } else {
1469                 // add RIGHT flag as START
1470                 flags |= ((masked << 1) & ~ABS_HORIZONTAL_DIR_FLAGS);
1471                 // first clean RIGHT bit then add LEFT flag as END
1472                 flags |= ((masked << 1) & ABS_HORIZONTAL_DIR_FLAGS) << 2;
1473             }
1474             return flags;
1475         }
1476 
1477         /**
1478          * Convenience method to create movement flags.
1479          * <p>
1480          * For instance, if you want to let your items be drag & dropped vertically and swiped
1481          * left to be dismissed, you can call this method with:
1482          * <code>makeMovementFlags(UP | DOWN, LEFT);</code>
1483          *
1484          * @param dragFlags  The directions in which the item can be dragged.
1485          * @param swipeFlags The directions in which the item can be swiped.
1486          * @return Returns an integer composed of the given drag and swipe flags.
1487          */
makeMovementFlags(int dragFlags, int swipeFlags)1488         public static int makeMovementFlags(int dragFlags, int swipeFlags) {
1489             return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags) |
1490                     makeFlag(ACTION_STATE_SWIPE, swipeFlags) | makeFlag(ACTION_STATE_DRAG,
1491                     dragFlags);
1492         }
1493 
1494         /**
1495          * Shifts the given direction flags to the offset of the given action state.
1496          *
1497          * @param actionState The action state you want to get flags in. Should be one of
1498          *                    {@link #ACTION_STATE_IDLE}, {@link #ACTION_STATE_SWIPE} or
1499          *                    {@link #ACTION_STATE_DRAG}.
1500          * @param directions  The direction flags. Can be composed from {@link #UP}, {@link #DOWN},
1501          *                    {@link #RIGHT}, {@link #LEFT} {@link #START} and {@link #END}.
1502          * @return And integer that represents the given directions in the provided actionState.
1503          */
makeFlag(int actionState, int directions)1504         public static int makeFlag(int actionState, int directions) {
1505             return directions << (actionState * DIRECTION_FLAG_COUNT);
1506         }
1507 
1508         /**
1509          * Should return a composite flag which defines the enabled move directions in each state
1510          * (idle, swiping, dragging).
1511          * <p>
1512          * Instead of composing this flag manually, you can use {@link #makeMovementFlags(int,
1513          * int)}
1514          * or {@link #makeFlag(int, int)}.
1515          * <p>
1516          * This flag is composed of 3 sets of 8 bits, where first 8 bits are for IDLE state, next
1517          * 8 bits are for SWIPE state and third 8 bits are for DRAG state.
1518          * Each 8 bit sections can be constructed by simply OR'ing direction flags defined in
1519          * {@link ItemTouchHelper}.
1520          * <p>
1521          * For example, if you want it to allow swiping LEFT and RIGHT but only allow starting to
1522          * swipe by swiping RIGHT, you can return:
1523          * <pre>
1524          *      makeFlag(ACTION_STATE_IDLE, RIGHT) | makeFlag(ACTION_STATE_SWIPE, LEFT | RIGHT);
1525          * </pre>
1526          * This means, allow right movement while IDLE and allow right and left movement while
1527          * swiping.
1528          *
1529          * @param recyclerView The RecyclerView to which ItemTouchHelper is attached.
1530          * @param viewHolder   The ViewHolder for which the movement information is necessary.
1531          * @return flags specifying which movements are allowed on this ViewHolder.
1532          * @see #makeMovementFlags(int, int)
1533          * @see #makeFlag(int, int)
1534          */
getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder)1535         public abstract int getMovementFlags(RecyclerView recyclerView,
1536                 ViewHolder viewHolder);
1537 
1538         /**
1539          * Converts a given set of flags to absolution direction which means {@link #START} and
1540          * {@link #END} are replaced with {@link #LEFT} and {@link #RIGHT} depending on the layout
1541          * direction.
1542          *
1543          * @param flags           The flag value that include any number of movement flags.
1544          * @param layoutDirection The layout direction of the RecyclerView.
1545          * @return Updated flags which includes only absolute direction values.
1546          */
convertToAbsoluteDirection(int flags, int layoutDirection)1547         public int convertToAbsoluteDirection(int flags, int layoutDirection) {
1548             int masked = flags & RELATIVE_DIR_FLAGS;
1549             if (masked == 0) {
1550                 return flags;// does not have any relative flags, good.
1551             }
1552             flags &= ~masked; //remove start / end
1553             if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) {
1554                 // no change. just OR with 2 bits shifted mask and return
1555                 flags |= masked >> 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT.
1556                 return flags;
1557             } else {
1558                 // add START flag as RIGHT
1559                 flags |= ((masked >> 1) & ~RELATIVE_DIR_FLAGS);
1560                 // first clean start bit then add END flag as LEFT
1561                 flags |= ((masked >> 1) & RELATIVE_DIR_FLAGS) >> 2;
1562             }
1563             return flags;
1564         }
1565 
getAbsoluteMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder)1566         final int getAbsoluteMovementFlags(RecyclerView recyclerView,
1567                 ViewHolder viewHolder) {
1568             final int flags = getMovementFlags(recyclerView, viewHolder);
1569             return convertToAbsoluteDirection(flags, ViewCompat.getLayoutDirection(recyclerView));
1570         }
1571 
hasDragFlag(RecyclerView recyclerView, ViewHolder viewHolder)1572         private boolean hasDragFlag(RecyclerView recyclerView, ViewHolder viewHolder) {
1573             final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder);
1574             return (flags & ACTION_MODE_DRAG_MASK) != 0;
1575         }
1576 
hasSwipeFlag(RecyclerView recyclerView, ViewHolder viewHolder)1577         private boolean hasSwipeFlag(RecyclerView recyclerView,
1578                 ViewHolder viewHolder) {
1579             final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder);
1580             return (flags & ACTION_MODE_SWIPE_MASK) != 0;
1581         }
1582 
1583         /**
1584          * Return true if the current ViewHolder can be dropped over the the target ViewHolder.
1585          * <p>
1586          * This method is used when selecting drop target for the dragged View. After Views are
1587          * eliminated either via bounds check or via this method, resulting set of views will be
1588          * passed to {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)}.
1589          * <p>
1590          * Default implementation returns true.
1591          *
1592          * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to.
1593          * @param current      The ViewHolder that user is dragging.
1594          * @param target       The ViewHolder which is below the dragged ViewHolder.
1595          * @return True if the dragged ViewHolder can be replaced with the target ViewHolder, false
1596          * otherwise.
1597          */
canDropOver(RecyclerView recyclerView, ViewHolder current, ViewHolder target)1598         public boolean canDropOver(RecyclerView recyclerView, ViewHolder current,
1599                 ViewHolder target) {
1600             return true;
1601         }
1602 
1603         /**
1604          * Called when ItemTouchHelper wants to move the dragged item from its old position to
1605          * the new position.
1606          * <p>
1607          * If this method returns true, ItemTouchHelper assumes {@code viewHolder} has been moved
1608          * to the adapter position of {@code target} ViewHolder
1609          * ({@link ViewHolder#getAdapterPosition()
1610          * ViewHolder#getAdapterPosition()}).
1611          * <p>
1612          * If you don't support drag & drop, this method will never be called.
1613          *
1614          * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to.
1615          * @param viewHolder   The ViewHolder which is being dragged by the user.
1616          * @param target       The ViewHolder over which the currently active item is being
1617          *                     dragged.
1618          * @return True if the {@code viewHolder} has been moved to the adapter position of
1619          * {@code target}.
1620          * @see #onMoved(RecyclerView, ViewHolder, int, ViewHolder, int, int, int)
1621          */
onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target)1622         public abstract boolean onMove(RecyclerView recyclerView,
1623                 ViewHolder viewHolder, ViewHolder target);
1624 
1625         /**
1626          * Returns whether ItemTouchHelper should start a drag and drop operation if an item is
1627          * long pressed.
1628          * <p>
1629          * Default value returns true but you may want to disable this if you want to start
1630          * dragging on a custom view touch using {@link #startDrag(ViewHolder)}.
1631          *
1632          * @return True if ItemTouchHelper should start dragging an item when it is long pressed,
1633          * false otherwise. Default value is <code>true</code>.
1634          * @see #startDrag(ViewHolder)
1635          */
isLongPressDragEnabled()1636         public boolean isLongPressDragEnabled() {
1637             return true;
1638         }
1639 
1640         /**
1641          * Returns whether ItemTouchHelper should start a swipe operation if a pointer is swiped
1642          * over the View.
1643          * <p>
1644          * Default value returns true but you may want to disable this if you want to start
1645          * swiping on a custom view touch using {@link #startSwipe(ViewHolder)}.
1646          *
1647          * @return True if ItemTouchHelper should start swiping an item when user swipes a pointer
1648          * over the View, false otherwise. Default value is <code>true</code>.
1649          * @see #startSwipe(ViewHolder)
1650          */
isItemViewSwipeEnabled()1651         public boolean isItemViewSwipeEnabled() {
1652             return true;
1653         }
1654 
1655         /**
1656          * When finding views under a dragged view, by default, ItemTouchHelper searches for views
1657          * that overlap with the dragged View. By overriding this method, you can extend or shrink
1658          * the search box.
1659          *
1660          * @return The extra margin to be added to the hit box of the dragged View.
1661          */
getBoundingBoxMargin()1662         public int getBoundingBoxMargin() {
1663             return 0;
1664         }
1665 
1666         /**
1667          * Returns the fraction that the user should move the View to be considered as swiped.
1668          * The fraction is calculated with respect to RecyclerView's bounds.
1669          * <p>
1670          * Default value is .5f, which means, to swipe a View, user must move the View at least
1671          * half of RecyclerView's width or height, depending on the swipe direction.
1672          *
1673          * @param viewHolder The ViewHolder that is being dragged.
1674          * @return A float value that denotes the fraction of the View size. Default value
1675          * is .5f .
1676          */
getSwipeThreshold(ViewHolder viewHolder)1677         public float getSwipeThreshold(ViewHolder viewHolder) {
1678             return .5f;
1679         }
1680 
1681         /**
1682          * Returns the fraction that the user should move the View to be considered as it is
1683          * dragged. After a view is moved this amount, ItemTouchHelper starts checking for Views
1684          * below it for a possible drop.
1685          *
1686          * @param viewHolder The ViewHolder that is being dragged.
1687          * @return A float value that denotes the fraction of the View size. Default value is
1688          * .5f .
1689          */
getMoveThreshold(ViewHolder viewHolder)1690         public float getMoveThreshold(ViewHolder viewHolder) {
1691             return .5f;
1692         }
1693 
1694         /**
1695          * Defines the minimum velocity which will be considered as a swipe action by the user.
1696          * <p>
1697          * You can increase this value to make it harder to swipe or decrease it to make it easier.
1698          * Keep in mind that ItemTouchHelper also checks the perpendicular velocity and makes sure
1699          * current direction velocity is larger then the perpendicular one. Otherwise, user's
1700          * movement is ambiguous. You can change the threshold by overriding
1701          * {@link #getSwipeVelocityThreshold(float)}.
1702          * <p>
1703          * The velocity is calculated in pixels per second.
1704          * <p>
1705          * The default framework value is passed as a parameter so that you can modify it with a
1706          * multiplier.
1707          *
1708          * @param defaultValue The default value (in pixels per second) used by the
1709          *                     ItemTouchHelper.
1710          * @return The minimum swipe velocity. The default implementation returns the
1711          * <code>defaultValue</code> parameter.
1712          * @see #getSwipeVelocityThreshold(float)
1713          * @see #getSwipeThreshold(ViewHolder)
1714          */
getSwipeEscapeVelocity(float defaultValue)1715         public float getSwipeEscapeVelocity(float defaultValue) {
1716             return defaultValue;
1717         }
1718 
1719         /**
1720          * Defines the maximum velocity ItemTouchHelper will ever calculate for pointer movements.
1721          * <p>
1722          * To consider a movement as swipe, ItemTouchHelper requires it to be larger than the
1723          * perpendicular movement. If both directions reach to the max threshold, none of them will
1724          * be considered as a swipe because it is usually an indication that user rather tried to
1725          * scroll then swipe.
1726          * <p>
1727          * The velocity is calculated in pixels per second.
1728          * <p>
1729          * You can customize this behavior by changing this method. If you increase the value, it
1730          * will be easier for the user to swipe diagonally and if you decrease the value, user will
1731          * need to make a rather straight finger movement to trigger a swipe.
1732          *
1733          * @param defaultValue The default value(in pixels per second) used by the ItemTouchHelper.
1734          * @return The velocity cap for pointer movements. The default implementation returns the
1735          * <code>defaultValue</code> parameter.
1736          * @see #getSwipeEscapeVelocity(float)
1737          */
getSwipeVelocityThreshold(float defaultValue)1738         public float getSwipeVelocityThreshold(float defaultValue) {
1739             return defaultValue;
1740         }
1741 
1742         /**
1743          * Called by ItemTouchHelper to select a drop target from the list of ViewHolders that
1744          * are under the dragged View.
1745          * <p>
1746          * Default implementation filters the View with which dragged item have changed position
1747          * in the drag direction. For instance, if the view is dragged UP, it compares the
1748          * <code>view.getTop()</code> of the two views before and after drag started. If that value
1749          * is different, the target view passes the filter.
1750          * <p>
1751          * Among these Views which pass the test, the one closest to the dragged view is chosen.
1752          * <p>
1753          * This method is called on the main thread every time user moves the View. If you want to
1754          * override it, make sure it does not do any expensive operations.
1755          *
1756          * @param selected    The ViewHolder being dragged by the user.
1757          * @param dropTargets The list of ViewHolder that are under the dragged View and
1758          *                    candidate as a drop.
1759          * @param curX        The updated left value of the dragged View after drag translations
1760          *                    are applied. This value does not include margins added by
1761          *                    {@link RecyclerView.ItemDecoration}s.
1762          * @param curY        The updated top value of the dragged View after drag translations
1763          *                    are applied. This value does not include margins added by
1764          *                    {@link RecyclerView.ItemDecoration}s.
1765          * @return A ViewHolder to whose position the dragged ViewHolder should be
1766          * moved to.
1767          */
chooseDropTarget(ViewHolder selected, List<ViewHolder> dropTargets, int curX, int curY)1768         public ViewHolder chooseDropTarget(ViewHolder selected,
1769                 List<ViewHolder> dropTargets, int curX, int curY) {
1770             int right = curX + selected.itemView.getWidth();
1771             int bottom = curY + selected.itemView.getHeight();
1772             ViewHolder winner = null;
1773             int winnerScore = -1;
1774             final int dx = curX - selected.itemView.getLeft();
1775             final int dy = curY - selected.itemView.getTop();
1776             final int targetsSize = dropTargets.size();
1777             for (int i = 0; i < targetsSize; i++) {
1778                 final ViewHolder target = dropTargets.get(i);
1779                 if (dx > 0) {
1780                     int diff = target.itemView.getRight() - right;
1781                     if (diff < 0 && target.itemView.getRight() > selected.itemView.getRight()) {
1782                         final int score = Math.abs(diff);
1783                         if (score > winnerScore) {
1784                             winnerScore = score;
1785                             winner = target;
1786                         }
1787                     }
1788                 }
1789                 if (dx < 0) {
1790                     int diff = target.itemView.getLeft() - curX;
1791                     if (diff > 0 && target.itemView.getLeft() < selected.itemView.getLeft()) {
1792                         final int score = Math.abs(diff);
1793                         if (score > winnerScore) {
1794                             winnerScore = score;
1795                             winner = target;
1796                         }
1797                     }
1798                 }
1799                 if (dy < 0) {
1800                     int diff = target.itemView.getTop() - curY;
1801                     if (diff > 0 && target.itemView.getTop() < selected.itemView.getTop()) {
1802                         final int score = Math.abs(diff);
1803                         if (score > winnerScore) {
1804                             winnerScore = score;
1805                             winner = target;
1806                         }
1807                     }
1808                 }
1809 
1810                 if (dy > 0) {
1811                     int diff = target.itemView.getBottom() - bottom;
1812                     if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) {
1813                         final int score = Math.abs(diff);
1814                         if (score > winnerScore) {
1815                             winnerScore = score;
1816                             winner = target;
1817                         }
1818                     }
1819                 }
1820             }
1821             return winner;
1822         }
1823 
1824         /**
1825          * Called when a ViewHolder is swiped by the user.
1826          * <p>
1827          * If you are returning relative directions ({@link #START} , {@link #END}) from the
1828          * {@link #getMovementFlags(RecyclerView, ViewHolder)} method, this method
1829          * will also use relative directions. Otherwise, it will use absolute directions.
1830          * <p>
1831          * If you don't support swiping, this method will never be called.
1832          * <p>
1833          * ItemTouchHelper will keep a reference to the View until it is detached from
1834          * RecyclerView.
1835          * As soon as it is detached, ItemTouchHelper will call
1836          * {@link #clearView(RecyclerView, ViewHolder)}.
1837          *
1838          * @param viewHolder The ViewHolder which has been swiped by the user.
1839          * @param direction  The direction to which the ViewHolder is swiped. It is one of
1840          *                   {@link #UP}, {@link #DOWN},
1841          *                   {@link #LEFT} or {@link #RIGHT}. If your
1842          *                   {@link #getMovementFlags(RecyclerView, ViewHolder)}
1843          *                   method
1844          *                   returned relative flags instead of {@link #LEFT} / {@link #RIGHT};
1845          *                   `direction` will be relative as well. ({@link #START} or {@link
1846          *                   #END}).
1847          */
onSwiped(ViewHolder viewHolder, int direction)1848         public abstract void onSwiped(ViewHolder viewHolder, int direction);
1849 
1850         /**
1851          * Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed.
1852          * <p/>
1853          * If you override this method, you should call super.
1854          *
1855          * @param viewHolder  The new ViewHolder that is being swiped or dragged. Might be null if
1856          *                    it is cleared.
1857          * @param actionState One of {@link ItemTouchHelper#ACTION_STATE_IDLE},
1858          *                    {@link ItemTouchHelper#ACTION_STATE_SWIPE} or
1859          *                    {@link ItemTouchHelper#ACTION_STATE_DRAG}.
1860          * @see #clearView(RecyclerView, RecyclerView.ViewHolder)
1861          */
onSelectedChanged(ViewHolder viewHolder, int actionState)1862         public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
1863             if (viewHolder != null) {
1864                 sUICallback.onSelected(viewHolder.itemView);
1865             }
1866         }
1867 
getMaxDragScroll(RecyclerView recyclerView)1868         private int getMaxDragScroll(RecyclerView recyclerView) {
1869             if (mCachedMaxScrollSpeed == -1) {
1870                 mCachedMaxScrollSpeed = recyclerView.getResources().getDimensionPixelSize(
1871                         R.dimen.item_touch_helper_max_drag_scroll_per_frame);
1872             }
1873             return mCachedMaxScrollSpeed;
1874         }
1875 
1876         /**
1877          * Called when {@link #onMove(RecyclerView, ViewHolder, ViewHolder)} returns true.
1878          * <p>
1879          * ItemTouchHelper does not create an extra Bitmap or View while dragging, instead, it
1880          * modifies the existing View. Because of this reason, it is important that the View is
1881          * still part of the layout after it is moved. This may not work as intended when swapped
1882          * Views are close to RecyclerView bounds or there are gaps between them (e.g. other Views
1883          * which were not eligible for dropping over).
1884          * <p>
1885          * This method is responsible to give necessary hint to the LayoutManager so that it will
1886          * keep the View in visible area. For example, for LinearLayoutManager, this is as simple
1887          * as calling {@link LinearLayoutManager#scrollToPositionWithOffset(int, int)}.
1888          *
1889          * Default implementation calls {@link RecyclerView#scrollToPosition(int)} if the View's
1890          * new position is likely to be out of bounds.
1891          * <p>
1892          * It is important to ensure the ViewHolder will stay visible as otherwise, it might be
1893          * removed by the LayoutManager if the move causes the View to go out of bounds. In that
1894          * case, drag will end prematurely.
1895          *
1896          * @param recyclerView The RecyclerView controlled by the ItemTouchHelper.
1897          * @param viewHolder   The ViewHolder under user's control.
1898          * @param fromPos      The previous adapter position of the dragged item (before it was
1899          *                     moved).
1900          * @param target       The ViewHolder on which the currently active item has been dropped.
1901          * @param toPos        The new adapter position of the dragged item.
1902          * @param x            The updated left value of the dragged View after drag translations
1903          *                     are applied. This value does not include margins added by
1904          *                     {@link RecyclerView.ItemDecoration}s.
1905          * @param y            The updated top value of the dragged View after drag translations
1906          *                     are applied. This value does not include margins added by
1907          *                     {@link RecyclerView.ItemDecoration}s.
1908          */
onMoved(final RecyclerView recyclerView, final ViewHolder viewHolder, int fromPos, final ViewHolder target, int toPos, int x, int y)1909         public void onMoved(final RecyclerView recyclerView,
1910                 final ViewHolder viewHolder, int fromPos, final ViewHolder target, int toPos, int x,
1911                 int y) {
1912             final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
1913             if (layoutManager instanceof ViewDropHandler) {
1914                 ((ViewDropHandler) layoutManager).prepareForDrop(viewHolder.itemView,
1915                         target.itemView, x, y);
1916                 return;
1917             }
1918 
1919             // if layout manager cannot handle it, do some guesswork
1920             if (layoutManager.canScrollHorizontally()) {
1921                 final int minLeft = layoutManager.getDecoratedLeft(target.itemView);
1922                 if (minLeft <= recyclerView.getPaddingLeft()) {
1923                     recyclerView.scrollToPosition(toPos);
1924                 }
1925                 final int maxRight = layoutManager.getDecoratedRight(target.itemView);
1926                 if (maxRight >= recyclerView.getWidth() - recyclerView.getPaddingRight()) {
1927                     recyclerView.scrollToPosition(toPos);
1928                 }
1929             }
1930 
1931             if (layoutManager.canScrollVertically()) {
1932                 final int minTop = layoutManager.getDecoratedTop(target.itemView);
1933                 if (minTop <= recyclerView.getPaddingTop()) {
1934                     recyclerView.scrollToPosition(toPos);
1935                 }
1936                 final int maxBottom = layoutManager.getDecoratedBottom(target.itemView);
1937                 if (maxBottom >= recyclerView.getHeight() - recyclerView.getPaddingBottom()) {
1938                     recyclerView.scrollToPosition(toPos);
1939                 }
1940             }
1941         }
1942 
onDraw(Canvas c, RecyclerView parent, ViewHolder selected, List<ItemTouchHelper.RecoverAnimation> recoverAnimationList, int actionState, float dX, float dY)1943         private void onDraw(Canvas c, RecyclerView parent, ViewHolder selected,
1944                 List<ItemTouchHelper.RecoverAnimation> recoverAnimationList,
1945                 int actionState, float dX, float dY) {
1946             final int recoverAnimSize = recoverAnimationList.size();
1947             for (int i = 0; i < recoverAnimSize; i++) {
1948                 final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i);
1949                 anim.update();
1950                 final int count = c.save();
1951                 onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState,
1952                         false);
1953                 c.restoreToCount(count);
1954             }
1955             if (selected != null) {
1956                 final int count = c.save();
1957                 onChildDraw(c, parent, selected, dX, dY, actionState, true);
1958                 c.restoreToCount(count);
1959             }
1960         }
1961 
onDrawOver(Canvas c, RecyclerView parent, ViewHolder selected, List<ItemTouchHelper.RecoverAnimation> recoverAnimationList, int actionState, float dX, float dY)1962         private void onDrawOver(Canvas c, RecyclerView parent, ViewHolder selected,
1963                 List<ItemTouchHelper.RecoverAnimation> recoverAnimationList,
1964                 int actionState, float dX, float dY) {
1965             final int recoverAnimSize = recoverAnimationList.size();
1966             for (int i = 0; i < recoverAnimSize; i++) {
1967                 final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i);
1968                 final int count = c.save();
1969                 onChildDrawOver(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState,
1970                         false);
1971                 c.restoreToCount(count);
1972             }
1973             if (selected != null) {
1974                 final int count = c.save();
1975                 onChildDrawOver(c, parent, selected, dX, dY, actionState, true);
1976                 c.restoreToCount(count);
1977             }
1978             boolean hasRunningAnimation = false;
1979             for (int i = recoverAnimSize - 1; i >= 0; i--) {
1980                 final RecoverAnimation anim = recoverAnimationList.get(i);
1981                 if (anim.mEnded && !anim.mIsPendingCleanup) {
1982                     recoverAnimationList.remove(i);
1983                 } else if (!anim.mEnded) {
1984                     hasRunningAnimation = true;
1985                 }
1986             }
1987             if (hasRunningAnimation) {
1988                 parent.invalidate();
1989             }
1990         }
1991 
1992         /**
1993          * Called by the ItemTouchHelper when the user interaction with an element is over and it
1994          * also completed its animation.
1995          * <p>
1996          * This is a good place to clear all changes on the View that was done in
1997          * {@link #onSelectedChanged(RecyclerView.ViewHolder, int)},
1998          * {@link #onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int,
1999          * boolean)} or
2000          * {@link #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, boolean)}.
2001          *
2002          * @param recyclerView The RecyclerView which is controlled by the ItemTouchHelper.
2003          * @param viewHolder   The View that was interacted by the user.
2004          */
clearView(RecyclerView recyclerView, ViewHolder viewHolder)2005         public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) {
2006             sUICallback.clearView(viewHolder.itemView);
2007         }
2008 
2009         /**
2010          * Called by ItemTouchHelper on RecyclerView's onDraw callback.
2011          * <p>
2012          * If you would like to customize how your View's respond to user interactions, this is
2013          * a good place to override.
2014          * <p>
2015          * Default implementation translates the child by the given <code>dX</code>,
2016          * <code>dY</code>.
2017          * ItemTouchHelper also takes care of drawing the child after other children if it is being
2018          * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this
2019          * is
2020          * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L
2021          * and after, it changes View's elevation value to be greater than all other children.)
2022          *
2023          * @param c                 The canvas which RecyclerView is drawing its children
2024          * @param recyclerView      The RecyclerView to which ItemTouchHelper is attached to
2025          * @param viewHolder        The ViewHolder which is being interacted by the User or it was
2026          *                          interacted and simply animating to its original position
2027          * @param dX                The amount of horizontal displacement caused by user's action
2028          * @param dY                The amount of vertical displacement caused by user's action
2029          * @param actionState       The type of interaction on the View. Is either {@link
2030          *                          #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}.
2031          * @param isCurrentlyActive True if this view is currently being controlled by the user or
2032          *                          false it is simply animating back to its original state.
2033          * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int,
2034          * boolean)
2035          */
onChildDraw(Canvas c, RecyclerView recyclerView, ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive)2036         public void onChildDraw(Canvas c, RecyclerView recyclerView,
2037                 ViewHolder viewHolder,
2038                 float dX, float dY, int actionState, boolean isCurrentlyActive) {
2039             sUICallback.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState,
2040                     isCurrentlyActive);
2041         }
2042 
2043         /**
2044          * Called by ItemTouchHelper on RecyclerView's onDraw callback.
2045          * <p>
2046          * If you would like to customize how your View's respond to user interactions, this is
2047          * a good place to override.
2048          * <p>
2049          * Default implementation translates the child by the given <code>dX</code>,
2050          * <code>dY</code>.
2051          * ItemTouchHelper also takes care of drawing the child after other children if it is being
2052          * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this
2053          * is
2054          * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L
2055          * and after, it changes View's elevation value to be greater than all other children.)
2056          *
2057          * @param c                 The canvas which RecyclerView is drawing its children
2058          * @param recyclerView      The RecyclerView to which ItemTouchHelper is attached to
2059          * @param viewHolder        The ViewHolder which is being interacted by the User or it was
2060          *                          interacted and simply animating to its original position
2061          * @param dX                The amount of horizontal displacement caused by user's action
2062          * @param dY                The amount of vertical displacement caused by user's action
2063          * @param actionState       The type of interaction on the View. Is either {@link
2064          *                          #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}.
2065          * @param isCurrentlyActive True if this view is currently being controlled by the user or
2066          *                          false it is simply animating back to its original state.
2067          * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int,
2068          * boolean)
2069          */
onChildDrawOver(Canvas c, RecyclerView recyclerView, ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive)2070         public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
2071                 ViewHolder viewHolder,
2072                 float dX, float dY, int actionState, boolean isCurrentlyActive) {
2073             sUICallback.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, actionState,
2074                     isCurrentlyActive);
2075         }
2076 
2077         /**
2078          * Called by the ItemTouchHelper when user action finished on a ViewHolder and now the View
2079          * will be animated to its final position.
2080          * <p>
2081          * Default implementation uses ItemAnimator's duration values. If
2082          * <code>animationType</code> is {@link #ANIMATION_TYPE_DRAG}, it returns
2083          * {@link RecyclerView.ItemAnimator#getMoveDuration()}, otherwise, it returns
2084          * {@link RecyclerView.ItemAnimator#getRemoveDuration()}. If RecyclerView does not have
2085          * any {@link RecyclerView.ItemAnimator} attached, this method returns
2086          * {@code DEFAULT_DRAG_ANIMATION_DURATION} or {@code DEFAULT_SWIPE_ANIMATION_DURATION}
2087          * depending on the animation type.
2088          *
2089          * @param recyclerView  The RecyclerView to which the ItemTouchHelper is attached to.
2090          * @param animationType The type of animation. Is one of {@link #ANIMATION_TYPE_DRAG},
2091          *                      {@link #ANIMATION_TYPE_SWIPE_CANCEL} or
2092          *                      {@link #ANIMATION_TYPE_SWIPE_SUCCESS}.
2093          * @param animateDx     The horizontal distance that the animation will offset
2094          * @param animateDy     The vertical distance that the animation will offset
2095          * @return The duration for the animation
2096          */
getAnimationDuration(RecyclerView recyclerView, int animationType, float animateDx, float animateDy)2097         public long getAnimationDuration(RecyclerView recyclerView, int animationType,
2098                 float animateDx, float animateDy) {
2099             final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator();
2100             if (itemAnimator == null) {
2101                 return animationType == ANIMATION_TYPE_DRAG ? DEFAULT_DRAG_ANIMATION_DURATION
2102                         : DEFAULT_SWIPE_ANIMATION_DURATION;
2103             } else {
2104                 return animationType == ANIMATION_TYPE_DRAG ? itemAnimator.getMoveDuration()
2105                         : itemAnimator.getRemoveDuration();
2106             }
2107         }
2108 
2109         /**
2110          * Called by the ItemTouchHelper when user is dragging a view out of bounds.
2111          * <p>
2112          * You can override this method to decide how much RecyclerView should scroll in response
2113          * to this action. Default implementation calculates a value based on the amount of View
2114          * out of bounds and the time it spent there. The longer user keeps the View out of bounds,
2115          * the faster the list will scroll. Similarly, the larger portion of the View is out of
2116          * bounds, the faster the RecyclerView will scroll.
2117          *
2118          * @param recyclerView        The RecyclerView instance to which ItemTouchHelper is
2119          *                            attached to.
2120          * @param viewSize            The total size of the View in scroll direction, excluding
2121          *                            item decorations.
2122          * @param viewSizeOutOfBounds The total size of the View that is out of bounds. This value
2123          *                            is negative if the View is dragged towards left or top edge.
2124          * @param totalSize           The total size of RecyclerView in the scroll direction.
2125          * @param msSinceStartScroll  The time passed since View is kept out of bounds.
2126          * @return The amount that RecyclerView should scroll. Keep in mind that this value will
2127          * be passed to {@link RecyclerView#scrollBy(int, int)} method.
2128          */
interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize, int viewSizeOutOfBounds, int totalSize, long msSinceStartScroll)2129         public int interpolateOutOfBoundsScroll(RecyclerView recyclerView,
2130                 int viewSize, int viewSizeOutOfBounds,
2131                 int totalSize, long msSinceStartScroll) {
2132             final int maxScroll = getMaxDragScroll(recyclerView);
2133             final int absOutOfBounds = Math.abs(viewSizeOutOfBounds);
2134             final int direction = (int) Math.signum(viewSizeOutOfBounds);
2135             // might be negative if other direction
2136             float outOfBoundsRatio = Math.min(1f, 1f * absOutOfBounds / viewSize);
2137             final int cappedScroll = (int) (direction * maxScroll *
2138                     sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio));
2139             final float timeRatio;
2140             if (msSinceStartScroll > DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS) {
2141                 timeRatio = 1f;
2142             } else {
2143                 timeRatio = (float) msSinceStartScroll / DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS;
2144             }
2145             final int value = (int) (cappedScroll * sDragScrollInterpolator
2146                     .getInterpolation(timeRatio));
2147             if (value == 0) {
2148                 return viewSizeOutOfBounds > 0 ? 1 : -1;
2149             }
2150             return value;
2151         }
2152     }
2153 
2154     /**
2155      * A simple wrapper to the default Callback which you can construct with drag and swipe
2156      * directions and this class will handle the flag callbacks. You should still override onMove
2157      * or
2158      * onSwiped depending on your use case.
2159      *
2160      * <pre>
2161      * ItemTouchHelper mIth = new ItemTouchHelper(
2162      *     new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
2163      *         ItemTouchHelper.LEFT) {
2164      *         public abstract boolean onMove(RecyclerView recyclerView,
2165      *             ViewHolder viewHolder, ViewHolder target) {
2166      *             final int fromPos = viewHolder.getAdapterPosition();
2167      *             final int toPos = target.getAdapterPosition();
2168      *             // move item in `fromPos` to `toPos` in adapter.
2169      *             return true;// true if moved, false otherwise
2170      *         }
2171      *         public void onSwiped(ViewHolder viewHolder, int direction) {
2172      *             // remove from adapter
2173      *         }
2174      * });
2175      * </pre>
2176      */
2177     public abstract static class SimpleCallback extends Callback {
2178 
2179         private int mDefaultSwipeDirs;
2180 
2181         private int mDefaultDragDirs;
2182 
2183         /**
2184          * Creates a Callback for the given drag and swipe allowance. These values serve as
2185          * defaults
2186          * and if you want to customize behavior per ViewHolder, you can override
2187          * {@link #getSwipeDirs(RecyclerView, ViewHolder)}
2188          * and / or {@link #getDragDirs(RecyclerView, ViewHolder)}.
2189          *
2190          * @param dragDirs  Binary OR of direction flags in which the Views can be dragged. Must be
2191          *                  composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link
2192          *                  #END},
2193          *                  {@link #UP} and {@link #DOWN}.
2194          * @param swipeDirs Binary OR of direction flags in which the Views can be swiped. Must be
2195          *                  composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link
2196          *                  #END},
2197          *                  {@link #UP} and {@link #DOWN}.
2198          */
SimpleCallback(int dragDirs, int swipeDirs)2199         public SimpleCallback(int dragDirs, int swipeDirs) {
2200             mDefaultSwipeDirs = swipeDirs;
2201             mDefaultDragDirs = dragDirs;
2202         }
2203 
2204         /**
2205          * Updates the default swipe directions. For example, you can use this method to toggle
2206          * certain directions depending on your use case.
2207          *
2208          * @param defaultSwipeDirs Binary OR of directions in which the ViewHolders can be swiped.
2209          */
setDefaultSwipeDirs(int defaultSwipeDirs)2210         public void setDefaultSwipeDirs(int defaultSwipeDirs) {
2211             mDefaultSwipeDirs = defaultSwipeDirs;
2212         }
2213 
2214         /**
2215          * Updates the default drag directions. For example, you can use this method to toggle
2216          * certain directions depending on your use case.
2217          *
2218          * @param defaultDragDirs Binary OR of directions in which the ViewHolders can be dragged.
2219          */
setDefaultDragDirs(int defaultDragDirs)2220         public void setDefaultDragDirs(int defaultDragDirs) {
2221             mDefaultDragDirs = defaultDragDirs;
2222         }
2223 
2224         /**
2225          * Returns the swipe directions for the provided ViewHolder.
2226          * Default implementation returns the swipe directions that was set via constructor or
2227          * {@link #setDefaultSwipeDirs(int)}.
2228          *
2229          * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to.
2230          * @param viewHolder   The RecyclerView for which the swipe direction is queried.
2231          * @return A binary OR of direction flags.
2232          */
getSwipeDirs(RecyclerView recyclerView, ViewHolder viewHolder)2233         public int getSwipeDirs(RecyclerView recyclerView, ViewHolder viewHolder) {
2234             return mDefaultSwipeDirs;
2235         }
2236 
2237         /**
2238          * Returns the drag directions for the provided ViewHolder.
2239          * Default implementation returns the drag directions that was set via constructor or
2240          * {@link #setDefaultDragDirs(int)}.
2241          *
2242          * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to.
2243          * @param viewHolder   The RecyclerView for which the swipe direction is queried.
2244          * @return A binary OR of direction flags.
2245          */
getDragDirs(RecyclerView recyclerView, ViewHolder viewHolder)2246         public int getDragDirs(RecyclerView recyclerView, ViewHolder viewHolder) {
2247             return mDefaultDragDirs;
2248         }
2249 
2250         @Override
getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder)2251         public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
2252             return makeMovementFlags(getDragDirs(recyclerView, viewHolder),
2253                     getSwipeDirs(recyclerView, viewHolder));
2254         }
2255     }
2256 
2257     private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
2258 
2259         @Override
onDown(MotionEvent e)2260         public boolean onDown(MotionEvent e) {
2261             return true;
2262         }
2263 
2264         @Override
onLongPress(MotionEvent e)2265         public void onLongPress(MotionEvent e) {
2266             View child = findChildView(e);
2267             if (child != null) {
2268                 ViewHolder vh = mRecyclerView.getChildViewHolder(child);
2269                 if (vh != null) {
2270                     if (!mCallback.hasDragFlag(mRecyclerView, vh)) {
2271                         return;
2272                     }
2273                     int pointerId = e.getPointerId(0);
2274                     // Long press is deferred.
2275                     // Check w/ active pointer id to avoid selecting after motion
2276                     // event is canceled.
2277                     if (pointerId == mActivePointerId) {
2278                         final int index = e.findPointerIndex(mActivePointerId);
2279                         final float x = e.getX(index);
2280                         final float y = e.getY(index);
2281                         mInitialTouchX = x;
2282                         mInitialTouchY = y;
2283                         mDx = mDy = 0f;
2284                         if (DEBUG) {
2285                             Log.d(TAG,
2286                                     "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY);
2287                         }
2288                         if (mCallback.isLongPressDragEnabled()) {
2289                             select(vh, ACTION_STATE_DRAG);
2290                         }
2291                     }
2292                 }
2293             }
2294         }
2295     }
2296 
2297     private class RecoverAnimation implements AnimatorListenerCompat {
2298 
2299         final float mStartDx;
2300 
2301         final float mStartDy;
2302 
2303         final float mTargetX;
2304 
2305         final float mTargetY;
2306 
2307         final ViewHolder mViewHolder;
2308 
2309         final int mActionState;
2310 
2311         private final ValueAnimatorCompat mValueAnimator;
2312 
2313         private final int mAnimationType;
2314 
2315         public boolean mIsPendingCleanup;
2316 
2317         float mX;
2318 
2319         float mY;
2320 
2321         // if user starts touching a recovering view, we put it into interaction mode again,
2322         // instantly.
2323         boolean mOverridden = false;
2324 
2325         private boolean mEnded = false;
2326 
2327         private float mFraction;
2328 
RecoverAnimation(ViewHolder viewHolder, int animationType, int actionState, float startDx, float startDy, float targetX, float targetY)2329         public RecoverAnimation(ViewHolder viewHolder, int animationType,
2330                 int actionState, float startDx, float startDy, float targetX, float targetY) {
2331             mActionState = actionState;
2332             mAnimationType = animationType;
2333             mViewHolder = viewHolder;
2334             mStartDx = startDx;
2335             mStartDy = startDy;
2336             mTargetX = targetX;
2337             mTargetY = targetY;
2338             mValueAnimator = AnimatorCompatHelper.emptyValueAnimator();
2339             mValueAnimator.addUpdateListener(
2340                     new AnimatorUpdateListenerCompat() {
2341                         @Override
2342                         public void onAnimationUpdate(ValueAnimatorCompat animation) {
2343                             setFraction(animation.getAnimatedFraction());
2344                         }
2345                     });
2346             mValueAnimator.setTarget(viewHolder.itemView);
2347             mValueAnimator.addListener(this);
2348             setFraction(0f);
2349         }
2350 
setDuration(long duration)2351         public void setDuration(long duration) {
2352             mValueAnimator.setDuration(duration);
2353         }
2354 
start()2355         public void start() {
2356             mViewHolder.setIsRecyclable(false);
2357             mValueAnimator.start();
2358         }
2359 
cancel()2360         public void cancel() {
2361             mValueAnimator.cancel();
2362         }
2363 
setFraction(float fraction)2364         public void setFraction(float fraction) {
2365             mFraction = fraction;
2366         }
2367 
2368         /**
2369          * We run updates on onDraw method but use the fraction from animator callback.
2370          * This way, we can sync translate x/y values w/ the animators to avoid one-off frames.
2371          */
update()2372         public void update() {
2373             if (mStartDx == mTargetX) {
2374                 mX = ViewCompat.getTranslationX(mViewHolder.itemView);
2375             } else {
2376                 mX = mStartDx + mFraction * (mTargetX - mStartDx);
2377             }
2378             if (mStartDy == mTargetY) {
2379                 mY = ViewCompat.getTranslationY(mViewHolder.itemView);
2380             } else {
2381                 mY = mStartDy + mFraction * (mTargetY - mStartDy);
2382             }
2383         }
2384 
2385         @Override
onAnimationStart(ValueAnimatorCompat animation)2386         public void onAnimationStart(ValueAnimatorCompat animation) {
2387 
2388         }
2389 
2390         @Override
onAnimationEnd(ValueAnimatorCompat animation)2391         public void onAnimationEnd(ValueAnimatorCompat animation) {
2392             if (!mEnded) {
2393                 mViewHolder.setIsRecyclable(true);
2394             }
2395             mEnded = true;
2396         }
2397 
2398         @Override
onAnimationCancel(ValueAnimatorCompat animation)2399         public void onAnimationCancel(ValueAnimatorCompat animation) {
2400             setFraction(1f); //make sure we recover the view's state.
2401         }
2402 
2403         @Override
onAnimationRepeat(ValueAnimatorCompat animation)2404         public void onAnimationRepeat(ValueAnimatorCompat animation) {
2405 
2406         }
2407     }
2408 }