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