diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java index 552c178a9..8a8463801 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java @@ -125,6 +125,7 @@ public class ConversationListArchiveFragment extends ConversationListFragment im @Override protected void onItemSwiped(long threadId, int unreadCount) { archiveDecoration.onArchiveStarted(); + itemAnimator.enable(); new SnackbarAsyncTask(getViewLifecycleOwner().getLifecycle(), requireView(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index b13ad14ec..916eff64b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -157,6 +157,7 @@ import org.thoughtcrime.securesms.util.views.Stub; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -214,6 +215,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode private SignalBottomActionBar bottomActionBar; protected ConversationListArchiveItemDecoration archiveDecoration; + protected ConversationListItemAnimator itemAnimator; private Stopwatch startupStopwatch; public static ConversationListFragment newInstance() { @@ -272,9 +274,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode cameraFab.show(); archiveDecoration = new ConversationListArchiveItemDecoration(new ColorDrawable(getResources().getColor(R.color.conversation_list_archive_background_end))); + itemAnimator = new ConversationListItemAnimator(); list.setLayoutManager(new LinearLayoutManager(requireActivity())); - list.setItemAnimator(new ConversationListItemAnimator()); + list.setItemAnimator(itemAnimator); list.addOnScrollListener(new ScrollListener()); list.addItemDecoration(archiveDecoration); @@ -1276,6 +1279,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode @SuppressLint("StaticFieldLeak") protected void onItemSwiped(long threadId, int unreadCount) { archiveDecoration.onArchiveStarted(); + itemAnimator.enable(); new SnackbarAsyncTask(getViewLifecycleOwner().getLifecycle(), requireView(), @@ -1315,7 +1319,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode ApplicationDependencies.getMessageNotifier().updateNotification(context); } } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId); + }.executeOnExecutor(SignalExecutors.BOUNDED, threadId); } private class PaymentNotificationListener implements UnreadPaymentsView.Listener { @@ -1358,14 +1362,18 @@ public class ConversationListFragment extends MainFragment implements ActionMode private class ArchiveListenerCallback extends ItemTouchHelper.SimpleCallback { + private static final long SWIPE_ANIMATION_DURATION = 175; + private static final float MIN_ICON_SCALE = 0.85f; private static final float MAX_ICON_SCALE = 1f; private final int archiveColorStart; private final int archiveColorEnd; + private WeakReference lastTouched; + ArchiveListenerCallback(@ColorInt int archiveColorStart, @ColorInt int archiveColorEnd) { - super(0, ItemTouchHelper.RIGHT); + super(0, ItemTouchHelper.END); this.archiveColorStart = archiveColorStart; this.archiveColorEnd = archiveColorEnd; } @@ -1389,13 +1397,33 @@ public class ConversationListFragment extends MainFragment implements ActionMode return 0; } + lastTouched = new WeakReference<>(viewHolder); + return super.getSwipeDirs(recyclerView, viewHolder); } - @SuppressLint("StaticFieldLeak") @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { - if (viewHolder.itemView instanceof ConversationListItemInboxZero) return; + if (lastTouched != null) { + Log.w(TAG, "Falling back to slower onSwiped() event."); + onTrueSwipe(viewHolder); + lastTouched = null; + } + } + + @Override + public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, float animateDx, float animateDy) { + if (animationType == ItemTouchHelper.ANIMATION_TYPE_SWIPE_SUCCESS && lastTouched != null && lastTouched.get() != null) { + onTrueSwipe(lastTouched.get()); + lastTouched = null; + } else if (animationType == ItemTouchHelper.ANIMATION_TYPE_SWIPE_CANCEL) { + lastTouched = null; + } + + return SWIPE_ANIMATION_DURATION; + } + + private void onTrueSwipe(RecyclerView.ViewHolder viewHolder) { final long threadId = ((ConversationListItem)viewHolder.itemView).getThreadId(); final int unreadCount = ((ConversationListItem)viewHolder.itemView).getUnreadCount(); @@ -1409,24 +1437,26 @@ public class ConversationListFragment extends MainFragment implements ActionMode boolean isCurrentlyActive) { if (viewHolder.itemView instanceof ConversationListItemInboxZero) return; + float absoluteDx = Math.abs(dX); + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { Resources resources = getResources(); View itemView = viewHolder.itemView; - float percentDx = Math.abs(dX) / viewHolder.itemView.getWidth(); + float percentDx = absoluteDx / viewHolder.itemView.getWidth(); int color = ArgbEvaluatorCompat.getInstance().evaluate(Math.min(1f, percentDx * (1 / 0.25f)), archiveColorStart, archiveColorEnd); float scaleStartPoint = DimensionUnit.DP.toPixels(48f); float scaleEndPoint = DimensionUnit.DP.toPixels(96f); float scale; - if (dX < scaleStartPoint) { + if (absoluteDx < scaleStartPoint) { scale = MIN_ICON_SCALE; - } else if (dX > scaleEndPoint) { + } else if (absoluteDx > scaleEndPoint) { scale = MAX_ICON_SCALE; } else { - scale = Math.min(MAX_ICON_SCALE, MIN_ICON_SCALE + ((dX - scaleStartPoint) / (scaleEndPoint - scaleStartPoint)) * (MAX_ICON_SCALE - MIN_ICON_SCALE)); + scale = Math.min(MAX_ICON_SCALE, MIN_ICON_SCALE + ((absoluteDx - scaleStartPoint) / (scaleEndPoint - scaleStartPoint)) * (MAX_ICON_SCALE - MIN_ICON_SCALE)); } - if (dX > 0) { + if (absoluteDx > 0) { if (archiveDrawable == null) { archiveDrawable = Objects.requireNonNull(AppCompatResources.getDrawable(requireContext(), getArchiveIconRes())); archiveDrawable.setColorFilter(new SimpleColorFilter(Color.WHITE)); @@ -1441,8 +1471,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode float gutter = resources.getDimension(R.dimen.dsl_settings_gutter); float extra = resources.getDimension(R.dimen.conversation_list_fragment_archive_padding); - canvas.translate(itemView.getLeft() + gutter + extra, - itemView.getTop() + (itemView.getBottom() - itemView.getTop() - archiveDrawable.getIntrinsicHeight()) / 2f); + if (ViewUtil.isLtr(requireContext())) { + canvas.translate(itemView.getLeft() + gutter + extra, + itemView.getTop() + (itemView.getBottom() - itemView.getTop() - archiveDrawable.getIntrinsicHeight()) / 2f); + } else { + canvas.translate(itemView.getRight() - gutter - extra, + itemView.getTop() + (itemView.getBottom() - itemView.getTop() - archiveDrawable.getIntrinsicHeight()) / 2f); + } canvas.scale(scale, scale, archiveDrawable.getIntrinsicWidth() / 2f, archiveDrawable.getIntrinsicHeight() / 2f); @@ -1450,7 +1485,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode canvas.restore(); ViewCompat.setElevation(viewHolder.itemView, DimensionUnit.DP.toPixels(4f)); - } else if (dX == 0) { + } else if (absoluteDx == 0) { ViewCompat.setElevation(viewHolder.itemView, DimensionUnit.DP.toPixels(0f)); } @@ -1464,6 +1499,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { super.clearView(recyclerView, viewHolder); ViewCompat.setElevation(viewHolder.itemView, 0); + lastTouched = null; + itemAnimator.postDisable(requireView().getHandler()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemAnimator.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemAnimator.java index ab2500f7b..2cb2b259e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemAnimator.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemAnimator.java @@ -1,16 +1,42 @@ package org.thoughtcrime.securesms.conversationlist; +import android.os.Handler; +import androidx.annotation.MainThread; import androidx.recyclerview.widget.DefaultItemAnimator; -import androidx.recyclerview.widget.RecyclerView; + +import org.signal.core.util.logging.Log; public class ConversationListItemAnimator extends DefaultItemAnimator { + private static final String TAG = Log.tag(ConversationListItemAnimator.class); + + private static final long ANIMATION_DURATION = 200; + + private boolean shouldDisable; + public ConversationListItemAnimator() { setSupportsChangeAnimations(false); } - @Override public boolean animateRemove(RecyclerView.ViewHolder holder) { - return super.animateRemove(holder); + @MainThread + public void enable() { + setMoveDuration(ANIMATION_DURATION); + shouldDisable = false; + } + + /** + * We need to reasonable ensure that the animation has started before we disable things here, so we add a slight delay. + */ + @MainThread + public void postDisable(Handler handler) { + shouldDisable = true; + handler.postDelayed(() -> { + if (shouldDisable) { + setMoveDuration(0); + } else { + Log.w(TAG, "Disable was canceled by an enable."); + } + }, 50); } } diff --git a/app/src/main/res/drawable-ldrtl/conversation_list_item_background_default.xml b/app/src/main/res/drawable-ldrtl/conversation_list_item_background_default.xml new file mode 100644 index 000000000..590379445 --- /dev/null +++ b/app/src/main/res/drawable-ldrtl/conversation_list_item_background_default.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/conversation_list_item_background_default.xml b/app/src/main/res/drawable/conversation_list_item_background_default.xml index 1fbe47de1..57f5d988a 100644 --- a/app/src/main/res/drawable/conversation_list_item_background_default.xml +++ b/app/src/main/res/drawable/conversation_list_item_background_default.xml @@ -1,6 +1,8 @@ - + \ No newline at end of file