com.github.shareme.gwsmaterialuikit.library.advancerv.swipeable.RecyclerViewSwipeManager.java Source code

Java tutorial

Introduction

Here is the source code for com.github.shareme.gwsmaterialuikit.library.advancerv.swipeable.RecyclerViewSwipeManager.java

Source

/*
 *    Copyright (C) 2015 Haruki Hasegawa
 *    Modifications Copyright(C) 2016 Fred Grott(GrottWorkShop)
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.github.shareme.gwsmaterialuikit.library.advancerv.swipeable;

import android.graphics.Rect;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;

import com.github.shareme.gwsmaterialuikit.library.advancerv.animator.SwipeDismissItemAnimator;
import com.github.shareme.gwsmaterialuikit.library.advancerv.swipeable.action.SwipeResultAction;
import com.github.shareme.gwsmaterialuikit.library.advancerv.swipeable.action.SwipeResultActionDefault;
import com.github.shareme.gwsmaterialuikit.library.advancerv.utils.CustomRecyclerViewUtils;
import com.github.shareme.gwsmaterialuikit.library.advancerv.utils.WrapperAdapterUtils;

import timber.log.Timber;

/**
 * Provides item swipe operation for {@link RecyclerView}
 */
@SuppressWarnings("PointlessBitwiseExpression")
public class RecyclerViewSwipeManager implements SwipeableItemConstants {
    private static final String TAG = "ARVSwipeManager";

    /**
     * Used for listening item swipe events
     */
    public interface OnItemSwipeEventListener {
        /**
         * Callback method to be invoked when swiping is started.
         *
         * @param position The position of the item.
         */
        void onItemSwipeStarted(int position);

        /**
         * Callback method to be invoked when swiping is finished.
         *
         * @param position           The position of the item.
         * @param result             The result code of the swipe operation.
         * @param afterSwipeReaction The reaction type to the swipe operation.
         */
        void onItemSwipeFinished(int position, int result, int afterSwipeReaction);
    }

    // ---

    private static final int MIN_DISTANCE_TOUCH_SLOP_MUL = 10;
    private static final int SLIDE_ITEM_IMMEDIATELY_SET_TRANSLATION_THRESHOLD_DP = 8;

    private static final boolean LOCAL_LOGV = false;
    private static final boolean LOCAL_LOGD = false;

    private RecyclerView.OnItemTouchListener mInternalUseOnItemTouchListener;
    private RecyclerView mRecyclerView;

    private long mReturnToDefaultPositionAnimationDuration = 300;
    private long mMoveToOutsideWindowAnimationDuration = 200;

    private int mTouchSlop;
    private int mMinFlingVelocity; // [pixels per second]
    private int mMaxFlingVelocity; // [pixels per second]
    private int mInitialTouchX;
    private int mInitialTouchY;
    private long mCheckingTouchSlop = RecyclerView.NO_ID;
    private boolean mSwipeHorizontal;

    private ItemSlidingAnimator mItemSlideAnimator;
    private SwipeableItemWrapperAdapter<RecyclerView.ViewHolder> mAdapter;
    private RecyclerView.ViewHolder mSwipingItem;
    private int mSwipingItemPosition = RecyclerView.NO_POSITION;
    private long mSwipingItemId = RecyclerView.NO_ID;
    private final Rect mSwipingItemMargins = new Rect();
    private int mTouchedItemOffsetX;
    private int mTouchedItemOffsetY;
    private int mLastTouchX;
    private int mLastTouchY;
    private int mSwipingItemReactionType;
    private VelocityTracker mVelocityTracker;
    private SwipingItemOperator mSwipingItemOperator;
    private OnItemSwipeEventListener mItemSwipeEventListener;
    private InternalHandler mHandler;
    private int mLongPressTimeout;

    /**
     * Constructor.
     */
    public RecyclerViewSwipeManager() {
        mInternalUseOnItemTouchListener = new RecyclerView.OnItemTouchListener() {
            @Override
            public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                return RecyclerViewSwipeManager.this.onInterceptTouchEvent(rv, e);
            }

            @Override
            public void onTouchEvent(RecyclerView rv, MotionEvent e) {
                RecyclerViewSwipeManager.this.onTouchEvent(rv, e);
            }

            @Override
            public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
                RecyclerViewSwipeManager.this.onRequestDisallowInterceptTouchEvent(disallowIntercept);
            }
        };
        mVelocityTracker = VelocityTracker.obtain();
        mLongPressTimeout = ViewConfiguration.getLongPressTimeout();
    }

    /**
     * Create wrapped adapter.
     *
     * @param adapter The target adapter.
     * @return Wrapped adapter which is associated to this {@link RecyclerViewSwipeManager} instance.
     */
    @SuppressWarnings("unchecked")
    public RecyclerView.Adapter createWrappedAdapter(@NonNull RecyclerView.Adapter adapter) {
        if (!adapter.hasStableIds()) {
            throw new IllegalArgumentException("The passed adapter does not support stable IDs");
        }

        if (mAdapter != null) {
            throw new IllegalStateException("already have a wrapped adapter");
        }

        mAdapter = new SwipeableItemWrapperAdapter(this, adapter);

        return mAdapter;
    }

    /**
     * Indicates this manager instance has released or not.
     *
     * @return True if this manager instance has released
     */
    public boolean isReleased() {
        return (mInternalUseOnItemTouchListener == null);
    }

    /**
     * <p>Attaches {@link RecyclerView} instance.</p>
     * <p>Before calling this method, the target {@link RecyclerView} must set
     * the wrapped adapter instance which is returned by the
     * {@link #createWrappedAdapter(RecyclerView.Adapter)} method.</p>
     *
     * @param rv The {@link RecyclerView} instance
     */
    public void attachRecyclerView(@NonNull RecyclerView rv) {
        if (isReleased()) {
            throw new IllegalStateException("Accessing released object");
        }

        if (mRecyclerView != null) {
            throw new IllegalStateException("RecyclerView instance has already been set");
        }

        if (mAdapter == null || getSwipeableItemWrapperAdapter(rv) != mAdapter) {
            throw new IllegalStateException("adapter is not set properly");
        }

        final int layoutOrientation = CustomRecyclerViewUtils.getOrientation(rv);
        if (layoutOrientation == CustomRecyclerViewUtils.ORIENTATION_UNKNOWN) {
            throw new IllegalStateException("failed to determine layout orientation");
        }

        mRecyclerView = rv;
        mRecyclerView.addOnItemTouchListener(mInternalUseOnItemTouchListener);

        final ViewConfiguration vc = ViewConfiguration.get(rv.getContext());

        mTouchSlop = vc.getScaledTouchSlop();
        mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
        mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();

        mItemSlideAnimator = new ItemSlidingAnimator(mAdapter);
        mItemSlideAnimator
                .setImmediatelySetTranslationThreshold((int) (rv.getResources().getDisplayMetrics().density
                        * SLIDE_ITEM_IMMEDIATELY_SET_TRANSLATION_THRESHOLD_DP + 0.5f));

        mSwipeHorizontal = (layoutOrientation == CustomRecyclerViewUtils.ORIENTATION_VERTICAL);
        mHandler = new InternalHandler(this);
    }

    /**
     * <p>Detach the {@link RecyclerView} instance and release internal field references.</p>
     * <p>This method should be called in order to avoid memory leaks.</p>
     */
    public void release() {
        cancelSwipe(true);

        if (mHandler != null) {
            mHandler.release();
            mHandler = null;
        }

        if (mRecyclerView != null && mInternalUseOnItemTouchListener != null) {
            mRecyclerView.removeOnItemTouchListener(mInternalUseOnItemTouchListener);
        }
        mInternalUseOnItemTouchListener = null;

        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }

        if (mItemSlideAnimator != null) {
            mItemSlideAnimator.endAnimations();
            mItemSlideAnimator = null;
        }

        mAdapter = null;
        mRecyclerView = null;
    }

    /**
     * Indicates whether currently performing item swiping.
     *
     * @return True if currently performing item swiping
     */
    public boolean isSwiping() {
        return (mSwipingItem != null) && (!mHandler.isCancelSwipeRequested());
    }

    /**
     * Sets the time required to consider press as long press. (default: 500ms)
     *
     * @param longPressTimeout Integer in milli seconds.
     */
    public void setLongPressTimeout(int longPressTimeout) {
        mLongPressTimeout = longPressTimeout;
    }

    /*package*/ boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        final int action = MotionEventCompat.getActionMasked(e);

        if (LOCAL_LOGV) {
            Timber.v("onInterceptTouchEvent() action = " + action);
        }

        switch (action) {
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            if (handleActionUpOrCancel(e, true)) {
                return true;
            }
            break;

        case MotionEvent.ACTION_DOWN:
            if (!isSwiping()) {
                handleActionDown(rv, e);
            }
            break;

        case MotionEvent.ACTION_MOVE:
            if (isSwiping()) {
                // NOTE: The first ACTION_MOVE event will come here. (maybe a bug of RecyclerView?)
                handleActionMoveWhileSwiping(e);
                return true;
            } else {
                if (handleActionMoveWhileNotSwiping(rv, e)) {
                    return true;
                }
            }
            break;
        }

        return false;
    }

    /*package*/ void onTouchEvent(RecyclerView rv, MotionEvent e) {
        final int action = MotionEventCompat.getActionMasked(e);

        if (LOCAL_LOGV) {
            Timber.v("onTouchEvent() action = " + action);
        }

        if (!isSwiping()) {
            // Log.w(TAG, "onTouchEvent() - unexpected state");
            return;
        }

        switch (action) {
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            handleActionUpOrCancel(e, true);
            break;

        case MotionEvent.ACTION_MOVE:
            handleActionMoveWhileSwiping(e);
            break;
        }
    }

    /*package*/ void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept) {
            cancelSwipe(true);
        }
    }

    private boolean handleActionDown(RecyclerView rv, MotionEvent e) {
        final RecyclerView.Adapter adapter = rv.getAdapter();
        final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithTranslation(rv,
                e.getX(), e.getY());

        if (!(holder instanceof SwipeableItemViewHolder)) {
            return false;
        }

        final int itemPosition = CustomRecyclerViewUtils.getSynchronizedPosition(holder);

        // verify the touched item is valid state
        if (!(itemPosition >= 0 && itemPosition < adapter.getItemCount())) {
            return false;
        }
        if (holder.getItemId() != adapter.getItemId(itemPosition)) {
            return false;
        }

        final int touchX = (int) (e.getX() + 0.5f);
        final int touchY = (int) (e.getY() + 0.5f);

        final View view = holder.itemView;
        final int translateX = (int) (ViewCompat.getTranslationX(view) + 0.5f);
        final int translateY = (int) (ViewCompat.getTranslationY(view) + 0.5f);
        final int viewX = touchX - (view.getLeft() + translateX);
        final int viewY = touchY - (view.getTop() + translateY);

        final int reactionType = mAdapter.getSwipeReactionType(holder, itemPosition, viewX, viewY);

        if (reactionType == 0) {
            return false;
        }

        mInitialTouchX = touchX;
        mInitialTouchY = touchY;
        mCheckingTouchSlop = holder.getItemId();
        mSwipingItemReactionType = reactionType;

        if ((reactionType & REACTION_START_SWIPE_ON_LONG_PRESS) != 0) {
            mHandler.startLongPressDetection(e, mLongPressTimeout);
        }

        return true;
    }

    private static SwipeableItemWrapperAdapter getSwipeableItemWrapperAdapter(RecyclerView rv) {
        return WrapperAdapterUtils.findWrappedAdapter(rv.getAdapter(), SwipeableItemWrapperAdapter.class);
    }

    private boolean handleActionUpOrCancel(MotionEvent e, boolean invokeFinish) {
        int action = MotionEvent.ACTION_CANCEL;

        if (e != null) {
            action = MotionEventCompat.getActionMasked(e);
            mLastTouchX = (int) (e.getX() + 0.5f);
            mLastTouchY = (int) (e.getY() + 0.5f);
        }

        if (isSwiping()) {
            if (invokeFinish) {
                handleActionUpOrCancelWhileSwiping(action);
            }
            return true;
        } else {
            handleActionUpOrCancelWhileNotSwiping();
            return false;
        }
    }

    private void handleActionUpOrCancelWhileNotSwiping() {
        if (mHandler != null) {
            mHandler.cancelLongPressDetection();
        }

        mCheckingTouchSlop = RecyclerView.NO_ID;
        mSwipingItemReactionType = 0;
    }

    private void handleActionUpOrCancelWhileSwiping(int action) {
        int result = RESULT_CANCELED;

        if (action == MotionEvent.ACTION_UP) {
            final boolean horizontal = mSwipeHorizontal;
            final View itemView = mSwipingItem.itemView;
            final int viewSize = (horizontal) ? itemView.getWidth() : itemView.getHeight();
            final float distance = (horizontal) ? (mLastTouchX - mInitialTouchX) : (mLastTouchY - mInitialTouchY);
            final float absDistance = Math.abs(distance);

            mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity); // 1000: pixels per second

            final float velocity = (horizontal) ? mVelocityTracker.getXVelocity() : mVelocityTracker.getYVelocity();
            final float absVelocity = Math.abs(velocity);

            if ((absDistance > (mTouchSlop * MIN_DISTANCE_TOUCH_SLOP_MUL)) && ((distance * velocity) > 0.0f)
                    && (absVelocity <= mMaxFlingVelocity)
                    && ((absDistance > (viewSize / 2)) || (absVelocity >= mMinFlingVelocity))) {

                if (horizontal && (distance < 0) && SwipeReactionUtils.canSwipeLeft(mSwipingItemReactionType)) {
                    result = RESULT_SWIPED_LEFT;
                } else if ((!horizontal) && (distance < 0)
                        && SwipeReactionUtils.canSwipeUp(mSwipingItemReactionType)) {
                    result = RESULT_SWIPED_UP;
                } else if (horizontal && (distance > 0)
                        && SwipeReactionUtils.canSwipeRight(mSwipingItemReactionType)) {
                    result = RESULT_SWIPED_RIGHT;
                } else if ((!horizontal) && (distance > 0)
                        && SwipeReactionUtils.canSwipeDown(mSwipingItemReactionType)) {
                    result = RESULT_SWIPED_DOWN;
                }
            }
        }

        if (LOCAL_LOGD) {
            Timber.d("swiping finished  --- result = " + result);
        }

        finishSwiping(result);
    }

    private boolean handleActionMoveWhileNotSwiping(RecyclerView rv, MotionEvent e) {
        if (mCheckingTouchSlop == RecyclerView.NO_ID) {
            return false;
        }

        final int dx = (int) (e.getX() + 0.5f) - mInitialTouchX;
        final int dy = (int) (e.getY() + 0.5f) - mInitialTouchY;

        final int scrollAxisDelta;
        final int swipeAxisDelta;

        if (mSwipeHorizontal) {
            scrollAxisDelta = dy;
            swipeAxisDelta = dx;
        } else {
            scrollAxisDelta = dx;
            swipeAxisDelta = dy;
        }

        if (Math.abs(scrollAxisDelta) > mTouchSlop) {
            // scrolling occurred
            mCheckingTouchSlop = RecyclerView.NO_ID;
            return false;
        }

        if (Math.abs(swipeAxisDelta) <= mTouchSlop) {
            return false;
        }

        // check swipeable direction mask
        boolean dirMasked;
        if (mSwipeHorizontal) {
            if (swipeAxisDelta < 0) {
                dirMasked = ((mSwipingItemReactionType & REACTION_MASK_START_SWIPE_LEFT) != 0);
            } else {
                dirMasked = ((mSwipingItemReactionType & REACTION_MASK_START_SWIPE_RIGHT) != 0);
            }
        } else {
            if (swipeAxisDelta < 0) {
                dirMasked = ((mSwipingItemReactionType & REACTION_MASK_START_SWIPE_UP) != 0);
            } else {
                dirMasked = ((mSwipingItemReactionType & REACTION_MASK_START_SWIPE_DOWN) != 0);
            }
        }

        if (dirMasked) {
            // masked
            mCheckingTouchSlop = RecyclerView.NO_ID;
            return false;
        }

        final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithTranslation(rv,
                e.getX(), e.getY());

        if (holder == null || holder.getItemId() != mCheckingTouchSlop) {
            mCheckingTouchSlop = RecyclerView.NO_ID;
            return false;
        }

        return checkConditionAndStartSwiping(e, holder);
    }

    private void handleActionMoveWhileSwiping(MotionEvent e) {
        mLastTouchX = (int) (e.getX() + 0.5f);
        mLastTouchY = (int) (e.getY() + 0.5f);
        mVelocityTracker.addMovement(e);

        final int swipeDistanceX = mLastTouchX - mTouchedItemOffsetX;
        final int swipeDistanceY = mLastTouchY - mTouchedItemOffsetY;
        mSwipingItemPosition = getItemPosition(mAdapter, mSwipingItemId, mSwipingItemPosition);

        mSwipingItemOperator.update(mSwipingItemPosition, swipeDistanceX, swipeDistanceY);
    }

    private boolean checkConditionAndStartSwiping(MotionEvent e, RecyclerView.ViewHolder holder) {
        final int itemPosition = CustomRecyclerViewUtils.getSynchronizedPosition(holder);

        if (itemPosition == RecyclerView.NO_POSITION) {
            return false;
        }

        startSwiping(e, holder, itemPosition);

        return true;
    }

    private void startSwiping(MotionEvent e, RecyclerView.ViewHolder holder, int itemPosition) {
        if (LOCAL_LOGD) {
            Timber.d("swiping started");
        }

        mHandler.cancelLongPressDetection();

        mSwipingItem = holder;
        mSwipingItemPosition = itemPosition;
        mSwipingItemId = mAdapter.getItemId(itemPosition);
        mLastTouchX = (int) (e.getX() + 0.5f);
        mLastTouchY = (int) (e.getY() + 0.5f);
        mTouchedItemOffsetX = mLastTouchX;
        mTouchedItemOffsetY = mLastTouchY;
        mCheckingTouchSlop = RecyclerView.NO_ID;
        CustomRecyclerViewUtils.getLayoutMargins(holder.itemView, mSwipingItemMargins);

        mSwipingItemOperator = new SwipingItemOperator(this, mSwipingItem, mSwipingItemReactionType,
                mSwipeHorizontal);
        mSwipingItemOperator.start();

        mVelocityTracker.clear();
        mVelocityTracker.addMovement(e);

        mRecyclerView.getParent().requestDisallowInterceptTouchEvent(true);

        // raise onItemSwipeStarted() event
        if (mItemSwipeEventListener != null) {
            mItemSwipeEventListener.onItemSwipeStarted(itemPosition);
        }

        // raise onSwipeItemStarted() event
        mAdapter.onSwipeItemStarted(this, holder, mSwipingItemId);
    }

    private void finishSwiping(int result) {
        final RecyclerView.ViewHolder swipingItem = mSwipingItem;

        if (swipingItem == null) {
            return;
        }

        // cancel deferred request
        mHandler.removeDeferredCancelSwipeRequest();

        mHandler.cancelLongPressDetection();

        if (mRecyclerView != null && mRecyclerView.getParent() != null) {
            mRecyclerView.getParent().requestDisallowInterceptTouchEvent(false);
        }

        final int itemPosition = getItemPosition(mAdapter, mSwipingItemId, mSwipingItemPosition);

        mVelocityTracker.clear();

        mSwipingItem = null;
        mSwipingItemPosition = RecyclerView.NO_POSITION;
        mSwipingItemId = RecyclerView.NO_ID;
        mLastTouchX = 0;
        mLastTouchY = 0;
        mInitialTouchX = 0;
        mTouchedItemOffsetX = 0;
        mTouchedItemOffsetY = 0;
        mCheckingTouchSlop = RecyclerView.NO_ID;
        mSwipingItemReactionType = 0;

        if (mSwipingItemOperator != null) {
            mSwipingItemOperator.finish();
            mSwipingItemOperator = null;
        }

        final int slideDir = resultCodeToSlideDirection(result);
        SwipeResultAction resultAction = null;

        if (mAdapter != null) {
            resultAction = mAdapter.onSwipeItemFinished(swipingItem, itemPosition, result);
        }

        if (resultAction == null) {
            resultAction = new SwipeResultActionDefault(); // set default action
        }

        int afterReaction = resultAction.getResultActionType();

        verifyAfterReaction(result, afterReaction);

        //noinspection UnusedAssignment
        boolean slideAnimated = false;

        switch (afterReaction) {
        case AFTER_SWIPE_REACTION_DEFAULT:
            slideAnimated = mItemSlideAnimator.finishSwipeSlideToDefaultPosition(swipingItem, mSwipeHorizontal,
                    true, mReturnToDefaultPositionAnimationDuration, itemPosition, resultAction);
            break;
        case AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION:
            slideAnimated = mItemSlideAnimator.finishSwipeSlideToOutsideOfWindow(swipingItem, slideDir, true,
                    mMoveToOutsideWindowAnimationDuration, itemPosition, resultAction);
            break;
        case AFTER_SWIPE_REACTION_REMOVE_ITEM: {
            final RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator();

            final long removeAnimationDuration = (itemAnimator != null) ? itemAnimator.getRemoveDuration() : 0;

            if (supportsViewPropertyAnimator()) {
                final long moveAnimationDuration = (itemAnimator != null) ? itemAnimator.getMoveDuration() : 0;

                final RemovingItemDecorator decorator = new RemovingItemDecorator(mRecyclerView, swipingItem,
                        result, removeAnimationDuration, moveAnimationDuration);

                decorator.setMoveAnimationInterpolator(SwipeDismissItemAnimator.MOVE_INTERPOLATOR);
                decorator.start();
            }

            slideAnimated = mItemSlideAnimator.finishSwipeSlideToOutsideOfWindow(swipingItem, slideDir, true,
                    removeAnimationDuration, itemPosition, resultAction);
        }
            break;
        default:
            throw new IllegalStateException("Unknown after reaction type: " + afterReaction);
        }

        if (mAdapter != null) {
            mAdapter.onSwipeItemFinished2(swipingItem, itemPosition, result, afterReaction, resultAction);
        }

        // raise onItemSwipeFinished() event
        if (mItemSwipeEventListener != null) {
            mItemSwipeEventListener.onItemSwipeFinished(itemPosition, result, afterReaction);
        }

        // invoke onSwipeSlideItemAnimationEnd
        if (!slideAnimated) {
            resultAction.slideAnimationEnd();
        }
    }

    private static void verifyAfterReaction(int result, int afterReaction) {
        if ((afterReaction == AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION)
                || (afterReaction == AFTER_SWIPE_REACTION_REMOVE_ITEM)) {
            switch (result) {
            case RESULT_SWIPED_LEFT:
            case RESULT_SWIPED_UP:
            case RESULT_SWIPED_RIGHT:
            case RESULT_SWIPED_DOWN:
                break;
            default:
                throw new IllegalStateException("Unexpected after reaction has been requested: result = " + result
                        + ", afterReaction = " + afterReaction);
            }
        }
    }

    private static int resultCodeToSlideDirection(int result) {
        switch (result) {
        case RESULT_SWIPED_LEFT:
            return ItemSlidingAnimator.DIR_LEFT;
        case RESULT_SWIPED_UP:
            return ItemSlidingAnimator.DIR_UP;
        case RESULT_SWIPED_RIGHT:
            return ItemSlidingAnimator.DIR_RIGHT;
        case RESULT_SWIPED_DOWN:
            return ItemSlidingAnimator.DIR_DOWN;
        default:
            // NOTE: returned value should not be used.
            return ItemSlidingAnimator.DIR_LEFT;
        }
    }

    private static int getItemPosition(@Nullable RecyclerView.Adapter adapter, long itemId, int itemPositionGuess) {
        if (adapter == null)
            return RecyclerView.NO_POSITION;

        final int itemCount = adapter.getItemCount();
        if (itemPositionGuess >= 0 && itemPositionGuess < itemCount) {
            if (adapter.getItemId(itemPositionGuess) == itemId)
                return itemPositionGuess;
        }

        for (int i = 0; i < itemCount; i++) {
            if (adapter.getItemId(i) == itemId)
                return i;
        }

        return RecyclerView.NO_POSITION;
    }

    public void cancelSwipe() {
        cancelSwipe(false);
    }

    /*package*/ void cancelSwipe(boolean immediately) {
        handleActionUpOrCancel(null, false);

        if (immediately) {
            finishSwiping(RESULT_CANCELED);
        } else {
            if (isSwiping()) {
                mHandler.requestDeferredCancelSwipe();
            }
        }
    }

    /*package*/ boolean isAnimationRunning(RecyclerView.ViewHolder item) {
        return (mItemSlideAnimator != null) && (mItemSlideAnimator.isRunning(item));
    }

    private void slideItem(RecyclerView.ViewHolder holder, float amount, boolean horizontal,
            boolean shouldAnimate) {
        if (amount == OUTSIDE_OF_THE_WINDOW_LEFT) {
            mItemSlideAnimator.slideToOutsideOfWindow(holder, ItemSlidingAnimator.DIR_LEFT, shouldAnimate,
                    mMoveToOutsideWindowAnimationDuration);
        } else if (amount == OUTSIDE_OF_THE_WINDOW_TOP) {
            mItemSlideAnimator.slideToOutsideOfWindow(holder, ItemSlidingAnimator.DIR_UP, shouldAnimate,
                    mMoveToOutsideWindowAnimationDuration);
        } else if (amount == OUTSIDE_OF_THE_WINDOW_RIGHT) {
            mItemSlideAnimator.slideToOutsideOfWindow(holder, ItemSlidingAnimator.DIR_RIGHT, shouldAnimate,
                    mMoveToOutsideWindowAnimationDuration);
        } else if (amount == OUTSIDE_OF_THE_WINDOW_BOTTOM) {
            mItemSlideAnimator.slideToOutsideOfWindow(holder, ItemSlidingAnimator.DIR_DOWN, shouldAnimate,
                    mMoveToOutsideWindowAnimationDuration);
        } else if (amount == 0.0f) {
            mItemSlideAnimator.slideToDefaultPosition(holder, horizontal, shouldAnimate,
                    mReturnToDefaultPositionAnimationDuration);
        } else {
            mItemSlideAnimator.slideToSpecifiedPosition(holder, amount, horizontal);
        }
    }

    /**
     * Gets the duration of the "return to default position" animation
     *
     * @return Duration of the "return to default position" animation in milliseconds
     */
    public long getReturnToDefaultPositionAnimationDuration() {
        return mReturnToDefaultPositionAnimationDuration;
    }

    /**
     * Sets the duration of the "return to default position" animation
     *
     * @param duration Duration of the "return to default position" animation in milliseconds
     */
    public void setReturnToDefaultPositionAnimationDuration(long duration) {
        mReturnToDefaultPositionAnimationDuration = duration;
    }

    /**
     * Gets the duration of the "move to outside of the window" animation
     *
     * @return Duration of the "move to outside of the window" animation in milliseconds
     */
    public long getMoveToOutsideWindowAnimationDuration() {
        return mMoveToOutsideWindowAnimationDuration;
    }

    /**
     * Sets the duration of the "move to outside of the window" animation
     *
     * @param duration Duration of the "move to outside of the window" animation in milliseconds
     */
    public void setMoveToOutsideWindowAnimationDuration(long duration) {
        mMoveToOutsideWindowAnimationDuration = duration;
    }

    /**
     * Gets OnItemSwipeEventListener listener
     *
     * @return The listener object
     */
    public @Nullable OnItemSwipeEventListener getOnItemSwipeEventListener() {
        return mItemSwipeEventListener;
    }

    /**
     * Sets OnItemSwipeEventListener listener
     *
     * @param listener The listener object
     */
    public void setOnItemSwipeEventListener(@Nullable OnItemSwipeEventListener listener) {
        mItemSwipeEventListener = listener;
    }

    /*package*/ boolean swipeHorizontal() {
        return mSwipeHorizontal;
    }

    /*package*/ void applySlideItem(RecyclerView.ViewHolder holder, int itemPosition, float prevAmount,
            float amount, boolean horizontal, boolean shouldAnimate, boolean isSwiping) {
        final SwipeableItemViewHolder holder2 = (SwipeableItemViewHolder) holder;
        final View containerView = holder2.getSwipeableContainerView();

        if (containerView == null) {
            return;
        }

        final int reqBackgroundType;

        if (amount == 0.0f) {
            if (prevAmount == 0.0f) {
                reqBackgroundType = DRAWABLE_SWIPE_NEUTRAL_BACKGROUND;
            } else {
                reqBackgroundType = determineBackgroundType(prevAmount, horizontal);
            }
        } else {
            reqBackgroundType = determineBackgroundType(amount, horizontal);
        }

        if (amount == 0.0f) {
            slideItem(holder, amount, horizontal, shouldAnimate);
            mAdapter.onUpdateSlideAmount(holder, itemPosition, horizontal, amount, isSwiping, reqBackgroundType);
        } else {
            float adjustedAmount = amount;
            float minLimit = horizontal ? holder2.getMaxLeftSwipeAmount() : holder2.getMaxUpSwipeAmount();
            float maxLimit = horizontal ? holder2.getMaxRightSwipeAmount() : holder2.getMaxDownSwipeAmount();

            adjustedAmount = Math.max(adjustedAmount, minLimit);
            adjustedAmount = Math.min(adjustedAmount, maxLimit);

            mAdapter.onUpdateSlideAmount(holder, itemPosition, horizontal, amount, isSwiping, reqBackgroundType);
            slideItem(holder, adjustedAmount, horizontal, shouldAnimate);
        }
    }

    private static int determineBackgroundType(float amount, boolean horizontal) {
        if (horizontal) {
            return (amount < 0) ? DRAWABLE_SWIPE_LEFT_BACKGROUND : DRAWABLE_SWIPE_RIGHT_BACKGROUND;
        } else {
            return (amount < 0) ? DRAWABLE_SWIPE_UP_BACKGROUND : DRAWABLE_SWIPE_DOWN_BACKGROUND;
        }
    }

    /*package*/ void cancelPendingAnimations(RecyclerView.ViewHolder holder) {
        if (mItemSlideAnimator != null) {
            mItemSlideAnimator.endAnimation(holder);
        }
    }

    /*package*/ int getSwipeContainerViewTranslationX(RecyclerView.ViewHolder holder) {
        return mItemSlideAnimator.getSwipeContainerViewTranslationX(holder);
    }

    /*package*/ int getSwipeContainerViewTranslationY(RecyclerView.ViewHolder holder) {
        return mItemSlideAnimator.getSwipeContainerViewTranslationY(holder);
    }

    /*package*/ void handleOnLongPress(MotionEvent e) {
        RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForItemId(mCheckingTouchSlop);
        if (holder != null) {
            checkConditionAndStartSwiping(e, holder);
        }
    }

    private static boolean supportsViewPropertyAnimator() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
    }

    private static class InternalHandler extends Handler {
        private static final int MSG_LONGPRESS = 1;
        private static final int MSG_DEFERRED_CANCEL_SWIPE = 2;

        private RecyclerViewSwipeManager mHolder;
        private MotionEvent mDownMotionEvent;

        public InternalHandler(RecyclerViewSwipeManager holder) {
            mHolder = holder;
        }

        public void release() {
            removeCallbacks(null);
            mHolder = null;
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_LONGPRESS:
                mHolder.handleOnLongPress(mDownMotionEvent);
                break;
            case MSG_DEFERRED_CANCEL_SWIPE:
                mHolder.cancelSwipe(true);
                break;
            }
        }

        public void startLongPressDetection(MotionEvent e, int timeout) {
            cancelLongPressDetection();
            mDownMotionEvent = MotionEvent.obtain(e);
            sendEmptyMessageAtTime(MSG_LONGPRESS, e.getDownTime() + timeout);
        }

        public void cancelLongPressDetection() {
            removeMessages(MSG_LONGPRESS);
            if (mDownMotionEvent != null) {
                mDownMotionEvent.recycle();
                mDownMotionEvent = null;
            }
        }

        public void removeDeferredCancelSwipeRequest() {
            removeMessages(MSG_DEFERRED_CANCEL_SWIPE);
        }

        public void requestDeferredCancelSwipe() {
            if (isCancelSwipeRequested()) {
                return;
            }
            sendEmptyMessage(MSG_DEFERRED_CANCEL_SWIPE);
        }

        public boolean isCancelSwipeRequested() {
            return hasMessages(MSG_DEFERRED_CANCEL_SWIPE);
        }
    }
}