mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-22 10:46:50 +00:00
Add sliding animation when a new message is received.
This commit is contained in:
committed by
Greyson Parrelli
parent
f198b890fa
commit
2167522f7d
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user