From 2167522f7d7055d6bd1d490b1c5ee54fdbef3ce4 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 20 Oct 2021 16:40:34 -0300 Subject: [PATCH] Add sliding animation when a new message is received. --- .../securesms/BindableConversationItem.java | 4 + .../conversation/ConversationAdapter.java | 21 +++- .../conversation/ConversationFragment.java | 16 +-- .../conversation/ConversationItem.java | 8 +- .../mutiselect/MultiselectItemAnimator.kt | 106 ++++++++++++++++-- .../securesms/util/Projection.java | 4 + 6 files changed, 136 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index e23dc0571e..a25a9891ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -56,6 +56,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, // Intentionally Blank. } + default void updateContactNameColor() { + // Intentionally Blank. + } + interface EventListener { void onQuoteClicked(MmsMessageRecord messageRecord); void onLinkPreviewClicked(@NonNull LinkPreview linkPreview); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java index c4cf4488ef..25b5c3d422 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java @@ -100,7 +100,8 @@ public class ConversationAdapter private static final int MESSAGE_TYPE_FOOTER = 6; private static final int MESSAGE_TYPE_PLACEHOLDER = 7; - private static final int PAYLOAD_TIMESTAMP = 0; + private static final int PAYLOAD_TIMESTAMP = 0; + public static final int PAYLOAD_NAME_COLORS = 1; private final ItemClickListener clickListener; private final Context context; @@ -227,8 +228,13 @@ public class ConversationAdapter } } - @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) { - if (payloads.contains(PAYLOAD_TIMESTAMP)) { + private boolean containsValidPayload(@NonNull List payloads) { + return payloads.contains(PAYLOAD_TIMESTAMP) || payloads.contains(PAYLOAD_NAME_COLORS); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) { + if (containsValidPayload(payloads)) { switch (getItemViewType(position)) { case MESSAGE_TYPE_INCOMING_TEXT: case MESSAGE_TYPE_INCOMING_MULTIMEDIA: @@ -236,7 +242,14 @@ public class ConversationAdapter case MESSAGE_TYPE_OUTGOING_MULTIMEDIA: case MESSAGE_TYPE_UPDATE: ConversationViewHolder conversationViewHolder = (ConversationViewHolder) holder; - conversationViewHolder.getBindable().updateTimestamps(); + if (payloads.contains(PAYLOAD_TIMESTAMP)) { + conversationViewHolder.getBindable().updateTimestamps(); + } + + if (payloads.contains(PAYLOAD_NAME_COLORS)) { + conversationViewHolder.getBindable().updateContactNameColor(); + } + default: return; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 9a5cb7c2a5..084d827f1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -223,6 +223,8 @@ public class ConversationFragment extends LoggingFragment implements Multiselect private ConversationUpdateTick conversationUpdateTick; private MultiselectItemDecoration multiselectItemDecoration; + private int listSubmissionCount = 0; + public static void prepare(@NonNull Context context) { FrameLayout parent = new FrameLayout(context); parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT)); @@ -256,8 +258,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect toolbarShadow = requireActivity().findViewById(R.id.conversation_toolbar_shadow); reactionsShade = view.findViewById(R.id.reactions_shade); - ConversationIntents.Args args = ConversationIntents.Args.from(requireActivity().getIntent()); - final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true); final MultiselectItemAnimator multiselectItemAnimator = new MultiselectItemAnimator(() -> { ConversationAdapter adapter = getListAdapter(); @@ -266,18 +266,18 @@ public class ConversationFragment extends LoggingFragment implements Multiselect } else { return Util.hasItems(adapter.getSelectedItems()); } - }, multiselectPart -> { + }, () -> listSubmissionCount < 2, multiselectPart -> { ConversationAdapter adapter = getListAdapter(); if (adapter == null) { return false; } else { return adapter.getSelectedItems().contains(multiselectPart); } - }); + }, () -> list.canScrollVertically(1) || list.canScrollVertically(-1)); multiselectItemDecoration = new MultiselectItemDecoration(requireContext(), () -> conversationViewModel.getWallpaper().getValue(), multiselectItemAnimator::getSelectedProgressForPart, - multiselectItemAnimator::isInitialAnimation); + multiselectItemAnimator::isInitialMultiSelectAnimation); list.setHasFixedSize(false); list.setLayoutManager(layoutManager); @@ -321,7 +321,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect conversationViewModel.getMessages().observe(getViewLifecycleOwner(), messages -> { ConversationAdapter adapter = getListAdapter(); if (adapter != null) { - getListAdapter().submitList(messages); + getListAdapter().submitList(messages, () -> { + listSubmissionCount++; + }); } }); @@ -354,7 +356,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect ConversationAdapter adapter = getListAdapter(); if (adapter != null) { - adapter.notifyDataSetChanged(); + adapter.notifyItemRangeChanged(0, adapter.getItemCount(), ConversationAdapter.PAYLOAD_NAME_COLORS); } }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index f70c851d3d..a2bf5123f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -32,7 +32,6 @@ import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextPaint; -import android.text.TextUtils; import android.text.style.BackgroundColorSpan; import android.text.style.CharacterStyle; import android.text.style.ClickableSpan; @@ -332,6 +331,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo getActiveFooter(messageRecord).setMessageRecord(messageRecord, locale); } + @Override + public void updateContactNameColor() { + setGroupAuthorColor(messageRecord, hasWallpaper, colorizer); + } + @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { @@ -1751,7 +1755,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo projections.add(quoteView.getProjection(coordinateRoot).translateX(bodyBubble.getTranslationX() + this.getTranslationX())); } - return projections; + return projections.stream().map(p -> p.translateY(this.getTranslationY())).collect(Collectors.toList()); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemAnimator.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemAnimator.kt index d1b8b616b0..08ae922b02 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemAnimator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemAnimator.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.conversation.mutiselect +import android.animation.ObjectAnimator import android.animation.ValueAnimator import androidx.core.animation.doOnEnd import androidx.recyclerview.widget.RecyclerView @@ -12,7 +13,9 @@ import androidx.recyclerview.widget.RecyclerView */ class MultiselectItemAnimator( private val isInMultiSelectMode: () -> Boolean, - private val isPartSelected: (MultiselectPart) -> Boolean + private val isLoadingInitialContent: () -> Boolean, + private val isPartSelected: (MultiselectPart) -> Boolean, + private val isParentFilled: () -> Boolean ) : RecyclerView.ItemAnimator() { private data class Selection( @@ -20,14 +23,26 @@ class MultiselectItemAnimator( val viewHolder: RecyclerView.ViewHolder ) - var isInitialAnimation: Boolean = true + private data class SlideInfo( + val viewHolder: RecyclerView.ViewHolder, + val operation: Operation + ) + + private enum class Operation { + ADD, + CHANGE + } + + var isInitialMultiSelectAnimation: Boolean = true private set private val selected: MutableSet = mutableSetOf() private val pendingSelectedAnimations: MutableSet = mutableSetOf() + private val pendingSlideAnimations: MutableSet = mutableSetOf() private val selectedAnimations: MutableMap = mutableMapOf() + private val slideAnimations: MutableMap = mutableMapOf() fun getSelectedProgressForPart(multiselectPart: MultiselectPart): Float { return if (pendingSelectedAnimations.any { it.multiselectPart == multiselectPart }) { @@ -43,8 +58,37 @@ class MultiselectItemAnimator( } override fun animateAppearance(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo?, postLayoutInfo: ItemHolderInfo): Boolean { - dispatchAnimationFinished(viewHolder) - return false + return animateSlide(viewHolder, preLayoutInfo, postLayoutInfo, Operation.ADD) + } + + private fun animateSlide(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo?, postLayoutInfo: ItemHolderInfo, operation: Operation): Boolean { + if (isInMultiSelectMode() || isLoadingInitialContent()) { + dispatchAnimationFinished(viewHolder) + return false + } + + if (operation == Operation.CHANGE && !isParentFilled()) { + dispatchAnimationFinished(viewHolder) + return false + } + + val translationY = if (preLayoutInfo == null) { + postLayoutInfo.bottom - postLayoutInfo.top + } else { + preLayoutInfo.top - postLayoutInfo.top + }.toFloat() + + viewHolder.itemView.translationY = translationY + val slideInfo = SlideInfo(viewHolder, operation) + + if (slideAnimations.filterKeys { slideInfo.viewHolder == viewHolder }.isNotEmpty()) { + dispatchAnimationFinished(viewHolder) + return false + } + + pendingSlideAnimations.add(slideInfo) + dispatchAnimationStarted(viewHolder) + return true } override fun animatePersistence(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo, postLayoutInfo: ItemHolderInfo): Boolean { @@ -60,9 +104,13 @@ class MultiselectItemAnimator( val isInMultiSelectMode = isInMultiSelectMode() if (!isInMultiSelectMode) { selected.clear() - isInitialAnimation = true - dispatchAnimationFinished(newHolder) - return false + isInitialMultiSelectAnimation = true + return if (preLayoutInfo.top == postLayoutInfo.top) { + dispatchAnimationFinished(newHolder) + false + } else { + animateSlide(newHolder, preLayoutInfo, postLayoutInfo, Operation.CHANGE) + } } var isAnimationStarted = false @@ -83,7 +131,7 @@ class MultiselectItemAnimator( pendingSelectedAnimations.add(Selection(part, newHolder)) selected.add(part) isAnimationStarted = true - } else if (isInitialAnimation) { + } else if (isInitialMultiSelectAnimation) { pendingSelectedAnimations.add(Selection(part, newHolder)) isAnimationStarted = true } @@ -99,6 +147,29 @@ class MultiselectItemAnimator( } override fun runPendingAnimations() { + runPendingSelectedAnimations() + runPendingSlideAnimations() + } + + private fun runPendingSlideAnimations() { + for (slideInfo in pendingSlideAnimations) { + val animator = ObjectAnimator.ofFloat(slideInfo.viewHolder.itemView, "translationY", 0f) + slideAnimations[slideInfo] = animator + animator.duration = 150L + animator.addUpdateListener { + (slideInfo.viewHolder.itemView.parent as RecyclerView?)?.invalidateItemDecorations() + } + animator.doOnEnd { + dispatchAnimationFinished(slideInfo.viewHolder) + slideAnimations.remove(slideInfo) + } + animator.start() + } + + pendingSlideAnimations.clear() + } + + private fun runPendingSelectedAnimations() { for (selection in pendingSelectedAnimations) { val animator = ValueAnimator.ofFloat(0f, 1f) selectedAnimations[selection] = animator @@ -109,7 +180,7 @@ class MultiselectItemAnimator( animator.doOnEnd { dispatchAnimationFinished(selection.viewHolder) selectedAnimations.remove(selection) - isInitialAnimation = false + isInitialMultiSelectAnimation = false } animator.start() } @@ -119,15 +190,17 @@ class MultiselectItemAnimator( override fun endAnimation(item: RecyclerView.ViewHolder) { endSelectedAnimation(item) + endSlideAnimation(item) } override fun endAnimations() { endSelectedAnimations() + endSlideAnimations() dispatchAnimationsFinished() } override fun isRunning(): Boolean { - return selectedAnimations.values.any { it.isRunning } + return (selectedAnimations.values + slideAnimations.values).any { it.isRunning } } override fun onAnimationFinished(viewHolder: RecyclerView.ViewHolder) { @@ -143,8 +216,21 @@ class MultiselectItemAnimator( } } + private fun endSlideAnimation(item: RecyclerView.ViewHolder) { + val selections = slideAnimations.filter { (k, _) -> k.viewHolder == item } + selections.forEach { (k, v) -> + v.end() + slideAnimations.remove(k) + } + } + fun endSelectedAnimations() { selectedAnimations.values.forEach { it.end() } selectedAnimations.clear() } + + fun endSlideAnimations() { + slideAnimations.values.forEach { it.end() } + slideAnimations.clear() + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Projection.java b/app/src/main/java/org/thoughtcrime/securesms/util/Projection.java index 033d213b85..3e9d07ecb2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Projection.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Projection.java @@ -105,6 +105,10 @@ public final class Projection { return new Projection(x + xTranslation, y, width, height, corners); } + public @NonNull Projection translateY(float yTranslation) { + return new Projection(x, y + yTranslation, width, height, corners); + } + public @NonNull Projection withDimensions(int width, int height) { return new Projection(x, y, width, height, corners); }