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