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

Unverified Commit 23b68555 authored by cketti's avatar cketti Committed by GitHub
Browse files

Merge pull request #6470 from thundernest/swipe_select_state

Deselect message during swipe
parents e5f57441 789fbe4d
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -480,11 +480,11 @@ class MessageListAdapter internal constructor(
        }
    }

    private fun selectMessage(item: MessageListItem) {
    fun selectMessage(item: MessageListItem) {
        selected = selected + item.uniqueId
    }

    private fun deselectMessage(item: MessageListItem) {
    fun deselectMessage(item: MessageListItem) {
        selected = selected - item.uniqueId
    }

+71 −23
Original line number Diff line number Diff line
@@ -810,7 +810,24 @@ class MessageListFragment :

    private fun toggleMessageSelect(messageListItem: MessageListItem) {
        adapter.toggleSelection(messageListItem)
        updateAfterSelectionChange()
    }

    private fun selectMessage(messageListItem: MessageListItem) {
        adapter.selectMessage(messageListItem)
        updateAfterSelectionChange()
    }

    private fun deselectMessage(messageListItem: MessageListItem) {
        adapter.deselectMessage(messageListItem)
        updateAfterSelectionChange()
    }

    private fun isMessageSelected(messageListItem: MessageListItem): Boolean {
        return adapter.isSelected(messageListItem)
    }

    private fun updateAfterSelectionChange() {
        if (adapter.selectedCount == 0) {
            actionMode?.finish()
            actionMode = null
@@ -1434,7 +1451,31 @@ class MessageListFragment :
    private val isPullToRefreshAllowed: Boolean
        get() = isRemoteSearchAllowed || isCheckMailAllowed

    private val swipeListener = MessageListSwipeListener { item, action ->
    private var itemSelectedOnSwipeStart = false

    private val swipeListener = object : MessageListSwipeListener {
        override fun onSwipeStarted(item: MessageListItem, action: SwipeAction) {
            itemSelectedOnSwipeStart = isMessageSelected(item)
            if (itemSelectedOnSwipeStart && action != SwipeAction.ToggleSelection) {
                deselectMessage(item)
            }
        }

        override fun onSwipeActionChanged(item: MessageListItem, action: SwipeAction) {
            if (action == SwipeAction.ToggleSelection) {
                if (itemSelectedOnSwipeStart && !isMessageSelected(item)) {
                    selectMessage(item)
                }
            } else if (isMessageSelected(item)) {
                deselectMessage(item)
            }
        }

        override fun onSwipeAction(item: MessageListItem, action: SwipeAction) {
            if (action.removesItem || action == SwipeAction.ToggleSelection) {
                itemSelectedOnSwipeStart = false
            }

            when (action) {
                SwipeAction.None -> Unit
                SwipeAction.ToggleSelection -> {
@@ -1468,6 +1509,13 @@ class MessageListFragment :
            }
        }

        override fun onSwipeEnded(item: MessageListItem) {
            if (itemSelectedOnSwipeStart && !isMessageSelected(item)) {
                selectMessage(item)
            }
        }
    }

    private fun notifyItemChanged(item: MessageListItem) {
        val position = adapter.getPosition(item) ?: return
        adapter.notifyItemChanged(position)
+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)
    }
}
+40 −12
Original line number Diff line number Diff line
@@ -68,12 +68,30 @@ class MessageListSwipeCallback(
        throw UnsupportedOperationException("not implemented")
    }

    override fun onSwipeStarted(viewHolder: ViewHolder, direction: Int) {
        val swipeAction = when (direction) {
            ItemTouchHelper.RIGHT -> swipeRightAction
            ItemTouchHelper.LEFT -> swipeLeftAction
            else -> error("Unsupported direction: $direction")
        }

        listener.onSwipeStarted(viewHolder.messageListItem, swipeAction)
    }

    override fun onSwipeDirectionChanged(viewHolder: ViewHolder, direction: Int) {
        val swipeAction = when (direction) {
            ItemTouchHelper.RIGHT -> swipeRightAction
            ItemTouchHelper.LEFT -> swipeLeftAction
            else -> error("Unsupported direction: $direction")
        }

        listener.onSwipeActionChanged(viewHolder.messageListItem, swipeAction)
    }

    override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
        val holder = viewHolder as MessageViewHolder
        val item = adapter.getItemById(holder.uniqueId) ?: error("Couldn't find MessageListItem")
        val item = viewHolder.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) {
@@ -83,6 +101,10 @@ class MessageListSwipeCallback(
        }
    }

    override fun onSwipeEnded(viewHolder: ViewHolder) {
        listener.onSwipeEnded(viewHolder.messageListItem)
    }

    override fun clearView(recyclerView: RecyclerView, viewHolder: ViewHolder) {
        super.clearView(recyclerView, viewHolder)
        viewHolder.markAsSwiped(false)
@@ -99,26 +121,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) {
@@ -208,14 +229,21 @@ class MessageListSwipeCallback(
        val percentage = abs(animateDx) / recyclerView.width
        return (super.getAnimationDuration(recyclerView, animationType, animateDx, animateDy) * percentage).toLong()
    }

    private val ViewHolder.messageListItem: MessageListItem
        get() = (this as? MessageViewHolder)?.uniqueId?.let { adapter.getItemById(it) }
            ?: error("Couldn't find MessageListItem")
}

fun interface SwipeActionSupportProvider {
    fun isActionSupported(item: MessageListItem, action: SwipeAction): Boolean
}

fun interface MessageListSwipeListener {
interface MessageListSwipeListener {
    fun onSwipeStarted(item: MessageListItem, action: SwipeAction)
    fun onSwipeActionChanged(item: MessageListItem, action: SwipeAction)
    fun onSwipeAction(item: MessageListItem, action: SwipeAction)
    fun onSwipeEnded(item: MessageListItem)
}

private fun ViewHolder.markAsSwiped(value: Boolean) {
+159 −58
Original line number Diff line number Diff line
@@ -193,6 +193,11 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration

    float mDy;

    /**
     * Current swipe direction. Used for {@link Callback#onSwipeDirectionChanged(ViewHolder, int)}
     */
    private int mSwipeDirection = 0;

    /**
     * The coordinates of the selected view at the time it is selected. We record these values
     * when action starts so that we can consistently position it even if LayoutManager moves the
@@ -554,6 +559,14 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration
            if ((mSelectedFlags & (LEFT | RIGHT)) != 0 && dx != 0) {
                dx = limitDeltaX(parent, dx);
            }

            if (dx != 0) {
                int direction = dx > 0 ? RIGHT : LEFT;
                if (direction != mSwipeDirection) {
                    mSwipeDirection = direction;
                    mCallback.onSwipeDirectionChanged(mSelected, direction);
                }
            }
        }

        mCallback.onDraw(c, parent, mSelected,
@@ -582,6 +595,7 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration
        if (selected == mSelected && actionState == mActionState) {
            return;
        }

        mDragScrollStartTimeInMs = Long.MIN_VALUE;
        final int prevActionState = mActionState;
        // prevent duplicate animations
@@ -615,75 +629,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 +722,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 +738,11 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration
                    if ((animator == null || !animator.isRunning(null))
                            && !hasRunningRecoverAnim()) {
                        mCallback.onSwiped(anim.mViewHolder, swipeDir);
                        if (moveBackAfterwards) {
                            startMoveBackAnimation(anim);
                        } else {
                            mCallback.onSwipeEnded(anim.mViewHolder);
                        }
                    } else {
                        mRecyclerView.post(this);
                    }
@@ -739,6 +751,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();
@@ -1051,6 +1077,10 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration
        }
        mDx = mDy = 0f;
        mActivePointerId = motionEvent.getPointerId(0);

        mSwipeDirection = dx > 0 ? RIGHT : LEFT;
        mCallback.onSwipeStarted(vh, mSwipeDirection);

        select(vh, ACTION_STATE_SWIPE);
    }

@@ -2005,13 +2035,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 +2085,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 +2124,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);
        }
@@ -2228,9 +2260,18 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration
         * @param direction The swipe direction.
         * @return The maximum distance in pixels that a view can be moved during a swipe.
         */
        public int getMaxSwipeDistance(RecyclerView recyclerView, int direction) {
        public int getMaxSwipeDistance(@NonNull RecyclerView recyclerView, int direction) {
            return recyclerView.getWidth();
        }

        public void onSwipeStarted(@NonNull ViewHolder viewHolder, int direction) {
        }

        public void onSwipeDirectionChanged(@NonNull ViewHolder viewHolder, int direction) {
        }

        public void onSwipeEnded(@NonNull ViewHolder viewHolder) {
        }
    }

    /**
@@ -2526,4 +2567,64 @@ 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.onSwipeEnded(mViewHolder);

            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);
                }
            }
        }
    }
}