Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 273d0b43 authored by cketti's avatar cketti
Browse files

Handle animating a swiped view back to its start position inside `ItemTouchHelper`

parent e5f57441
Loading
Loading
Loading
Loading
+16 −4
Original line number Diff line number Diff line
@@ -9,9 +9,21 @@ class MessageListItemAnimator : DefaultItemAnimator() {
        changeDuration = 120
    }

    override fun canReuseUpdatedViewHolder(viewHolder: ViewHolder, payloads: MutableList<Any>): Boolean {
        // ItemTouchHelper expects swiped views to be removed from the view hierarchy. So we don't reuse views that are
        // marked as having been swiped.
        return !viewHolder.wasSwiped && super.canReuseUpdatedViewHolder(viewHolder, payloads)
    override fun animateChange(
        oldHolder: ViewHolder,
        newHolder: ViewHolder,
        fromX: Int,
        fromY: Int,
        toX: Int,
        toY: Int
    ): Boolean {
        if (oldHolder == newHolder && newHolder.wasSwiped) {
            // Don't touch views currently being swiped
            dispatchChangeFinished(oldHolder, true)
            dispatchChangeFinished(newHolder, false)
            return false
        }

        return super.animateChange(oldHolder, newHolder, fromX, fromY, toX, toY)
    }
}
+7 −9
Original line number Diff line number Diff line
@@ -72,8 +72,7 @@ class MessageListSwipeCallback(
        val holder = viewHolder as MessageViewHolder
        val item = adapter.getItemById(holder.uniqueId) ?: error("Couldn't find MessageListItem")

        // ItemTouchHelper expects swiped views to be removed from the view hierarchy. We mark this ViewHolder so that
        // MessageListItemAnimator knows not to reuse it during an animation.
        // Mark view to prevent MessageListItemAnimator from interfering with swipe animations
        viewHolder.markAsSwiped(true)

        when (direction) {
@@ -99,26 +98,25 @@ class MessageListSwipeCallback(
        dX: Float,
        dY: Float,
        actionState: Int,
        isCurrentlyActive: Boolean
        isCurrentlyActive: Boolean,
        success: Boolean
    ) {
        val view = viewHolder.itemView
        val viewWidth = view.width
        val viewHeight = view.height

        val isViewAnimatingBack = !isCurrentlyActive

        if (dX != 0F) {
            canvas.withTranslation(x = view.left.toFloat(), y = view.top.toFloat()) {
                if (isViewAnimatingBack) {
                    drawBackground(dX, viewWidth, viewHeight)
                } else {
                if (isCurrentlyActive || !success) {
                    val holder = viewHolder as MessageViewHolder
                    drawLayout(dX, viewWidth, viewHeight, holder)
                } else {
                    drawBackground(dX, viewWidth, viewHeight)
                }
            }
        }

        super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
        super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive, success)
    }

    private fun Canvas.drawBackground(dX: Float, width: Int, height: Int) {
+127 −57
Original line number Diff line number Diff line
@@ -615,75 +615,68 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration
                final float currentTranslateX = limitDeltaX(mRecyclerView, mTmpPosition[0]);
                final float currentTranslateY = mTmpPosition[1];

                // find where we should animate to
                final float targetTranslateX, targetTranslateY;
                int animationType;
                final int animationType;
                if (prevActionState == ACTION_STATE_DRAG) {
                    animationType = ANIMATION_TYPE_DRAG;
                } else if (swipeDir > 0) {
                    animationType = ANIMATION_TYPE_SWIPE_SUCCESS;
                } else {
                    animationType = ANIMATION_TYPE_SWIPE_CANCEL;
                }

                final RecoverAnimation animation;
                final boolean useDefaultDuration;
                switch (swipeDir) {
                    case LEFT:
                    case RIGHT:
                    case START:
                    case END:
                        if (mCallback.shouldAnimateOut(swipeDir)) {
                            targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();
                            float targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();

                            animation = new MoveOutAnimation(prevSelected, animationType, prevActionState,
                                    currentTranslateX, currentTranslateY, targetTranslateX, currentTranslateY, swipeDir,
                                    /* moveBackAfterwards */ false);

                            useDefaultDuration = true;
                        } else if (wasFling) {
                            int maxSwipeDistance = mCallback.getMaxSwipeDistance(mRecyclerView, swipeDir);
                            targetTranslateX = Math.signum(mDx) * maxSwipeDistance;
                            float targetTranslateX = Math.signum(mDx) * maxSwipeDistance;

                            animation = new MoveOutAnimation(prevSelected, animationType, prevActionState,
                                    currentTranslateX, currentTranslateY, targetTranslateX, currentTranslateY, swipeDir,
                                    /* moveBackAfterwards */ true);

                            useDefaultDuration = true;
                        } else {
                            targetTranslateX = currentTranslateX;
                            // This is a dummy animation to ensure mCallback.onChildDraw() calls will be made even if
                            // the animating back part is delayed.
                            animation = new MoveOutAnimation(prevSelected, animationType, prevActionState,
                                    currentTranslateX, currentTranslateY, currentTranslateX, currentTranslateY,
                                    swipeDir, /* moveBackAfterwards */ true);

                            animation.setDuration(0);
                            useDefaultDuration = false;
                        }
                        targetTranslateY = 0;
                        break;
                    case UP:
                    case DOWN:
                        targetTranslateX = 0;
                        targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight();
                        break;
                        throw new UnsupportedOperationException();
                    default:
                        targetTranslateX = 0;
                        targetTranslateY = 0;
                }
                if (prevActionState == ACTION_STATE_DRAG) {
                    animationType = ANIMATION_TYPE_DRAG;
                } else if (swipeDir > 0) {
                    animationType = ANIMATION_TYPE_SWIPE_SUCCESS;
                } else {
                    animationType = ANIMATION_TYPE_SWIPE_CANCEL;
                        animation = new MoveBackAnimation(prevSelected, animationType, prevActionState,
                                currentTranslateX, currentTranslateY);
                        useDefaultDuration = true;
                }

                final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType,
                        prevActionState, currentTranslateX, currentTranslateY,
                        targetTranslateX, targetTranslateY) {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        if (this.mOverridden) {
                            return;
                        }
                        if (swipeDir <= 0) {
                            // this is a drag or failed swipe. recover immediately
                            mCallback.clearView(mRecyclerView, prevSelected);
                            // full cleanup will happen on onDrawOver
                        } else {
                            // wait until remove animation is complete.
                            mPendingCleanup.add(prevSelected.itemView);
                            mIsPendingCleanup = true;
                            if (swipeDir > 0) {
                                // Animation might be ended by other animators during a layout.
                                // We defer callback to avoid editing adapter during a layout.
                                postDispatchSwipe(this, swipeDir);
                if (useDefaultDuration) {
                    long duration = mCallback.getAnimationDuration(mRecyclerView, animationType,
                            animation.mTargetX - animation.mStartDx, animation.mTargetY - animation.mStartDy);
                    animation.setDuration(duration);
                }
                        }
                        // removed from the list after it is drawn for the last time
                        if (mOverdrawChild == prevSelected.itemView) {
                            removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
                        }
                    }
                };
                final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType,
                        targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY);
                rv.setDuration(duration);
                mRecoverAnimations.add(rv);
                rv.start();

                mRecoverAnimations.add(animation);
                animation.start();

                preventLayout = true;
            } else {
                removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
@@ -715,7 +708,7 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir) {
    void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir, final boolean moveBackAfterwards) {
        // wait until animations are complete.
        mRecyclerView.post(new Runnable() {
            @Override
@@ -731,6 +724,9 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration
                    if ((animator == null || !animator.isRunning(null))
                            && !hasRunningRecoverAnim()) {
                        mCallback.onSwiped(anim.mViewHolder, swipeDir);
                        if (moveBackAfterwards) {
                            startMoveBackAnimation(anim);
                        }
                    } else {
                        mRecyclerView.post(this);
                    }
@@ -739,6 +735,20 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration
        });
    }

    private void startMoveBackAnimation(RecoverAnimation animation) {
        MoveBackAnimation moveBackAnimation = new MoveBackAnimation(animation.mViewHolder, animation.mAnimationType,
                animation.mActionState, animation.mTargetX, animation.mTargetY);

        long duration = mCallback.getAnimationDuration(mRecyclerView, animation.mAnimationType,
                -animation.mTargetX, -animation.mTargetY);
        moveBackAnimation.setDuration(duration);

        mRecoverAnimations.remove(animation);
        mRecoverAnimations.add(moveBackAnimation);

        moveBackAnimation.start();
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    boolean hasRunningRecoverAnim() {
        final int size = mRecoverAnimations.size();
@@ -2005,13 +2015,15 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration
                final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i);
                anim.update();
                final int count = c.save();
                boolean isCurrentlyActive = anim instanceof MoveOutAnimation;
                boolean success = anim.mAnimationType == ANIMATION_TYPE_SWIPE_SUCCESS;
                onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState,
                        anim.mAnimationType == ANIMATION_TYPE_SWIPE_SUCCESS && !anim.mIsPendingCleanup);
                        isCurrentlyActive, success);
                c.restoreToCount(count);
            }
            if (selected != null) {
                final int count = c.save();
                onChildDraw(c, parent, selected, dX, dY, actionState, true);
                onChildDraw(c, parent, selected, dX, dY, actionState, true, false);
                c.restoreToCount(count);
            }
        }
@@ -2053,7 +2065,7 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration
         * This is a good place to clear all changes on the View that was done in
         * {@link #onSelectedChanged(RecyclerView.ViewHolder, int)},
         * {@link #onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int,
         * boolean)} or
         * boolean, boolean)} or
         * {@link #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, boolean)}.
         *
         * @param recyclerView The RecyclerView which is controlled by the ItemTouchHelper.
@@ -2092,7 +2104,7 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration
         */
        public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
                @NonNull ViewHolder viewHolder,
                float dX, float dY, int actionState, boolean isCurrentlyActive) {
                float dX, float dY, int actionState, boolean isCurrentlyActive, boolean success) {
            ItemTouchUIUtilImpl.INSTANCE.onDraw(c, recyclerView, viewHolder.itemView, dX, dY,
                    actionState, isCurrentlyActive);
        }
@@ -2526,4 +2538,62 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration

        }
    }

    private class MoveBackAnimation extends RecoverAnimation {
        MoveBackAnimation(ViewHolder viewHolder, int animationType, int actionState, float startDx, float startDy) {
            super(viewHolder, animationType, actionState, startDx, startDy, /* targetX */ 0, /* targetY */ 0);
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            if (this.mOverridden) {
                return;
            }

            mCallback.clearView(mRecyclerView, mViewHolder);
            // full cleanup will happen on onDrawOver

            // removed from the list after it is drawn for the last time
            if (mOverdrawChild == mViewHolder.itemView) {
                removeChildDrawingOrderCallbackIfNecessary(mViewHolder.itemView);
            }
        }
    }

    private class MoveOutAnimation extends RecoverAnimation {
        private final int mSwipeDirection;
        private final boolean mMoveBackAfterwards;

        MoveOutAnimation(ViewHolder viewHolder, int animationType, int actionState, float startDx, float startDy,
                float targetX, float targetY, int swipeDirection, boolean moveBackAfterwards) {
            super(viewHolder, animationType, actionState, startDx, startDy, targetX, targetY);
            this.mSwipeDirection = swipeDirection;
            this.mMoveBackAfterwards = moveBackAfterwards;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            if (this.mOverridden) {
                return;
            }

            if (!mMoveBackAfterwards) {
                mPendingCleanup.add(mViewHolder.itemView);
            }
            mIsPendingCleanup = true;

            // Animation might be ended by other animators during a layout.
            // We defer callback to avoid editing adapter during a layout.
            postDispatchSwipe(this, mSwipeDirection, mMoveBackAfterwards);

            if (!mMoveBackAfterwards) {
                // removed from the list after it is drawn for the last time
                if (mOverdrawChild == mViewHolder.itemView) {
                    removeChildDrawingOrderCallbackIfNecessary(mViewHolder.itemView);
                }
            }
        }
    }
}