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