Loading app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt +2 −2 Original line number Diff line number Diff line Loading @@ -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 } Loading app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt +71 −23 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 -> { Loading Loading @@ -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) Loading app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemAnimator.kt +16 −4 Original line number Diff line number Diff line Loading @@ -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) } } app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt +40 −12 Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -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) Loading @@ -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) { Loading Loading @@ -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) { Loading ui-utils/ItemTouchHelper/src/main/java/app/k9mail/ui/utils/itemtouchhelper/ItemTouchHelper.java +159 −58 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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, Loading Loading @@ -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 Loading Loading @@ -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); Loading Loading @@ -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 Loading @@ -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); } Loading @@ -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(); Loading Loading @@ -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); } Loading Loading @@ -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); } } Loading Loading @@ -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. Loading Loading @@ -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); } Loading Loading @@ -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) { } } /** Loading Loading @@ -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); } } } } } Loading
app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt +2 −2 Original line number Diff line number Diff line Loading @@ -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 } Loading
app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt +71 −23 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 -> { Loading Loading @@ -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) Loading
app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemAnimator.kt +16 −4 Original line number Diff line number Diff line Loading @@ -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) } }
app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt +40 −12 Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -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) Loading @@ -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) { Loading Loading @@ -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) { Loading
ui-utils/ItemTouchHelper/src/main/java/app/k9mail/ui/utils/itemtouchhelper/ItemTouchHelper.java +159 −58 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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, Loading Loading @@ -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 Loading Loading @@ -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); Loading Loading @@ -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 Loading @@ -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); } Loading @@ -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(); Loading Loading @@ -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); } Loading Loading @@ -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); } } Loading Loading @@ -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. Loading Loading @@ -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); } Loading Loading @@ -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) { } } /** Loading Loading @@ -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); } } } } }