Add sliding animation when a new message is received.

This commit is contained in:
Alex Hart
2021-10-20 16:40:34 -03:00
committed by Greyson Parrelli
parent f198b890fa
commit 2167522f7d
6 changed files with 136 additions and 23 deletions

View File

@@ -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);

View File

@@ -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<Object> payloads) {
if (payloads.contains(PAYLOAD_TIMESTAMP)) {
private boolean containsValidPayload(@NonNull List<Object> payloads) {
return payloads.contains(PAYLOAD_TIMESTAMP) || payloads.contains(PAYLOAD_NAME_COLORS);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> 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;
}

View File

@@ -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);
}
});

View File

@@ -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

View File

@@ -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<MultiselectPart> = mutableSetOf()
private val pendingSelectedAnimations: MutableSet<Selection> = mutableSetOf()
private val pendingSlideAnimations: MutableSet<SlideInfo> = mutableSetOf()
private val selectedAnimations: MutableMap<Selection, ValueAnimator> = mutableMapOf()
private val slideAnimations: MutableMap<SlideInfo, ValueAnimator> = 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()
}
}

View File

@@ -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);
}