mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 10:17:56 +00:00
Improve the archive animation.
This commit is contained in:
@@ -124,6 +124,8 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
protected void onItemSwiped(long threadId, int unreadCount) {
|
||||
archiveDecoration.onArchiveStarted();
|
||||
|
||||
new SnackbarAsyncTask<Long>(getViewLifecycleOwner().getLifecycle(),
|
||||
requireView(),
|
||||
getResources().getQuantityString(R.plurals.ConversationListFragment_moved_conversations_to_inbox, 1, 1),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<Long>(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<Long> 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);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.components.recyclerview;
|
||||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
@@ -57,4 +57,6 @@
|
||||
<color name="storage_color_videos">#ff9BCFBD</color>
|
||||
<color name="storage_color_audio">@color/core_ultramarine</color>
|
||||
<color name="storage_color_files">#ffA23474</color>
|
||||
<color name="conversation_list_archive_background_start">#ff28782A</color>
|
||||
<color name="conversation_list_archive_background_end">#ff329635</color>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user