Fix various issues with conversation animation.

fork-5.53.8
Alex Hart 2021-10-22 23:35:51 -03:00 zatwierdzone przez Greyson Parrelli
rodzic 1581a6e1cc
commit 9815851bb9
4 zmienionych plików z 141 dodań i 145 usunięć

Wyświetl plik

@ -97,7 +97,7 @@ public class ConversationAdapter
private static final int MESSAGE_TYPE_INCOMING_TEXT = 3; private static final int MESSAGE_TYPE_INCOMING_TEXT = 3;
private static final int MESSAGE_TYPE_UPDATE = 4; private static final int MESSAGE_TYPE_UPDATE = 4;
private static final int MESSAGE_TYPE_HEADER = 5; private static final int MESSAGE_TYPE_HEADER = 5;
private static final int MESSAGE_TYPE_FOOTER = 6; public static final int MESSAGE_TYPE_FOOTER = 6;
private static final int MESSAGE_TYPE_PLACEHOLDER = 7; private static final int MESSAGE_TYPE_PLACEHOLDER = 7;
private static final int PAYLOAD_TIMESTAMP = 0; private static final int PAYLOAD_TIMESTAMP = 0;
@ -118,13 +118,14 @@ public class ConversationAdapter
private String searchQuery; private String searchQuery;
private ConversationMessage recordToPulse; private ConversationMessage recordToPulse;
private View headerView; private View typingView;
private View footerView; private View footerView;
private PagingController pagingController; private PagingController pagingController;
private boolean hasWallpaper; private boolean hasWallpaper;
private boolean isMessageRequestAccepted; private boolean isMessageRequestAccepted;
private ConversationMessage inlineContent; private ConversationMessage inlineContent;
private Colorizer colorizer; private Colorizer colorizer;
private boolean isTypingViewEnabled;
ConversationAdapter(@NonNull Context context, ConversationAdapter(@NonNull Context context,
@NonNull LifecycleOwner lifecycleOwner, @NonNull LifecycleOwner lifecycleOwner,
@ -221,6 +222,7 @@ public class ConversationAdapter
v.setLayoutParams(new FrameLayout.LayoutParams(1, ViewUtil.dpToPx(100))); v.setLayoutParams(new FrameLayout.LayoutParams(1, ViewUtil.dpToPx(100)));
return new PlaceholderViewHolder(v); return new PlaceholderViewHolder(v);
case MESSAGE_TYPE_HEADER: case MESSAGE_TYPE_HEADER:
return new HeaderViewHolder(CachedInflater.from(parent.getContext()).inflate(R.layout.cursor_adapter_header_footer_view, parent, false));
case MESSAGE_TYPE_FOOTER: case MESSAGE_TYPE_FOOTER:
return new HeaderFooterViewHolder(CachedInflater.from(parent.getContext()).inflate(R.layout.cursor_adapter_header_footer_view, parent, false)); return new HeaderFooterViewHolder(CachedInflater.from(parent.getContext()).inflate(R.layout.cursor_adapter_header_footer_view, parent, false));
default: default:
@ -293,7 +295,7 @@ public class ConversationAdapter
} }
break; break;
case MESSAGE_TYPE_HEADER: case MESSAGE_TYPE_HEADER:
((HeaderFooterViewHolder) holder).bind(headerView); ((HeaderViewHolder) holder).bind(typingView);
break; break;
case MESSAGE_TYPE_FOOTER: case MESSAGE_TYPE_FOOTER:
((HeaderFooterViewHolder) holder).bind(footerView); ((HeaderFooterViewHolder) holder).bind(footerView);
@ -303,17 +305,14 @@ public class ConversationAdapter
@Override @Override
public int getItemCount() { public int getItemCount() {
boolean hasHeader = headerView != null;
boolean hasFooter = footerView != null; boolean hasFooter = footerView != null;
return super.getItemCount() + fastRecords.size() + (hasHeader ? 1 : 0) + (hasFooter ? 1 : 0); return super.getItemCount() + fastRecords.size() + (isTypingViewEnabled ? 1 : 0) + (hasFooter ? 1 : 0);
} }
@Override @Override
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
if (holder instanceof ConversationViewHolder) { if (holder instanceof ConversationViewHolder) {
((ConversationViewHolder) holder).getBindable().unbind(); ((ConversationViewHolder) holder).getBindable().unbind();
} else if (holder instanceof HeaderFooterViewHolder) {
((HeaderFooterViewHolder) holder).unbind();
} }
} }
@ -465,25 +464,24 @@ public class ConversationAdapter
/** /**
* Sets the view that appears at the bottom of the list (because the list is reversed). * Sets the view that appears at the bottom of the list (because the list is reversed).
*/ */
void setHeaderView(@Nullable View view) { void setTypingView(@NonNull View view) {
boolean hadHeader = hasHeader(); this.typingView = view;
this.headerView = view;
if (view == null && hadHeader) {
notifyItemRemoved(0);
} else if (view != null && hadHeader) {
notifyItemChanged(0);
} else if (view != null) {
notifyItemInserted(0);
}
} }
/** void setTypingViewEnabled(boolean isTypingViewEnabled) {
* Returns the header view, if one was set. if (typingView == null && isTypingViewEnabled) {
*/ throw new IllegalStateException("Must set header before enabling.");
@Nullable View getHeaderView() { }
return headerView;
if (this.isTypingViewEnabled && !isTypingViewEnabled) {
this.isTypingViewEnabled = false;
notifyItemRemoved(0);
} else if (this.isTypingViewEnabled) {
notifyItemChanged(0);
} else if (isTypingViewEnabled) {
this.isTypingViewEnabled = true;
notifyItemInserted(0);
}
} }
/** /**
@ -604,8 +602,8 @@ public class ConversationAdapter
} }
} }
private boolean hasHeader() { public boolean hasHeader() {
return headerView != null; return isTypingViewEnabled;
} }
public boolean hasFooter() { public boolean hasFooter() {
@ -749,7 +747,7 @@ public class ConversationAdapter
} }
} }
private static class HeaderFooterViewHolder extends RecyclerView.ViewHolder { public static class HeaderFooterViewHolder extends RecyclerView.ViewHolder {
private ViewGroup container; private ViewGroup container;
@ -778,6 +776,12 @@ public class ConversationAdapter
} }
} }
public static class HeaderViewHolder extends HeaderFooterViewHolder {
HeaderViewHolder(@NonNull View itemView) {
super(itemView);
}
}
private static class PlaceholderViewHolder extends RecyclerView.ViewHolder { private static class PlaceholderViewHolder extends RecyclerView.ViewHolder {
PlaceholderViewHolder(@NonNull View itemView) { PlaceholderViewHolder(@NonNull View itemView) {
super(itemView); super(itemView);

Wyświetl plik

@ -25,7 +25,6 @@ import android.content.res.Configuration;
import android.graphics.Rect; import android.graphics.Rect;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
@ -36,7 +35,6 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver; import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.animation.Animation; import android.view.animation.Animation;
import android.view.animation.AnimationUtils; import android.view.animation.AnimationUtils;
import android.widget.FrameLayout; import android.widget.FrameLayout;
@ -55,7 +53,9 @@ 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;
@ -154,11 +154,11 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.StorageUtil; import org.thoughtcrime.securesms.util.StorageUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util; 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,7 +223,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
private ConversationUpdateTick conversationUpdateTick; private ConversationUpdateTick conversationUpdateTick;
private MultiselectItemDecoration multiselectItemDecoration; private MultiselectItemDecoration multiselectItemDecoration;
private int listSubmissionCount = 0; 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);
@ -266,7 +266,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
} else { } else {
return Util.hasItems(adapter.getSelectedItems()); return Util.hasItems(adapter.getSelectedItems());
} }
}, () -> listSubmissionCount < 2, () -> list.canScrollVertically(1) || list.canScrollVertically(-1)); }, () -> !initialDataLoaded, () -> list.canScrollVertically(1) || list.canScrollVertically(-1));
multiselectItemDecoration = new MultiselectItemDecoration(requireContext(), multiselectItemDecoration = new MultiselectItemDecoration(requireContext(),
() -> conversationViewModel.getWallpaper().getValue()); () -> conversationViewModel.getWallpaper().getValue());
@ -301,19 +301,31 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
this::onViewHolderPositionTranslated this::onViewHolderPositionTranslated
).attachToRecyclerView(list); ).attachToRecyclerView(list);
setupListLayoutListeners();
giphyMp4ProjectionRecycler = initializeGiphyMp4(); giphyMp4ProjectionRecycler = initializeGiphyMp4();
this.groupViewModel = ViewModelProviders.of(requireActivity(), new ConversationGroupViewModel.Factory()).get(ConversationGroupViewModel.class); this.groupViewModel = ViewModelProviders.of(requireActivity(), new ConversationGroupViewModel.Factory()).get(ConversationGroupViewModel.class);
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, () -> {
listSubmissionCount++; hasSubmittedNonEmptyList.postValue(!messages.isEmpty());
}); });
} }
}); });
@ -382,35 +394,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
reactionsShade.setVisibility(View.GONE); reactionsShade.setVisibility(View.GONE);
} }
private void setupListLayoutListeners() {
list.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> setListVerticalTranslation());
list.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() {
@Override
public void onChildViewAttachedToWindow(@NonNull View view) {
setListVerticalTranslation();
}
@Override
public void onChildViewDetachedFromWindow(@NonNull View view) {
setListVerticalTranslation();
}
});
}
private void setListVerticalTranslation() {
if (list.canScrollVertically(1) || list.canScrollVertically(-1) || list.getChildCount() == 0) {
list.setTranslationY(0);
reactionsShade.setTranslationY(0);
list.setOverScrollMode(RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS);
} else {
int chTop = list.getChildAt(list.getChildCount() - 1).getTop();
list.setTranslationY(Math.min(0, -chTop));
reactionsShade.setTranslationY(Math.min(0, -chTop));
list.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER);
}
}
private void updateConversationItemTimestamps() { private void updateConversationItemTimestamps() {
ConversationAdapter conversationAdapter = getListAdapter(); ConversationAdapter conversationAdapter = getListAdapter();
if (conversationAdapter != null) { if (conversationAdapter != null) {
@ -736,44 +719,22 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
typingView.setTypists(GlideApp.with(ConversationFragment.this), recipients, resolved.isGroup(), resolved.hasWallpaper()); typingView.setTypists(GlideApp.with(ConversationFragment.this), recipients, resolved.isGroup(), resolved.hasWallpaper());
ConversationAdapter adapter = getListAdapter(); ConversationAdapter adapter = getListAdapter();
adapter.setTypingView(typingView);
if (adapter.getHeaderView() != null && adapter.getHeaderView() != typingView) {
Log.i(TAG, "Skipping typing indicator -- the header slot is occupied.");
return;
}
if (recipients.size() > 0) { if (recipients.size() > 0) {
if (!isTypingIndicatorShowing() && isAtBottom()) { if (!isTypingIndicatorShowing() && isAtBottom()) {
Context context = requireContext(); adapter.setTypingViewEnabled(true);
list.setVerticalScrollBarEnabled(false); list.scrollToPosition(0);
list.post(() -> {
if (!isReacting) {
getListLayoutManager().smoothScrollToPosition(context, 0, 250);
}
});
list.postDelayed(() -> list.setVerticalScrollBarEnabled(true), 300);
adapter.setHeaderView(typingView);
} else { } else {
if (isTypingIndicatorShowing()) { adapter.setTypingViewEnabled(true);
adapter.setHeaderView(typingView);
} else {
adapter.setHeaderView(typingView);
}
} }
} else { } else {
if (isTypingIndicatorShowing() && getListLayoutManager().findFirstCompletelyVisibleItemPosition() == 0 && getListLayoutManager().getItemCount() > 1 && !replacedByIncomingMessage) { if (isTypingIndicatorShowing() && getListLayoutManager().findFirstCompletelyVisibleItemPosition() == 0 && getListLayoutManager().getItemCount() > 1 && !replacedByIncomingMessage) {
if (!isReacting) { adapter.setTypingViewEnabled(false);
getListLayoutManager().smoothScrollToPosition(requireContext(), 1, 250);
}
list.setVerticalScrollBarEnabled(false);
list.postDelayed(() -> {
adapter.setHeaderView(null);
list.post(() -> list.setVerticalScrollBarEnabled(true));
}, 200);
} else if (!replacedByIncomingMessage) { } else if (!replacedByIncomingMessage) {
adapter.setHeaderView(null); adapter.setTypingViewEnabled(false);
} else { } else {
adapter.setHeaderView(null); adapter.setTypingViewEnabled(false);
} }
} }
}); });
@ -1023,17 +984,10 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
Toast.LENGTH_LONG).show(); Toast.LENGTH_LONG).show();
} }
private void clearHeaderIfNotTyping(ConversationAdapter adapter) {
if (adapter.getHeaderView() != typingView) {
adapter.setHeaderView(null);
}
}
public long stageOutgoingMessage(OutgoingMediaMessage message) { public long stageOutgoingMessage(OutgoingMediaMessage message) {
MessageRecord messageRecord = MmsDatabase.readerFor(message, threadId).getCurrent(); MessageRecord messageRecord = MmsDatabase.readerFor(message, threadId).getCurrent();
if (getListAdapter() != null) { if (getListAdapter() != null) {
clearHeaderIfNotTyping(getListAdapter());
setLastSeen(0); setLastSeen(0);
list.post(() -> list.scrollToPosition(0)); list.post(() -> list.scrollToPosition(0));
} }
@ -1045,7 +999,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
MessageRecord messageRecord = SmsDatabase.readerFor(message, threadId, messageId).getCurrent(); MessageRecord messageRecord = SmsDatabase.readerFor(message, threadId, messageId).getCurrent();
if (getListAdapter() != null) { if (getListAdapter() != null) {
clearHeaderIfNotTyping(getListAdapter());
setLastSeen(0); setLastSeen(0);
list.post(() -> list.scrollToPosition(0)); list.post(() -> list.scrollToPosition(0));
} }
@ -1068,8 +1021,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
setLastSeen(conversation.getLastSeen()); setLastSeen(conversation.getLastSeen());
clearHeaderIfNotTyping(adapter);
listener.onCursorChanged(); listener.onCursorChanged();
conversationScrollListener.onScrolled(list, 0, 0); conversationScrollListener.onScrolled(list, 0, 0);
@ -1113,7 +1064,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
} }
private boolean isTypingIndicatorShowing() { private boolean isTypingIndicatorShowing() {
return getListAdapter().getHeaderView() == typingView; return getListAdapter().hasHeader();
} }
public void onSearchQueryUpdated(@Nullable String query) { public void onSearchQueryUpdated(@Nullable String query) {

Wyświetl plik

@ -4,6 +4,7 @@ import android.animation.ObjectAnimator
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
import org.thoughtcrime.securesms.conversation.ConversationAdapter
/** /**
* Class for managing the triggering of item animations (here in the form of decoration redraws) whenever * Class for managing the triggering of item animations (here in the form of decoration redraws) whenever
@ -17,26 +18,33 @@ class MultiselectItemAnimator(
private val isParentFilled: () -> Boolean private val isParentFilled: () -> Boolean
) : RecyclerView.ItemAnimator() { ) : RecyclerView.ItemAnimator() {
private data class SlideInfo(
val viewHolder: RecyclerView.ViewHolder,
val operation: Operation
)
private enum class Operation { private enum class Operation {
ADD, ADD,
CHANGE CHANGE
} }
private val pendingSlideAnimations: MutableSet<SlideInfo> = mutableSetOf() private val pendingSlideAnimations: MutableSet<RecyclerView.ViewHolder> = mutableSetOf()
private var pendingTypingViewSlideOut: RecyclerView.ViewHolder? = null
private val slideAnimations: MutableMap<SlideInfo, ValueAnimator> = mutableMapOf() private val slideAnimations: MutableMap<RecyclerView.ViewHolder, ValueAnimator> = 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) {
pendingTypingViewSlideOut = viewHolder
dispatchAnimationStarted(viewHolder)
return true
}
dispatchAnimationFinished(viewHolder) dispatchAnimationFinished(viewHolder)
return false return false
} }
override fun animateAppearance(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo?, postLayoutInfo: ItemHolderInfo): Boolean { override fun animateAppearance(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo?, postLayoutInfo: ItemHolderInfo): Boolean {
if (viewHolder.absoluteAdapterPosition > 1) {
dispatchAnimationFinished(viewHolder)
return false
}
return animateSlide(viewHolder, preLayoutInfo, postLayoutInfo, Operation.ADD) return animateSlide(viewHolder, preLayoutInfo, postLayoutInfo, Operation.ADD)
} }
@ -46,7 +54,12 @@ class MultiselectItemAnimator(
return false return false
} }
if (operation == Operation.CHANGE && !isParentFilled()) { if (operation == Operation.CHANGE && !isParentFilled() || slideAnimations.containsKey(viewHolder)) {
dispatchAnimationFinished(viewHolder)
return false
}
if (slideAnimations.containsKey(viewHolder)) {
dispatchAnimationFinished(viewHolder) dispatchAnimationFinished(viewHolder)
return false return false
} }
@ -57,22 +70,31 @@ class MultiselectItemAnimator(
preLayoutInfo.top - postLayoutInfo.top preLayoutInfo.top - postLayoutInfo.top
}.toFloat() }.toFloat()
viewHolder.itemView.translationY = translationY if (translationY == 0f) {
val slideInfo = SlideInfo(viewHolder, operation)
if (slideAnimations.filterKeys { slideInfo.viewHolder == viewHolder }.isNotEmpty()) {
dispatchAnimationFinished(viewHolder) dispatchAnimationFinished(viewHolder)
return false return false
} }
pendingSlideAnimations.add(slideInfo) viewHolder.itemView.translationY = translationY
pendingSlideAnimations.add(viewHolder)
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 {
dispatchAnimationFinished(viewHolder) val isInMultiSelectMode = isInMultiSelectMode()
return false return if (!isInMultiSelectMode) {
if (pendingSlideAnimations.contains(viewHolder) || slideAnimations.containsKey(viewHolder)) {
dispatchAnimationFinished(viewHolder)
false
} else {
animateSlide(viewHolder, preLayoutInfo, postLayoutInfo, Operation.CHANGE)
}
} else {
dispatchAnimationFinished(viewHolder)
false
}
} }
override fun animateChange(oldHolder: RecyclerView.ViewHolder, newHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo, postLayoutInfo: ItemHolderInfo): Boolean { override fun animateChange(oldHolder: RecyclerView.ViewHolder, newHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo, postLayoutInfo: ItemHolderInfo): Boolean {
@ -80,35 +102,28 @@ class MultiselectItemAnimator(
dispatchAnimationFinished(oldHolder) dispatchAnimationFinished(oldHolder)
} }
val isInMultiSelectMode = isInMultiSelectMode() return animatePersistence(newHolder, preLayoutInfo, postLayoutInfo)
return if (!isInMultiSelectMode) {
if (preLayoutInfo.top == postLayoutInfo.top) {
dispatchAnimationFinished(newHolder)
false
} else {
animateSlide(newHolder, preLayoutInfo, postLayoutInfo, Operation.CHANGE)
}
} else {
dispatchAnimationFinished(newHolder)
false
}
} }
override fun runPendingAnimations() { override fun runPendingAnimations() {
runPendingSlideAnimations() runPendingSlideAnimations()
runPendingSlideOutAnimation()
} }
private fun runPendingSlideAnimations() { private fun runPendingSlideAnimations() {
for (slideInfo in pendingSlideAnimations) { for (viewHolder in pendingSlideAnimations) {
val animator = ObjectAnimator.ofFloat(slideInfo.viewHolder.itemView, "translationY", 0f) val animator = ObjectAnimator.ofFloat(viewHolder.itemView, "translationY", 0f)
slideAnimations[slideInfo] = animator slideAnimations[viewHolder]?.cancel()
slideAnimations[viewHolder] = animator
animator.duration = 150L animator.duration = 150L
animator.addUpdateListener { animator.addUpdateListener {
(slideInfo.viewHolder.itemView.parent as RecyclerView?)?.invalidate() (viewHolder.itemView.parent as RecyclerView?)?.invalidate()
} }
animator.doOnEnd { animator.doOnEnd {
dispatchAnimationFinished(slideInfo.viewHolder) viewHolder.itemView.translationY = 0f
slideAnimations.remove(slideInfo) slideAnimations.remove(viewHolder)
dispatchAnimationFinished(viewHolder)
dispatchFinishedWhenDone()
} }
animator.start() animator.start()
} }
@ -116,6 +131,29 @@ class MultiselectItemAnimator(
pendingSlideAnimations.clear() 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()
}
}
override fun endAnimation(item: RecyclerView.ViewHolder) { override fun endAnimation(item: RecyclerView.ViewHolder) {
endSlideAnimation(item) endSlideAnimation(item)
} }
@ -135,15 +173,16 @@ class MultiselectItemAnimator(
} }
private fun endSlideAnimation(item: RecyclerView.ViewHolder) { private fun endSlideAnimation(item: RecyclerView.ViewHolder) {
val selections = slideAnimations.filter { (k, _) -> k.viewHolder == item } slideAnimations[item]?.cancel()
selections.forEach { (k, v) ->
v.end()
slideAnimations.remove(k)
}
} }
fun endSlideAnimations() { fun endSlideAnimations() {
slideAnimations.values.forEach { it.end() } slideAnimations.values.forEach { it.cancel() }
slideAnimations.clear() }
private fun dispatchFinishedWhenDone() {
if (!isRunning) {
dispatchAnimationsFinished()
}
} }
} }

Wyświetl plik

@ -26,12 +26,14 @@
<org.thoughtcrime.securesms.conversation.mutiselect.MultiselectRecyclerView <org.thoughtcrime.securesms.conversation.mutiselect.MultiselectRecyclerView
android:id="@android:id/list" android:id="@android:id/list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:cacheColorHint="@color/signal_background_primary" android:cacheColorHint="@color/signal_background_primary"
android:clipChildren="false" android:clipChildren="false"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="2dp" android:paddingBottom="2dp"
android:scrollbars="vertical" /> android:scrollbars="vertical"
android:overScrollMode="ifContentScrolls"
app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/scroll_date_header" android:id="@+id/scroll_date_header"