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