kopia lustrzana https://github.com/ryukoposting/Signal-Android
Fix several beta issues with new slide animations.
rodzic
ced05fe579
commit
98fce53cf1
|
@ -20,7 +20,7 @@ class BodyBubbleLayoutTransition(bodyBubble: ConversationItemBodyBubble) : Layou
|
||||||
|
|
||||||
animator.duration = getAnimator(CHANGE_DISAPPEARING).duration
|
animator.duration = getAnimator(CHANGE_DISAPPEARING).duration
|
||||||
animator.addUpdateListener {
|
animator.addUpdateListener {
|
||||||
val parentRecycler: RecyclerView? = bodyBubble.parent.parent as? RecyclerView
|
val parentRecycler: RecyclerView? = bodyBubble.parent?.parent as? RecyclerView
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parentRecycler?.invalidate()
|
parentRecycler?.invalidate()
|
||||||
|
|
|
@ -166,7 +166,7 @@ public class ConversationAdapter
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemViewType(int position) {
|
public int getItemViewType(int position) {
|
||||||
if (hasHeader() && position == 0) {
|
if (isTypingViewEnabled() && position == 0) {
|
||||||
return MESSAGE_TYPE_HEADER;
|
return MESSAGE_TYPE_HEADER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,7 +365,7 @@ public class ConversationAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable ConversationMessage getItem(int position) {
|
public @Nullable ConversationMessage getItem(int position) {
|
||||||
position = hasHeader() ? position - 1 : position;
|
position = isTypingViewEnabled() ? position - 1 : position;
|
||||||
|
|
||||||
if (position == -1) {
|
if (position == -1) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -421,7 +421,7 @@ public class ConversationAdapter
|
||||||
*/
|
*/
|
||||||
@MainThread
|
@MainThread
|
||||||
int getAdapterPositionForMessagePosition(int messagePosition) {
|
int getAdapterPositionForMessagePosition(int messagePosition) {
|
||||||
return hasHeader() ? messagePosition + 1 : messagePosition;
|
return isTypingViewEnabled() ? messagePosition + 1 : messagePosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -602,7 +602,7 @@ public class ConversationAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasHeader() {
|
public boolean isTypingViewEnabled() {
|
||||||
return isTypingViewEnabled;
|
return isTypingViewEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -611,7 +611,7 @@ public class ConversationAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isHeaderPosition(int position) {
|
private boolean isHeaderPosition(int position) {
|
||||||
return hasHeader() && position == 0;
|
return isTypingViewEnabled() && position == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isFooterPosition(int position) {
|
private boolean isFooterPosition(int position) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import androidx.annotation.NonNull;
|
||||||
/**
|
/**
|
||||||
* Represents metadata about a conversation.
|
* Represents metadata about a conversation.
|
||||||
*/
|
*/
|
||||||
final class ConversationData {
|
public final class ConversationData {
|
||||||
private final long threadId;
|
private final long threadId;
|
||||||
private final long lastSeen;
|
private final long lastSeen;
|
||||||
private final int lastSeenPosition;
|
private final int lastSeenPosition;
|
||||||
|
|
|
@ -53,9 +53,7 @@ import androidx.core.app.ActivityOptionsCompat;
|
||||||
import androidx.core.text.HtmlCompat;
|
import androidx.core.text.HtmlCompat;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.lifecycle.Transformations;
|
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
@ -86,7 +84,7 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickList
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder;
|
import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder;
|
||||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||||
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer;
|
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer;
|
||||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectItemAnimator;
|
import org.thoughtcrime.securesms.conversation.mutiselect.ConversationItemAnimator;
|
||||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectItemDecoration;
|
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectItemDecoration;
|
||||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
|
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
|
||||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment;
|
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment;
|
||||||
|
@ -158,7 +156,6 @@ import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||||
import org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar;
|
import org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar;
|
||||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||||
|
@ -223,8 +220,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||||
private ConversationUpdateTick conversationUpdateTick;
|
private ConversationUpdateTick conversationUpdateTick;
|
||||||
private MultiselectItemDecoration multiselectItemDecoration;
|
private MultiselectItemDecoration multiselectItemDecoration;
|
||||||
|
|
||||||
private boolean initialDataLoaded = false;
|
|
||||||
|
|
||||||
public static void prepare(@NonNull Context context) {
|
public static void prepare(@NonNull Context context) {
|
||||||
FrameLayout parent = new FrameLayout(context);
|
FrameLayout parent = new FrameLayout(context);
|
||||||
parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT));
|
parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT));
|
||||||
|
@ -259,14 +254,14 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||||
reactionsShade = view.findViewById(R.id.reactions_shade);
|
reactionsShade = view.findViewById(R.id.reactions_shade);
|
||||||
|
|
||||||
final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true);
|
final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true);
|
||||||
final MultiselectItemAnimator multiselectItemAnimator = new MultiselectItemAnimator(() -> {
|
final ConversationItemAnimator conversationItemAnimator = new ConversationItemAnimator(() -> {
|
||||||
ConversationAdapter adapter = getListAdapter();
|
ConversationAdapter adapter = getListAdapter();
|
||||||
if (adapter == null) {
|
if (adapter == null) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return Util.hasItems(adapter.getSelectedItems());
|
return Util.hasItems(adapter.getSelectedItems());
|
||||||
}
|
}
|
||||||
}, () -> !initialDataLoaded, () -> list.canScrollVertically(1) || list.canScrollVertically(-1));
|
}, () -> conversationViewModel.shouldPlayMessageAnimations(), () -> list.canScrollVertically(1) || list.canScrollVertically(-1));
|
||||||
multiselectItemDecoration = new MultiselectItemDecoration(requireContext(),
|
multiselectItemDecoration = new MultiselectItemDecoration(requireContext(),
|
||||||
() -> conversationViewModel.getWallpaper().getValue());
|
() -> conversationViewModel.getWallpaper().getValue());
|
||||||
|
|
||||||
|
@ -276,7 +271,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||||
RecyclerViewColorizer recyclerViewColorizer = new RecyclerViewColorizer(list);
|
RecyclerViewColorizer recyclerViewColorizer = new RecyclerViewColorizer(list);
|
||||||
|
|
||||||
list.addItemDecoration(multiselectItemDecoration);
|
list.addItemDecoration(multiselectItemDecoration);
|
||||||
list.setItemAnimator(multiselectItemAnimator);
|
list.setItemAnimator(conversationItemAnimator);
|
||||||
|
|
||||||
getViewLifecycleOwner().getLifecycle().addObserver(multiselectItemDecoration);
|
getViewLifecycleOwner().getLifecycle().addObserver(multiselectItemDecoration);
|
||||||
|
|
||||||
|
@ -307,25 +302,12 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||||
this.messageCountsViewModel = ViewModelProviders.of(requireActivity()).get(MessageCountsViewModel.class);
|
this.messageCountsViewModel = ViewModelProviders.of(requireActivity()).get(MessageCountsViewModel.class);
|
||||||
this.conversationViewModel = ViewModelProviders.of(requireActivity(), new ConversationViewModel.Factory()).get(ConversationViewModel.class);
|
this.conversationViewModel = ViewModelProviders.of(requireActivity(), new ConversationViewModel.Factory()).get(ConversationViewModel.class);
|
||||||
|
|
||||||
MutableLiveData<Boolean> hasSubmittedNonEmptyList = new MutableLiveData<>(false);
|
|
||||||
LiveData<Boolean> hasMessagesInThread = Transformations.map(conversationViewModel.getConversationMetadata(), c -> c.getThreadSize() > 0);
|
|
||||||
|
|
||||||
LiveData<Boolean> playAnimations = LiveDataUtil.combineLatest(hasSubmittedNonEmptyList, hasMessagesInThread, (a, b) -> {
|
|
||||||
if (a && b) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return !b;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
playAnimations.observe(getViewLifecycleOwner(), p -> initialDataLoaded = p);
|
|
||||||
|
|
||||||
conversationViewModel.getChatColors().observe(getViewLifecycleOwner(), recyclerViewColorizer::setChatColors);
|
conversationViewModel.getChatColors().observe(getViewLifecycleOwner(), recyclerViewColorizer::setChatColors);
|
||||||
conversationViewModel.getMessages().observe(getViewLifecycleOwner(), messages -> {
|
conversationViewModel.getMessages().observe(getViewLifecycleOwner(), messages -> {
|
||||||
ConversationAdapter adapter = getListAdapter();
|
ConversationAdapter adapter = getListAdapter();
|
||||||
if (adapter != null) {
|
if (adapter != null) {
|
||||||
getListAdapter().submitList(messages, () -> {
|
getListAdapter().submitList(messages, () -> {
|
||||||
hasSubmittedNonEmptyList.postValue(!messages.isEmpty());
|
list.post(() -> conversationViewModel.onMessagesCommitted(messages));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -391,7 +373,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||||
public void clearFocusedItem() {
|
public void clearFocusedItem() {
|
||||||
multiselectItemDecoration.setFocusedItem(null);
|
multiselectItemDecoration.setFocusedItem(null);
|
||||||
list.invalidateItemDecorations();
|
list.invalidateItemDecorations();
|
||||||
reactionsShade.setVisibility(View.GONE);
|
reactionsShade.setVisibility(View.INVISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateConversationItemTimestamps() {
|
private void updateConversationItemTimestamps() {
|
||||||
|
@ -1040,7 +1022,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||||
.submit();
|
.submit();
|
||||||
} else if (conversation.getMessageRequestData().isMessageRequestAccepted()) {
|
} else if (conversation.getMessageRequestData().isMessageRequestAccepted()) {
|
||||||
snapToTopDataObserver.buildScrollPosition(conversation.shouldScrollToLastSeen() ? lastSeenPosition : lastScrolledPosition)
|
snapToTopDataObserver.buildScrollPosition(conversation.shouldScrollToLastSeen() ? lastSeenPosition : lastScrolledPosition)
|
||||||
.withOnPerformScroll((layoutManager, position) -> layoutManager.scrollToPositionWithOffset(position, list.getHeight() - (conversation.shouldScrollToLastSeen() ? lastSeenScrollOffset : 0)))
|
.withOnPerformScroll((layoutManager, position) -> layoutManager.scrollToPositionWithOffset(position, (list.getHeight() + reactionsShade.getHeight()) - (conversation.shouldScrollToLastSeen() ? lastSeenScrollOffset : 0)))
|
||||||
.withOnScrollRequestComplete(afterScroll)
|
.withOnScrollRequestComplete(afterScroll)
|
||||||
.submit();
|
.submit();
|
||||||
} else {
|
} else {
|
||||||
|
@ -1064,7 +1046,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isTypingIndicatorShowing() {
|
private boolean isTypingIndicatorShowing() {
|
||||||
return getListAdapter().hasHeader();
|
return getListAdapter().isTypingViewEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onSearchQueryUpdated(@Nullable String query) {
|
public void onSearchQueryUpdated(@Nullable String query) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import androidx.annotation.MainThread;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.lifecycle.Transformations;
|
import androidx.lifecycle.Transformations;
|
||||||
import androidx.lifecycle.ViewModel;
|
import androidx.lifecycle.ViewModel;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
@ -37,8 +38,10 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.Store;
|
||||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||||
import org.whispersystems.libsignal.util.Pair;
|
import org.whispersystems.libsignal.util.Pair;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
@ -76,6 +79,8 @@ public class ConversationViewModel extends ViewModel {
|
||||||
private final MutableLiveData<Integer> toolbarBottom;
|
private final MutableLiveData<Integer> toolbarBottom;
|
||||||
private final MutableLiveData<Integer> inlinePlayerHeight;
|
private final MutableLiveData<Integer> inlinePlayerHeight;
|
||||||
private final LiveData<Integer> conversationTopMargin;
|
private final LiveData<Integer> conversationTopMargin;
|
||||||
|
private final Store<ThreadAnimationState> threadAnimationStateStore;
|
||||||
|
private final Observer<ThreadAnimationState> threadAnimationStateStoreDriver;
|
||||||
|
|
||||||
private final Map<GroupId, Set<Recipient>> sessionMemberCache = new HashMap<>();
|
private final Map<GroupId, Set<Recipient>> sessionMemberCache = new HashMap<>();
|
||||||
|
|
||||||
|
@ -99,6 +104,7 @@ public class ConversationViewModel extends ViewModel {
|
||||||
this.toolbarBottom = new MutableLiveData<>();
|
this.toolbarBottom = new MutableLiveData<>();
|
||||||
this.inlinePlayerHeight = new MutableLiveData<>();
|
this.inlinePlayerHeight = new MutableLiveData<>();
|
||||||
this.conversationTopMargin = Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(toolbarBottom, inlinePlayerHeight, Integer::sum));
|
this.conversationTopMargin = Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(toolbarBottom, inlinePlayerHeight, Integer::sum));
|
||||||
|
this.threadAnimationStateStore = new Store<>(new ThreadAnimationState(-1L, null, false));
|
||||||
|
|
||||||
LiveData<Recipient> recipientLiveData = LiveDataUtil.mapAsync(recipientId, Recipient::resolved);
|
LiveData<Recipient> recipientLiveData = LiveDataUtil.mapAsync(recipientId, Recipient::resolved);
|
||||||
LiveData<ThreadAndRecipient> threadAndRecipient = LiveDataUtil.combineLatest(threadId, recipientLiveData, ThreadAndRecipient::new);
|
LiveData<ThreadAndRecipient> threadAndRecipient = LiveDataUtil.combineLatest(threadId, recipientLiveData, ThreadAndRecipient::new);
|
||||||
|
@ -158,6 +164,41 @@ public class ConversationViewModel extends ViewModel {
|
||||||
chatColors = LiveDataUtil.mapDistinct(Transformations.switchMap(recipientId,
|
chatColors = LiveDataUtil.mapDistinct(Transformations.switchMap(recipientId,
|
||||||
id -> Recipient.live(id).getLiveData()),
|
id -> Recipient.live(id).getLiveData()),
|
||||||
Recipient::getChatColors);
|
Recipient::getChatColors);
|
||||||
|
|
||||||
|
threadAnimationStateStore.update(threadId, (id, state) -> {
|
||||||
|
if (state.getThreadId() == id) {
|
||||||
|
return state;
|
||||||
|
} else {
|
||||||
|
return new ThreadAnimationState(id, null, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
threadAnimationStateStore.update(metadata, (m, state) -> {
|
||||||
|
if (state.getThreadId() == m.getThreadId()) {
|
||||||
|
return state.copy(state.getThreadId(), m, state.getHasCommittedNonEmptyMessageList());
|
||||||
|
} else {
|
||||||
|
return state.copy(m.getThreadId(), m, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.threadAnimationStateStoreDriver = state -> {};
|
||||||
|
threadAnimationStateStore.getStateLiveData().observeForever(threadAnimationStateStoreDriver);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onMessagesCommitted(@NonNull List<ConversationMessage> conversationMessages) {
|
||||||
|
if (Util.hasItems(conversationMessages)) {
|
||||||
|
threadAnimationStateStore.update(state -> {
|
||||||
|
if (state.getThreadId() == conversationMessages.get(0).getMessageRecord().getThreadId()) {
|
||||||
|
return state.copy(state.getThreadId(), state.getThreadMetadata(), true);
|
||||||
|
} else {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean shouldPlayMessageAnimations() {
|
||||||
|
return threadAnimationStateStore.getState().shouldPlayMessageAnimations();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setToolbarBottom(int bottom) {
|
void setToolbarBottom(int bottom) {
|
||||||
|
@ -301,6 +342,7 @@ public class ConversationViewModel extends ViewModel {
|
||||||
@Override
|
@Override
|
||||||
protected void onCleared() {
|
protected void onCleared() {
|
||||||
super.onCleared();
|
super.onCleared();
|
||||||
|
threadAnimationStateStore.getStateLiveData().removeObserver(threadAnimationStateStoreDriver);
|
||||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver);
|
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver);
|
||||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver);
|
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver);
|
||||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver);
|
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver);
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents how conversation bubbles should animate at any given time.
|
||||||
|
*/
|
||||||
|
data class ThreadAnimationState constructor(
|
||||||
|
val threadId: Long,
|
||||||
|
val threadMetadata: ConversationData?,
|
||||||
|
val hasCommittedNonEmptyMessageList: Boolean
|
||||||
|
) {
|
||||||
|
fun shouldPlayMessageAnimations(): Boolean {
|
||||||
|
return when {
|
||||||
|
threadId == -1L || threadMetadata == null -> false
|
||||||
|
threadMetadata.threadSize == 0 -> true
|
||||||
|
threadMetadata.threadSize > 0 && hasCommittedNonEmptyMessageList -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package org.thoughtcrime.securesms.conversation.mutiselect
|
package org.thoughtcrime.securesms.conversation.mutiselect
|
||||||
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorSet
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import androidx.core.animation.doOnEnd
|
import androidx.core.animation.doOnEnd
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -12,9 +13,9 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||||
*
|
*
|
||||||
* Can be expanded upon in the future to animate other things, such as message sends.
|
* Can be expanded upon in the future to animate other things, such as message sends.
|
||||||
*/
|
*/
|
||||||
class MultiselectItemAnimator(
|
class ConversationItemAnimator(
|
||||||
private val isInMultiSelectMode: () -> Boolean,
|
private val isInMultiSelectMode: () -> Boolean,
|
||||||
private val isLoadingInitialContent: () -> Boolean,
|
private val shouldPlayMessageAnimations: () -> Boolean,
|
||||||
private val isParentFilled: () -> Boolean
|
private val isParentFilled: () -> Boolean
|
||||||
) : RecyclerView.ItemAnimator() {
|
) : RecyclerView.ItemAnimator() {
|
||||||
|
|
||||||
|
@ -23,14 +24,30 @@ class MultiselectItemAnimator(
|
||||||
CHANGE
|
CHANGE
|
||||||
}
|
}
|
||||||
|
|
||||||
private val pendingSlideAnimations: MutableSet<RecyclerView.ViewHolder> = mutableSetOf()
|
private data class TweeningInfo(
|
||||||
private var pendingTypingViewSlideOut: RecyclerView.ViewHolder? = null
|
val startValue: Float,
|
||||||
|
val endValue: Float
|
||||||
|
) {
|
||||||
|
fun lerp(progress: Float): Float {
|
||||||
|
return startValue + progress * (endValue - startValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val slideAnimations: MutableMap<RecyclerView.ViewHolder, ValueAnimator> = mutableMapOf()
|
private data class AnimationInfo(
|
||||||
|
val sharedAnimator: ValueAnimator,
|
||||||
|
val tweeningInfo: TweeningInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
private val pendingSlideAnimations: MutableMap<RecyclerView.ViewHolder, TweeningInfo> = mutableMapOf()
|
||||||
|
private val slideAnimations: MutableMap<RecyclerView.ViewHolder, AnimationInfo> = mutableMapOf()
|
||||||
|
|
||||||
override fun animateDisappearance(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo, postLayoutInfo: ItemHolderInfo?): Boolean {
|
override fun animateDisappearance(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo, postLayoutInfo: ItemHolderInfo?): Boolean {
|
||||||
if (viewHolder is ConversationAdapter.HeaderViewHolder && pendingTypingViewSlideOut == null) {
|
if (viewHolder is ConversationAdapter.HeaderViewHolder &&
|
||||||
pendingTypingViewSlideOut = viewHolder
|
!pendingSlideAnimations.containsKey(viewHolder) &&
|
||||||
|
!slideAnimations.containsKey(viewHolder) &&
|
||||||
|
shouldPlayMessageAnimations()
|
||||||
|
) {
|
||||||
|
pendingSlideAnimations[viewHolder] = TweeningInfo(0f, viewHolder.itemView.height.toFloat())
|
||||||
dispatchAnimationStarted(viewHolder)
|
dispatchAnimationStarted(viewHolder)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -49,7 +66,7 @@ class MultiselectItemAnimator(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun animateSlide(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo?, postLayoutInfo: ItemHolderInfo, operation: Operation): Boolean {
|
private fun animateSlide(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo?, postLayoutInfo: ItemHolderInfo, operation: Operation): Boolean {
|
||||||
if (isInMultiSelectMode() || isLoadingInitialContent()) {
|
if (isInMultiSelectMode() || !shouldPlayMessageAnimations()) {
|
||||||
dispatchAnimationFinished(viewHolder)
|
dispatchAnimationFinished(viewHolder)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -77,14 +94,14 @@ class MultiselectItemAnimator(
|
||||||
|
|
||||||
viewHolder.itemView.translationY = translationY
|
viewHolder.itemView.translationY = translationY
|
||||||
|
|
||||||
pendingSlideAnimations.add(viewHolder)
|
pendingSlideAnimations[viewHolder] = TweeningInfo(translationY, 0f)
|
||||||
dispatchAnimationStarted(viewHolder)
|
dispatchAnimationStarted(viewHolder)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun animatePersistence(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo, postLayoutInfo: ItemHolderInfo): Boolean {
|
override fun animatePersistence(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo, postLayoutInfo: ItemHolderInfo): Boolean {
|
||||||
val isInMultiSelectMode = isInMultiSelectMode()
|
val isInMultiSelectMode = isInMultiSelectMode()
|
||||||
return if (!isInMultiSelectMode) {
|
return if (!isInMultiSelectMode && shouldPlayMessageAnimations()) {
|
||||||
if (pendingSlideAnimations.contains(viewHolder) || slideAnimations.containsKey(viewHolder)) {
|
if (pendingSlideAnimations.contains(viewHolder) || slideAnimations.containsKey(viewHolder)) {
|
||||||
dispatchAnimationFinished(viewHolder)
|
dispatchAnimationFinished(viewHolder)
|
||||||
false
|
false
|
||||||
|
@ -107,52 +124,42 @@ class MultiselectItemAnimator(
|
||||||
|
|
||||||
override fun runPendingAnimations() {
|
override fun runPendingAnimations() {
|
||||||
runPendingSlideAnimations()
|
runPendingSlideAnimations()
|
||||||
runPendingSlideOutAnimation()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runPendingSlideAnimations() {
|
private fun runPendingSlideAnimations() {
|
||||||
for (viewHolder in pendingSlideAnimations) {
|
val animators: MutableList<Animator> = mutableListOf()
|
||||||
val animator = ObjectAnimator.ofFloat(viewHolder.itemView, "translationY", 0f)
|
for ((viewHolder, tweeningInfo) in pendingSlideAnimations) {
|
||||||
slideAnimations[viewHolder]?.cancel()
|
val animator = ValueAnimator.ofFloat(0f, 1f)
|
||||||
slideAnimations[viewHolder] = animator
|
slideAnimations[viewHolder] = AnimationInfo(animator, tweeningInfo)
|
||||||
animator.duration = 150L
|
animator.duration = 150L
|
||||||
animator.addUpdateListener {
|
animator.addUpdateListener {
|
||||||
|
if (viewHolder in slideAnimations) {
|
||||||
|
viewHolder.itemView.translationY = tweeningInfo.lerp(it.animatedFraction)
|
||||||
(viewHolder.itemView.parent as RecyclerView?)?.invalidate()
|
(viewHolder.itemView.parent as RecyclerView?)?.invalidate()
|
||||||
}
|
}
|
||||||
animator.doOnEnd {
|
|
||||||
viewHolder.itemView.translationY = 0f
|
|
||||||
slideAnimations.remove(viewHolder)
|
|
||||||
dispatchAnimationFinished(viewHolder)
|
|
||||||
dispatchFinishedWhenDone()
|
|
||||||
}
|
}
|
||||||
animator.start()
|
animator.doOnEnd {
|
||||||
|
if (viewHolder in slideAnimations) {
|
||||||
|
handleAnimationEnd(viewHolder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animators.add(animator)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatorSet().apply {
|
||||||
|
playTogether(animators)
|
||||||
|
start()
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingSlideAnimations.clear()
|
pendingSlideAnimations.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runPendingSlideOutAnimation() {
|
private fun handleAnimationEnd(viewHolder: RecyclerView.ViewHolder) {
|
||||||
val viewHolder = pendingTypingViewSlideOut
|
|
||||||
if (viewHolder != null) {
|
|
||||||
pendingTypingViewSlideOut = null
|
|
||||||
slideAnimations[viewHolder]?.cancel()
|
|
||||||
|
|
||||||
val animator = ObjectAnimator.ofFloat(viewHolder.itemView, "translationY", viewHolder.itemView.height.toFloat())
|
|
||||||
|
|
||||||
slideAnimations[viewHolder] = animator
|
|
||||||
animator.duration = 150L
|
|
||||||
animator.addUpdateListener {
|
|
||||||
(viewHolder.itemView.parent as RecyclerView?)?.invalidate()
|
|
||||||
}
|
|
||||||
animator.doOnEnd {
|
|
||||||
viewHolder.itemView.translationY = 0f
|
viewHolder.itemView.translationY = 0f
|
||||||
slideAnimations.remove(viewHolder)
|
slideAnimations.remove(viewHolder)
|
||||||
dispatchAnimationFinished(viewHolder)
|
dispatchAnimationFinished(viewHolder)
|
||||||
dispatchFinishedWhenDone()
|
dispatchFinishedWhenDone()
|
||||||
}
|
}
|
||||||
animator.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun endAnimation(item: RecyclerView.ViewHolder) {
|
override fun endAnimation(item: RecyclerView.ViewHolder) {
|
||||||
endSlideAnimation(item)
|
endSlideAnimation(item)
|
||||||
|
@ -164,7 +171,7 @@ class MultiselectItemAnimator(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isRunning(): Boolean {
|
override fun isRunning(): Boolean {
|
||||||
return slideAnimations.values.any { it.isRunning }
|
return slideAnimations.values.any { it.sharedAnimator.isRunning }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationFinished(viewHolder: RecyclerView.ViewHolder) {
|
override fun onAnimationFinished(viewHolder: RecyclerView.ViewHolder) {
|
||||||
|
@ -173,11 +180,13 @@ class MultiselectItemAnimator(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun endSlideAnimation(item: RecyclerView.ViewHolder) {
|
private fun endSlideAnimation(item: RecyclerView.ViewHolder) {
|
||||||
slideAnimations[item]?.cancel()
|
slideAnimations[item]?.sharedAnimator?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun endSlideAnimations() {
|
fun endSlideAnimations() {
|
||||||
slideAnimations.values.forEach { it.cancel() }
|
slideAnimations.values.map { it.sharedAnimator }.forEach {
|
||||||
|
it.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dispatchFinishedWhenDone() {
|
private fun dispatchFinishedWhenDone() {
|
|
@ -12,7 +12,7 @@
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:background="@color/reactions_screen_shade_color"
|
android:background="@color/reactions_screen_shade_color"
|
||||||
app:layout_constraintTop_toBottomOf="@android:id/list"
|
app:layout_constraintTop_toBottomOf="@android:id/list"
|
||||||
android:visibility="gone" />
|
android:visibility="invisible" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/video_container"
|
android:id="@+id/video_container"
|
||||||
|
|
Ładowanie…
Reference in New Issue