From 273d0b433d7814ae45792b9629d05255e84bd6d0 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 10 Nov 2022 20:29:26 +0100 Subject: [PATCH 1/2] Handle animating a swiped view back to its start position inside `ItemTouchHelper` --- .../ui/messagelist/MessageListItemAnimator.kt | 20 +- .../messagelist/MessageListSwipeCallback.kt | 16 +- .../itemtouchhelper/ItemTouchHelper.java | 198 ++++++++++++------ 3 files changed, 157 insertions(+), 77 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemAnimator.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemAnimator.kt index ad9ec900c..af65ac4a7 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemAnimator.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemAnimator.kt @@ -9,9 +9,21 @@ class MessageListItemAnimator : DefaultItemAnimator() { changeDuration = 120 } - override fun canReuseUpdatedViewHolder(viewHolder: ViewHolder, payloads: MutableList): 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) } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt index a94bb7ad1..7ec398a4f 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt @@ -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) { diff --git a/ui-utils/ItemTouchHelper/src/main/java/app/k9mail/ui/utils/itemtouchhelper/ItemTouchHelper.java b/ui-utils/ItemTouchHelper/src/main/java/app/k9mail/ui/utils/itemtouchhelper/ItemTouchHelper.java index 9c6efeff2..bffd573c4 100644 --- a/ui-utils/ItemTouchHelper/src/main/java/app/k9mail/ui/utils/itemtouchhelper/ItemTouchHelper.java +++ b/ui-utils/ItemTouchHelper/src/main/java/app/k9mail/ui/utils/itemtouchhelper/ItemTouchHelper.java @@ -615,33 +615,7 @@ 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; - switch (swipeDir) { - case LEFT: - case RIGHT: - case START: - case END: - if (mCallback.shouldAnimateOut(swipeDir)) { - targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth(); - } else if (wasFling) { - int maxSwipeDistance = mCallback.getMaxSwipeDistance(mRecyclerView, swipeDir); - targetTranslateX = Math.signum(mDx) * maxSwipeDistance; - } else { - targetTranslateX = currentTranslateX; - } - targetTranslateY = 0; - break; - case UP: - case DOWN: - targetTranslateX = 0; - targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight(); - break; - default: - targetTranslateX = 0; - targetTranslateY = 0; - } + final int animationType; if (prevActionState == ACTION_STATE_DRAG) { animationType = ANIMATION_TYPE_DRAG; } else if (swipeDir > 0) { @@ -650,40 +624,59 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration animationType = ANIMATION_TYPE_SWIPE_CANCEL; } - 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 + final RecoverAnimation animation; + final boolean useDefaultDuration; + switch (swipeDir) { + case LEFT: + case RIGHT: + case START: + case END: + if (mCallback.shouldAnimateOut(swipeDir)) { + 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); + float targetTranslateX = Math.signum(mDx) * maxSwipeDistance; + + animation = new MoveOutAnimation(prevSelected, animationType, prevActionState, + currentTranslateX, currentTranslateY, targetTranslateX, currentTranslateY, swipeDir, + /* moveBackAfterwards */ true); + + useDefaultDuration = true; } 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); - } + // 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; } - // 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(); + break; + case UP: + case DOWN: + throw new UnsupportedOperationException(); + default: + animation = new MoveBackAnimation(prevSelected, animationType, prevActionState, + currentTranslateX, currentTranslateY); + useDefaultDuration = true; + } + + if (useDefaultDuration) { + long duration = mCallback.getAnimationDuration(mRecyclerView, animationType, + animation.mTargetX - animation.mStartDx, animation.mTargetY - animation.mStartDy); + animation.setDuration(duration); + } + + 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); + } + } + } + } } From 789fbe4d4349661ce68b7f2c22e9ceac00b1afa6 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 11 Nov 2022 14:00:11 +0100 Subject: [PATCH 2/2] Deselect message during swipe When swiping a selected message we remove the selection state at the start and restore it afterwards if the list item isn't removed. Except when the swipe action is "toggle selection". Then we keep the current selection state while the list item is dragged. --- .../k9/ui/messagelist/MessageListAdapter.kt | 4 +- .../k9/ui/messagelist/MessageListFragment.kt | 98 ++++++++++++++----- .../messagelist/MessageListSwipeCallback.kt | 36 ++++++- .../itemtouchhelper/ItemTouchHelper.java | 33 ++++++- 4 files changed, 140 insertions(+), 31 deletions(-) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt index 3a892bc58..b58281a03 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt @@ -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 } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt index a47113ead..f66cd1c51 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt @@ -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,36 +1451,67 @@ class MessageListFragment : private val isPullToRefreshAllowed: Boolean get() = isRemoteSearchAllowed || isCheckMailAllowed - private val swipeListener = MessageListSwipeListener { item, action -> - when (action) { - SwipeAction.None -> Unit - SwipeAction.ToggleSelection -> { - toggleMessageSelect(item) + 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) } - SwipeAction.ToggleRead -> { - setFlag(item, Flag.SEEN, !item.isRead) - } - SwipeAction.ToggleStar -> { - setFlag(item, Flag.FLAGGED, !item.isStarred) - } - SwipeAction.Archive -> { - onArchive(item.messageReference) - } - SwipeAction.Delete -> { - if (K9.isConfirmDelete) { - notifyItemChanged(item) + } + + override fun onSwipeActionChanged(item: MessageListItem, action: SwipeAction) { + if (action == SwipeAction.ToggleSelection) { + if (itemSelectedOnSwipeStart && !isMessageSelected(item)) { + selectMessage(item) } - onDelete(listOf(item.messageReference)) + } else if (isMessageSelected(item)) { + deselectMessage(item) } - SwipeAction.Spam -> { - if (K9.isConfirmSpam) { - notifyItemChanged(item) + } + + override fun onSwipeAction(item: MessageListItem, action: SwipeAction) { + if (action.removesItem || action == SwipeAction.ToggleSelection) { + itemSelectedOnSwipeStart = false + } + + when (action) { + SwipeAction.None -> Unit + SwipeAction.ToggleSelection -> { + toggleMessageSelect(item) + } + SwipeAction.ToggleRead -> { + setFlag(item, Flag.SEEN, !item.isRead) + } + SwipeAction.ToggleStar -> { + setFlag(item, Flag.FLAGGED, !item.isStarred) + } + SwipeAction.Archive -> { + onArchive(item.messageReference) + } + SwipeAction.Delete -> { + if (K9.isConfirmDelete) { + notifyItemChanged(item) + } + onDelete(listOf(item.messageReference)) + } + SwipeAction.Spam -> { + if (K9.isConfirmSpam) { + notifyItemChanged(item) + } + onSpam(listOf(item.messageReference)) + } + SwipeAction.Move -> { + notifyItemChanged(item) + onMove(item.messageReference) } - onSpam(listOf(item.messageReference)) } - SwipeAction.Move -> { - notifyItemChanged(item) - onMove(item.messageReference) + } + + override fun onSwipeEnded(item: MessageListItem) { + if (itemSelectedOnSwipeStart && !isMessageSelected(item)) { + selectMessage(item) } } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt index 7ec398a4f..fb3dbec14 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt @@ -68,9 +68,28 @@ 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 // Mark view to prevent MessageListItemAnimator from interfering with swipe animations viewHolder.markAsSwiped(true) @@ -82,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) @@ -206,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) { diff --git a/ui-utils/ItemTouchHelper/src/main/java/app/k9mail/ui/utils/itemtouchhelper/ItemTouchHelper.java b/ui-utils/ItemTouchHelper/src/main/java/app/k9mail/ui/utils/itemtouchhelper/ItemTouchHelper.java index bffd573c4..7e3115d70 100644 --- a/ui-utils/ItemTouchHelper/src/main/java/app/k9mail/ui/utils/itemtouchhelper/ItemTouchHelper.java +++ b/ui-utils/ItemTouchHelper/src/main/java/app/k9mail/ui/utils/itemtouchhelper/ItemTouchHelper.java @@ -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 @@ -726,6 +740,8 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration mCallback.onSwiped(anim.mViewHolder, swipeDir); if (moveBackAfterwards) { startMoveBackAnimation(anim); + } else { + mCallback.onSwipeEnded(anim.mViewHolder); } } else { mRecyclerView.post(this); @@ -1061,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); } @@ -2240,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) { + } } /** @@ -2551,6 +2580,8 @@ public class ItemTouchHelper extends RecyclerView.ItemDecoration return; } + mCallback.onSwipeEnded(mViewHolder); + mCallback.clearView(mRecyclerView, mViewHolder); // full cleanup will happen on onDrawOver