Fix several beta issues with new slide animations.

fork-5.53.8
Alex Hart 2021-10-25 13:39:01 -03:00 zatwierdzone przez GitHub
rodzic ced05fe579
commit 98fce53cf1
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
8 zmienionych plików z 149 dodań i 97 usunięć

Wyświetl plik

@ -20,7 +20,7 @@ class BodyBubbleLayoutTransition(bodyBubble: ConversationItemBodyBubble) : Layou
animator.duration = getAnimator(CHANGE_DISAPPEARING).duration
animator.addUpdateListener {
val parentRecycler: RecyclerView? = bodyBubble.parent.parent as? RecyclerView
val parentRecycler: RecyclerView? = bodyBubble.parent?.parent as? RecyclerView
try {
parentRecycler?.invalidate()

Wyświetl plik

@ -166,7 +166,7 @@ public class ConversationAdapter
@Override
public int getItemViewType(int position) {
if (hasHeader() && position == 0) {
if (isTypingViewEnabled() && position == 0) {
return MESSAGE_TYPE_HEADER;
}
@ -365,7 +365,7 @@ public class ConversationAdapter
}
public @Nullable ConversationMessage getItem(int position) {
position = hasHeader() ? position - 1 : position;
position = isTypingViewEnabled() ? position - 1 : position;
if (position == -1) {
return null;
@ -421,7 +421,7 @@ public class ConversationAdapter
*/
@MainThread
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;
}
@ -611,7 +611,7 @@ public class ConversationAdapter
}
private boolean isHeaderPosition(int position) {
return hasHeader() && position == 0;
return isTypingViewEnabled() && position == 0;
}
private boolean isFooterPosition(int position) {

Wyświetl plik

@ -5,7 +5,7 @@ import androidx.annotation.NonNull;
/**
* Represents metadata about a conversation.
*/
final class ConversationData {
public final class ConversationData {
private final long threadId;
private final long lastSeen;
private final int lastSeenPosition;

Wyświetl plik

@ -53,9 +53,7 @@ import androidx.core.app.ActivityOptionsCompat;
import androidx.core.text.HtmlCompat;
import androidx.core.view.ViewCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager;
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.colors.Colorizer;
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.MultiselectPart;
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.WindowUtil;
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.views.AdaptiveActionsToolbar;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
@ -223,8 +220,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
private ConversationUpdateTick conversationUpdateTick;
private MultiselectItemDecoration multiselectItemDecoration;
private boolean initialDataLoaded = false;
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));
@ -258,15 +253,15 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
toolbarShadow = requireActivity().findViewById(R.id.conversation_toolbar_shadow);
reactionsShade = view.findViewById(R.id.reactions_shade);
final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true);
final MultiselectItemAnimator multiselectItemAnimator = new MultiselectItemAnimator(() -> {
final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true);
final ConversationItemAnimator conversationItemAnimator = new ConversationItemAnimator(() -> {
ConversationAdapter adapter = getListAdapter();
if (adapter == null) {
return false;
} else {
return Util.hasItems(adapter.getSelectedItems());
}
}, () -> !initialDataLoaded, () -> list.canScrollVertically(1) || list.canScrollVertically(-1));
}, () -> conversationViewModel.shouldPlayMessageAnimations(), () -> list.canScrollVertically(1) || list.canScrollVertically(-1));
multiselectItemDecoration = new MultiselectItemDecoration(requireContext(),
() -> conversationViewModel.getWallpaper().getValue());
@ -276,7 +271,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
RecyclerViewColorizer recyclerViewColorizer = new RecyclerViewColorizer(list);
list.addItemDecoration(multiselectItemDecoration);
list.setItemAnimator(multiselectItemAnimator);
list.setItemAnimator(conversationItemAnimator);
getViewLifecycleOwner().getLifecycle().addObserver(multiselectItemDecoration);
@ -307,25 +302,12 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
this.messageCountsViewModel = ViewModelProviders.of(requireActivity()).get(MessageCountsViewModel.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.getMessages().observe(getViewLifecycleOwner(), messages -> {
ConversationAdapter adapter = getListAdapter();
if (adapter != null) {
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() {
multiselectItemDecoration.setFocusedItem(null);
list.invalidateItemDecorations();
reactionsShade.setVisibility(View.GONE);
reactionsShade.setVisibility(View.INVISIBLE);
}
private void updateConversationItemTimestamps() {
@ -1040,7 +1022,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
.submit();
} else if (conversation.getMessageRequestData().isMessageRequestAccepted()) {
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)
.submit();
} else {
@ -1064,7 +1046,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
}
private boolean isTypingIndicatorShowing() {
return getListAdapter().hasHeader();
return getListAdapter().isTypingViewEnabled();
}
public void onSearchQueryUpdated(@Nullable String query) {

Wyświetl plik

@ -6,6 +6,7 @@ import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
@ -37,8 +38,10 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.util.livedata.Store;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.whispersystems.libsignal.util.Pair;
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> inlinePlayerHeight;
private final LiveData<Integer> conversationTopMargin;
private final Store<ThreadAnimationState> threadAnimationStateStore;
private final Observer<ThreadAnimationState> threadAnimationStateStoreDriver;
private final Map<GroupId, Set<Recipient>> sessionMemberCache = new HashMap<>();
@ -83,22 +88,23 @@ public class ConversationViewModel extends ViewModel {
private int jumpToPosition;
private ConversationViewModel() {
this.context = ApplicationDependencies.getApplication();
this.mediaRepository = new MediaRepository();
this.conversationRepository = new ConversationRepository();
this.recentMedia = new MutableLiveData<>();
this.threadId = new MutableLiveData<>();
this.showScrollButtons = new MutableLiveData<>(false);
this.hasUnreadMentions = new MutableLiveData<>(false);
this.recipientId = new MutableLiveData<>();
this.events = new SingleLiveEvent<>();
this.pagingController = new ProxyPagingController<>();
this.conversationObserver = pagingController::onDataInvalidated;
this.messageUpdateObserver = pagingController::onDataItemChanged;
this.messageInsertObserver = messageId -> pagingController.onDataItemInserted(messageId, 0);
this.toolbarBottom = new MutableLiveData<>();
this.inlinePlayerHeight = new MutableLiveData<>();
this.conversationTopMargin = Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(toolbarBottom, inlinePlayerHeight, Integer::sum));
this.context = ApplicationDependencies.getApplication();
this.mediaRepository = new MediaRepository();
this.conversationRepository = new ConversationRepository();
this.recentMedia = new MutableLiveData<>();
this.threadId = new MutableLiveData<>();
this.showScrollButtons = new MutableLiveData<>(false);
this.hasUnreadMentions = new MutableLiveData<>(false);
this.recipientId = new MutableLiveData<>();
this.events = new SingleLiveEvent<>();
this.pagingController = new ProxyPagingController<>();
this.conversationObserver = pagingController::onDataInvalidated;
this.messageUpdateObserver = pagingController::onDataItemChanged;
this.messageInsertObserver = messageId -> pagingController.onDataItemInserted(messageId, 0);
this.toolbarBottom = new MutableLiveData<>();
this.inlinePlayerHeight = new MutableLiveData<>();
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<ThreadAndRecipient> threadAndRecipient = LiveDataUtil.combineLatest(threadId, recipientLiveData, ThreadAndRecipient::new);
@ -158,6 +164,41 @@ public class ConversationViewModel extends ViewModel {
chatColors = LiveDataUtil.mapDistinct(Transformations.switchMap(recipientId,
id -> Recipient.live(id).getLiveData()),
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) {
@ -301,6 +342,7 @@ public class ConversationViewModel extends ViewModel {
@Override
protected void onCleared() {
super.onCleared();
threadAnimationStateStore.getStateLiveData().removeObserver(threadAnimationStateStoreDriver);
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver);
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver);
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver);

Wyświetl plik

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

Wyświetl plik

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation.mutiselect
import android.animation.ObjectAnimator
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import androidx.core.animation.doOnEnd
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.
*/
class MultiselectItemAnimator(
class ConversationItemAnimator(
private val isInMultiSelectMode: () -> Boolean,
private val isLoadingInitialContent: () -> Boolean,
private val shouldPlayMessageAnimations: () -> Boolean,
private val isParentFilled: () -> Boolean
) : RecyclerView.ItemAnimator() {
@ -23,14 +24,30 @@ class MultiselectItemAnimator(
CHANGE
}
private val pendingSlideAnimations: MutableSet<RecyclerView.ViewHolder> = mutableSetOf()
private var pendingTypingViewSlideOut: RecyclerView.ViewHolder? = null
private data class TweeningInfo(
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 {
if (viewHolder is ConversationAdapter.HeaderViewHolder && pendingTypingViewSlideOut == null) {
pendingTypingViewSlideOut = viewHolder
if (viewHolder is ConversationAdapter.HeaderViewHolder &&
!pendingSlideAnimations.containsKey(viewHolder) &&
!slideAnimations.containsKey(viewHolder) &&
shouldPlayMessageAnimations()
) {
pendingSlideAnimations[viewHolder] = TweeningInfo(0f, viewHolder.itemView.height.toFloat())
dispatchAnimationStarted(viewHolder)
return true
}
@ -49,7 +66,7 @@ class MultiselectItemAnimator(
}
private fun animateSlide(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo?, postLayoutInfo: ItemHolderInfo, operation: Operation): Boolean {
if (isInMultiSelectMode() || isLoadingInitialContent()) {
if (isInMultiSelectMode() || !shouldPlayMessageAnimations()) {
dispatchAnimationFinished(viewHolder)
return false
}
@ -77,14 +94,14 @@ class MultiselectItemAnimator(
viewHolder.itemView.translationY = translationY
pendingSlideAnimations.add(viewHolder)
pendingSlideAnimations[viewHolder] = TweeningInfo(translationY, 0f)
dispatchAnimationStarted(viewHolder)
return true
}
override fun animatePersistence(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo, postLayoutInfo: ItemHolderInfo): Boolean {
val isInMultiSelectMode = isInMultiSelectMode()
return if (!isInMultiSelectMode) {
return if (!isInMultiSelectMode && shouldPlayMessageAnimations()) {
if (pendingSlideAnimations.contains(viewHolder) || slideAnimations.containsKey(viewHolder)) {
dispatchAnimationFinished(viewHolder)
false
@ -107,51 +124,41 @@ class MultiselectItemAnimator(
override fun runPendingAnimations() {
runPendingSlideAnimations()
runPendingSlideOutAnimation()
}
private fun runPendingSlideAnimations() {
for (viewHolder in pendingSlideAnimations) {
val animator = ObjectAnimator.ofFloat(viewHolder.itemView, "translationY", 0f)
slideAnimations[viewHolder]?.cancel()
slideAnimations[viewHolder] = animator
val animators: MutableList<Animator> = mutableListOf()
for ((viewHolder, tweeningInfo) in pendingSlideAnimations) {
val animator = ValueAnimator.ofFloat(0f, 1f)
slideAnimations[viewHolder] = AnimationInfo(animator, tweeningInfo)
animator.duration = 150L
animator.addUpdateListener {
(viewHolder.itemView.parent as RecyclerView?)?.invalidate()
if (viewHolder in slideAnimations) {
viewHolder.itemView.translationY = tweeningInfo.lerp(it.animatedFraction)
(viewHolder.itemView.parent as RecyclerView?)?.invalidate()
}
}
animator.doOnEnd {
viewHolder.itemView.translationY = 0f
slideAnimations.remove(viewHolder)
dispatchAnimationFinished(viewHolder)
dispatchFinishedWhenDone()
if (viewHolder in slideAnimations) {
handleAnimationEnd(viewHolder)
}
}
animator.start()
animators.add(animator)
}
AnimatorSet().apply {
playTogether(animators)
start()
}
pendingSlideAnimations.clear()
}
private fun runPendingSlideOutAnimation() {
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
slideAnimations.remove(viewHolder)
dispatchAnimationFinished(viewHolder)
dispatchFinishedWhenDone()
}
animator.start()
}
private fun handleAnimationEnd(viewHolder: RecyclerView.ViewHolder) {
viewHolder.itemView.translationY = 0f
slideAnimations.remove(viewHolder)
dispatchAnimationFinished(viewHolder)
dispatchFinishedWhenDone()
}
override fun endAnimation(item: RecyclerView.ViewHolder) {
@ -164,7 +171,7 @@ class MultiselectItemAnimator(
}
override fun isRunning(): Boolean {
return slideAnimations.values.any { it.isRunning }
return slideAnimations.values.any { it.sharedAnimator.isRunning }
}
override fun onAnimationFinished(viewHolder: RecyclerView.ViewHolder) {
@ -173,11 +180,13 @@ class MultiselectItemAnimator(
}
private fun endSlideAnimation(item: RecyclerView.ViewHolder) {
slideAnimations[item]?.cancel()
slideAnimations[item]?.sharedAnimator?.cancel()
}
fun endSlideAnimations() {
slideAnimations.values.forEach { it.cancel() }
slideAnimations.values.map { it.sharedAnimator }.forEach {
it.cancel()
}
}
private fun dispatchFinishedWhenDone() {

Wyświetl plik

@ -12,7 +12,7 @@
android:layout_height="0dp"
android:background="@color/reactions_screen_shade_color"
app:layout_constraintTop_toBottomOf="@android:id/list"
android:visibility="gone" />
android:visibility="invisible" />
<FrameLayout
android:id="@+id/video_container"