From e5b628b467fe6e9cd686699b8803ffd92386155b Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 5 Nov 2021 14:59:28 -0400 Subject: [PATCH] Improve the archive animation. --- .../ConversationListArchiveFragment.java | 2 + .../ConversationListArchiveItemDecoration.kt | 87 +++++++++++++++++++ .../ConversationListFragment.java | 29 ++++--- .../ConversationListItemAnimator.java | 2 +- app/src/main/res/values/colors.xml | 2 + 5 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveItemDecoration.kt rename app/src/main/java/org/thoughtcrime/securesms/{components/recyclerview => conversationlist}/ConversationListItemAnimator.java (86%) 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 d92bb03134..eeabb5ac38 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java @@ -124,6 +124,8 @@ public class ConversationListArchiveFragment extends ConversationListFragment im @SuppressLint("StaticFieldLeak") @Override protected void onItemSwiped(long threadId, int unreadCount) { + archiveDecoration.onArchiveStarted(); + new SnackbarAsyncTask(getViewLifecycleOwner().getLifecycle(), requireView(), getResources().getQuantityString(R.plurals.ConversationListFragment_moved_conversations_to_inbox, 1, 1), diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveItemDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveItemDecoration.kt new file mode 100644 index 0000000000..0b386b942a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveItemDecoration.kt @@ -0,0 +1,87 @@ +package org.thoughtcrime.securesms.conversationlist + +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +/** + * When an item is removed, the gap is held by the items below the space being translated down, OR the items above the space being translated up, OR both. + * Then the items are animated to close the gap. + * + * So what we want to do is find that gap and fill it with color to give the illusion of the archived row being covered up. + * + * We want to be careful to only draw this for removals due to archiving, and we also don't want to screw up interactions with the pinned chat headers. + */ +class ConversationListArchiveItemDecoration(val background: Drawable) : RecyclerView.ItemDecoration() { + + private var archiveTriggered: Boolean = false + private var archiveAnimationStarted: Boolean = false + + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + if (!archiveTriggered) { + return + } + + if (parent.isAnimating) { + archiveAnimationStarted = true + } else if (archiveAnimationStarted) { + archiveTriggered = false + archiveAnimationStarted = false + return + } + + val childCount = parent.layoutManager?.childCount ?: 0 + + var lastViewComingDown: View? = null + var firstViewComingUp: View? = null + + for (i in 0 until childCount) { + val child: View? = parent.layoutManager?.getChildAt(i) + val childHolder: RecyclerView.ViewHolder? = if (child != null) parent.getChildViewHolder(child) else null + + if (child != null && childHolder != null) { + if (child.translationY < 0) { + lastViewComingDown = child + } else if (child.translationY > 0 && firstViewComingUp == null) { + firstViewComingUp = child + } + } + } + + var top = 0 + var bottom = 0 + + if (lastViewComingDown != null && firstViewComingUp != null) { + top = lastViewComingDown.bottom + lastViewComingDown.translationY.toInt() + bottom = firstViewComingUp.top + firstViewComingUp.translationY.toInt() + } else if (lastViewComingDown != null) { + top = lastViewComingDown.bottom + lastViewComingDown.translationY.toInt() + bottom = lastViewComingDown.bottom + } else if (firstViewComingUp != null) { + top = firstViewComingUp.top + bottom = firstViewComingUp.top + firstViewComingUp.translationY.toInt() + } + + val gapHeight = bottom - top + val singleItemHeight = when { + firstViewComingUp != null -> firstViewComingUp.height + lastViewComingDown != null -> lastViewComingDown.height + else -> 0 + } + + // A bit unscientific, but this gives us the behavior we want around archiving things in the pinned chat section + if (gapHeight > singleItemHeight * 2) { + archiveTriggered = false + archiveAnimationStarted = false + return + } + + background.setBounds(0, top, parent.width, bottom) + background.draw(canvas) + } + + fun onArchiveStarted() { + archiveTriggered = true + } +} 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 b09a8c9185..c26d86d9e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -44,6 +44,7 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.IdRes; import androidx.annotation.NonNull; @@ -90,7 +91,6 @@ import org.thoughtcrime.securesms.components.UnreadPaymentsView; import org.thoughtcrime.securesms.components.menu.ActionItem; import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar; import org.thoughtcrime.securesms.components.menu.SignalContextMenu; -import org.thoughtcrime.securesms.components.recyclerview.ConversationListItemAnimator; import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton; import org.thoughtcrime.securesms.components.reminder.DozeReminder; import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder; @@ -212,8 +212,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode private VoiceNotePlayerView voiceNotePlayerView; private SignalBottomActionBar bottomActionBar; - - private Stopwatch startupStopwatch; + protected ConversationListArchiveItemDecoration archiveDecoration; + private Stopwatch startupStopwatch; public static ConversationListFragment newInstance() { return new ConversationListFragment(); @@ -270,13 +270,17 @@ public class ConversationListFragment extends MainFragment implements ActionMode fab.show(); cameraFab.show(); + archiveDecoration = new ConversationListArchiveItemDecoration(new ColorDrawable(getResources().getColor(R.color.conversation_list_archive_background_end))); + list.setLayoutManager(new LinearLayoutManager(requireActivity())); list.setItemAnimator(new ConversationListItemAnimator()); list.addOnScrollListener(new ScrollListener()); + list.addItemDecoration(archiveDecoration); snapToTopDataObserver = new SnapToTopDataObserver(list); - new ItemTouchHelper(new ArchiveListenerCallback()).attachToRecyclerView(list); + new ItemTouchHelper(new ArchiveListenerCallback(getResources().getColor(R.color.conversation_list_archive_background_start), + getResources().getColor(R.color.conversation_list_archive_background_end))).attachToRecyclerView(list); fab.setOnClickListener(v -> startActivity(new Intent(getActivity(), NewConversationActivity.class))); cameraFab.setOnClickListener(v -> { @@ -1267,6 +1271,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode @SuppressLint("StaticFieldLeak") protected void onItemSwiped(long threadId, int unreadCount) { + archiveDecoration.onArchiveStarted(); + new SnackbarAsyncTask(getViewLifecycleOwner().getLifecycle(), requireView(), getResources().getQuantityString(R.plurals.ConversationListFragment_conversations_archived, 1, 1), @@ -1275,7 +1281,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode Snackbar.LENGTH_LONG, false) { - private final ThreadDatabase threadDatabase= DatabaseFactory.getThreadDatabase(getActivity()); + private final ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(getActivity()); private List pinnedThreadIds; @@ -1348,12 +1354,15 @@ public class ConversationListFragment extends MainFragment implements ActionMode private class ArchiveListenerCallback extends ItemTouchHelper.SimpleCallback { - private static final int ARCHIVE_SWIPE_START_COLOR = 0xFF28782A; - private static final int ARCHIVE_SWIPE_END_COLOR = 0xFF329635; - private static final float MIN_ICON_SCALE = 0.75f; + private static final float MIN_ICON_SCALE = 0.75f; - ArchiveListenerCallback() { + private final int archiveColorStart; + private final int archiveColorEnd; + + ArchiveListenerCallback(@ColorInt int archiveColorStart, @ColorInt int archiveColorEnd) { super(0, ItemTouchHelper.RIGHT); + this.archiveColorStart = archiveColorStart; + this.archiveColorEnd = archiveColorEnd; } @Override @@ -1399,7 +1408,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode Resources resources = getResources(); View itemView = viewHolder.itemView; float percentDx = Math.abs(dX) / viewHolder.itemView.getWidth(); - int color = ArgbEvaluatorCompat.getInstance().evaluate(Math.min(1f, percentDx * (1 / 0.25f)), ARCHIVE_SWIPE_START_COLOR, ARCHIVE_SWIPE_END_COLOR); + 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(112f); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/recyclerview/ConversationListItemAnimator.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemAnimator.java similarity index 86% rename from app/src/main/java/org/thoughtcrime/securesms/components/recyclerview/ConversationListItemAnimator.java rename to app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemAnimator.java index 1438d3e664..ab2500f7b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/recyclerview/ConversationListItemAnimator.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItemAnimator.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.components.recyclerview; +package org.thoughtcrime.securesms.conversationlist; import androidx.recyclerview.widget.DefaultItemAnimator; diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e7c9844395..17c46b6049 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -57,4 +57,6 @@ #ff9BCFBD @color/core_ultramarine #ffA23474 + #ff28782A + #ff329635