kopia lustrzana https://github.com/ryukoposting/Signal-Android
Fix various issues with conversation animation.
rodzic
1581a6e1cc
commit
9815851bb9
|
@ -97,7 +97,7 @@ public class ConversationAdapter
|
|||
private static final int MESSAGE_TYPE_INCOMING_TEXT = 3;
|
||||
private static final int MESSAGE_TYPE_UPDATE = 4;
|
||||
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 PAYLOAD_TIMESTAMP = 0;
|
||||
|
@ -118,13 +118,14 @@ public class ConversationAdapter
|
|||
|
||||
private String searchQuery;
|
||||
private ConversationMessage recordToPulse;
|
||||
private View headerView;
|
||||
private View typingView;
|
||||
private View footerView;
|
||||
private PagingController pagingController;
|
||||
private boolean hasWallpaper;
|
||||
private boolean isMessageRequestAccepted;
|
||||
private ConversationMessage inlineContent;
|
||||
private Colorizer colorizer;
|
||||
private boolean isTypingViewEnabled;
|
||||
|
||||
ConversationAdapter(@NonNull Context context,
|
||||
@NonNull LifecycleOwner lifecycleOwner,
|
||||
|
@ -221,6 +222,7 @@ public class ConversationAdapter
|
|||
v.setLayoutParams(new FrameLayout.LayoutParams(1, ViewUtil.dpToPx(100)));
|
||||
return new PlaceholderViewHolder(v);
|
||||
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:
|
||||
return new HeaderFooterViewHolder(CachedInflater.from(parent.getContext()).inflate(R.layout.cursor_adapter_header_footer_view, parent, false));
|
||||
default:
|
||||
|
@ -293,7 +295,7 @@ public class ConversationAdapter
|
|||
}
|
||||
break;
|
||||
case MESSAGE_TYPE_HEADER:
|
||||
((HeaderFooterViewHolder) holder).bind(headerView);
|
||||
((HeaderViewHolder) holder).bind(typingView);
|
||||
break;
|
||||
case MESSAGE_TYPE_FOOTER:
|
||||
((HeaderFooterViewHolder) holder).bind(footerView);
|
||||
|
@ -303,17 +305,14 @@ public class ConversationAdapter
|
|||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
boolean hasHeader = headerView != 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
|
||||
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
|
||||
if (holder instanceof ConversationViewHolder) {
|
||||
((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).
|
||||
*/
|
||||
void setHeaderView(@Nullable View view) {
|
||||
boolean hadHeader = hasHeader();
|
||||
|
||||
this.headerView = view;
|
||||
|
||||
if (view == null && hadHeader) {
|
||||
notifyItemRemoved(0);
|
||||
} else if (view != null && hadHeader) {
|
||||
notifyItemChanged(0);
|
||||
} else if (view != null) {
|
||||
notifyItemInserted(0);
|
||||
}
|
||||
void setTypingView(@NonNull View view) {
|
||||
this.typingView = view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the header view, if one was set.
|
||||
*/
|
||||
@Nullable View getHeaderView() {
|
||||
return headerView;
|
||||
void setTypingViewEnabled(boolean isTypingViewEnabled) {
|
||||
if (typingView == null && isTypingViewEnabled) {
|
||||
throw new IllegalStateException("Must set header before enabling.");
|
||||
}
|
||||
|
||||
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() {
|
||||
return headerView != null;
|
||||
public boolean hasHeader() {
|
||||
return isTypingViewEnabled;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
@ -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 {
|
||||
PlaceholderViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
|
|
@ -25,7 +25,6 @@ import android.content.res.Configuration;
|
|||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
|
@ -36,7 +35,6 @@ import android.view.MenuItem;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.Window;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.FrameLayout;
|
||||
|
@ -55,7 +53,9 @@ 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;
|
||||
|
@ -154,11 +154,11 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
|||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
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,7 +223,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
private ConversationUpdateTick conversationUpdateTick;
|
||||
private MultiselectItemDecoration multiselectItemDecoration;
|
||||
|
||||
private int listSubmissionCount = 0;
|
||||
private boolean initialDataLoaded = false;
|
||||
|
||||
public static void prepare(@NonNull Context context) {
|
||||
FrameLayout parent = new FrameLayout(context);
|
||||
|
@ -266,7 +266,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
} else {
|
||||
return Util.hasItems(adapter.getSelectedItems());
|
||||
}
|
||||
}, () -> listSubmissionCount < 2, () -> list.canScrollVertically(1) || list.canScrollVertically(-1));
|
||||
}, () -> !initialDataLoaded, () -> list.canScrollVertically(1) || list.canScrollVertically(-1));
|
||||
multiselectItemDecoration = new MultiselectItemDecoration(requireContext(),
|
||||
() -> conversationViewModel.getWallpaper().getValue());
|
||||
|
||||
|
@ -301,19 +301,31 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
this::onViewHolderPositionTranslated
|
||||
).attachToRecyclerView(list);
|
||||
|
||||
setupListLayoutListeners();
|
||||
giphyMp4ProjectionRecycler = initializeGiphyMp4();
|
||||
|
||||
this.groupViewModel = ViewModelProviders.of(requireActivity(), new ConversationGroupViewModel.Factory()).get(ConversationGroupViewModel.class);
|
||||
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, () -> {
|
||||
listSubmissionCount++;
|
||||
hasSubmittedNonEmptyList.postValue(!messages.isEmpty());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -382,35 +394,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
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() {
|
||||
ConversationAdapter conversationAdapter = getListAdapter();
|
||||
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());
|
||||
|
||||
ConversationAdapter adapter = getListAdapter();
|
||||
|
||||
if (adapter.getHeaderView() != null && adapter.getHeaderView() != typingView) {
|
||||
Log.i(TAG, "Skipping typing indicator -- the header slot is occupied.");
|
||||
return;
|
||||
}
|
||||
adapter.setTypingView(typingView);
|
||||
|
||||
if (recipients.size() > 0) {
|
||||
if (!isTypingIndicatorShowing() && isAtBottom()) {
|
||||
Context context = requireContext();
|
||||
list.setVerticalScrollBarEnabled(false);
|
||||
list.post(() -> {
|
||||
if (!isReacting) {
|
||||
getListLayoutManager().smoothScrollToPosition(context, 0, 250);
|
||||
}
|
||||
});
|
||||
list.postDelayed(() -> list.setVerticalScrollBarEnabled(true), 300);
|
||||
adapter.setHeaderView(typingView);
|
||||
adapter.setTypingViewEnabled(true);
|
||||
list.scrollToPosition(0);
|
||||
} else {
|
||||
if (isTypingIndicatorShowing()) {
|
||||
adapter.setHeaderView(typingView);
|
||||
} else {
|
||||
adapter.setHeaderView(typingView);
|
||||
}
|
||||
adapter.setTypingViewEnabled(true);
|
||||
}
|
||||
} else {
|
||||
if (isTypingIndicatorShowing() && getListLayoutManager().findFirstCompletelyVisibleItemPosition() == 0 && getListLayoutManager().getItemCount() > 1 && !replacedByIncomingMessage) {
|
||||
if (!isReacting) {
|
||||
getListLayoutManager().smoothScrollToPosition(requireContext(), 1, 250);
|
||||
}
|
||||
list.setVerticalScrollBarEnabled(false);
|
||||
list.postDelayed(() -> {
|
||||
adapter.setHeaderView(null);
|
||||
list.post(() -> list.setVerticalScrollBarEnabled(true));
|
||||
}, 200);
|
||||
adapter.setTypingViewEnabled(false);
|
||||
} else if (!replacedByIncomingMessage) {
|
||||
adapter.setHeaderView(null);
|
||||
adapter.setTypingViewEnabled(false);
|
||||
} else {
|
||||
adapter.setHeaderView(null);
|
||||
adapter.setTypingViewEnabled(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1023,17 +984,10 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void clearHeaderIfNotTyping(ConversationAdapter adapter) {
|
||||
if (adapter.getHeaderView() != typingView) {
|
||||
adapter.setHeaderView(null);
|
||||
}
|
||||
}
|
||||
|
||||
public long stageOutgoingMessage(OutgoingMediaMessage message) {
|
||||
MessageRecord messageRecord = MmsDatabase.readerFor(message, threadId).getCurrent();
|
||||
|
||||
if (getListAdapter() != null) {
|
||||
clearHeaderIfNotTyping(getListAdapter());
|
||||
setLastSeen(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();
|
||||
|
||||
if (getListAdapter() != null) {
|
||||
clearHeaderIfNotTyping(getListAdapter());
|
||||
setLastSeen(0);
|
||||
list.post(() -> list.scrollToPosition(0));
|
||||
}
|
||||
|
@ -1068,8 +1021,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
|
||||
setLastSeen(conversation.getLastSeen());
|
||||
|
||||
clearHeaderIfNotTyping(adapter);
|
||||
|
||||
listener.onCursorChanged();
|
||||
|
||||
conversationScrollListener.onScrolled(list, 0, 0);
|
||||
|
@ -1113,7 +1064,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
}
|
||||
|
||||
private boolean isTypingIndicatorShowing() {
|
||||
return getListAdapter().getHeaderView() == typingView;
|
||||
return getListAdapter().hasHeader();
|
||||
}
|
||||
|
||||
public void onSearchQueryUpdated(@Nullable String query) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.animation.ObjectAnimator
|
|||
import android.animation.ValueAnimator
|
||||
import androidx.core.animation.doOnEnd
|
||||
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
|
||||
|
@ -17,26 +18,33 @@ class MultiselectItemAnimator(
|
|||
private val isParentFilled: () -> Boolean
|
||||
) : RecyclerView.ItemAnimator() {
|
||||
|
||||
private data class SlideInfo(
|
||||
val viewHolder: RecyclerView.ViewHolder,
|
||||
val operation: Operation
|
||||
)
|
||||
|
||||
private enum class Operation {
|
||||
ADD,
|
||||
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 {
|
||||
if (viewHolder is ConversationAdapter.HeaderViewHolder && pendingTypingViewSlideOut == null) {
|
||||
pendingTypingViewSlideOut = viewHolder
|
||||
dispatchAnimationStarted(viewHolder)
|
||||
return true
|
||||
}
|
||||
|
||||
dispatchAnimationFinished(viewHolder)
|
||||
return false
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -46,7 +54,12 @@ class MultiselectItemAnimator(
|
|||
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)
|
||||
return false
|
||||
}
|
||||
|
@ -57,22 +70,31 @@ class MultiselectItemAnimator(
|
|||
preLayoutInfo.top - postLayoutInfo.top
|
||||
}.toFloat()
|
||||
|
||||
viewHolder.itemView.translationY = translationY
|
||||
val slideInfo = SlideInfo(viewHolder, operation)
|
||||
|
||||
if (slideAnimations.filterKeys { slideInfo.viewHolder == viewHolder }.isNotEmpty()) {
|
||||
if (translationY == 0f) {
|
||||
dispatchAnimationFinished(viewHolder)
|
||||
return false
|
||||
}
|
||||
|
||||
pendingSlideAnimations.add(slideInfo)
|
||||
viewHolder.itemView.translationY = translationY
|
||||
|
||||
pendingSlideAnimations.add(viewHolder)
|
||||
dispatchAnimationStarted(viewHolder)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun animatePersistence(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo, postLayoutInfo: ItemHolderInfo): Boolean {
|
||||
dispatchAnimationFinished(viewHolder)
|
||||
return false
|
||||
val isInMultiSelectMode = isInMultiSelectMode()
|
||||
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 {
|
||||
|
@ -80,35 +102,28 @@ class MultiselectItemAnimator(
|
|||
dispatchAnimationFinished(oldHolder)
|
||||
}
|
||||
|
||||
val isInMultiSelectMode = isInMultiSelectMode()
|
||||
return if (!isInMultiSelectMode) {
|
||||
if (preLayoutInfo.top == postLayoutInfo.top) {
|
||||
dispatchAnimationFinished(newHolder)
|
||||
false
|
||||
} else {
|
||||
animateSlide(newHolder, preLayoutInfo, postLayoutInfo, Operation.CHANGE)
|
||||
}
|
||||
} else {
|
||||
dispatchAnimationFinished(newHolder)
|
||||
false
|
||||
}
|
||||
return animatePersistence(newHolder, preLayoutInfo, postLayoutInfo)
|
||||
}
|
||||
|
||||
override fun runPendingAnimations() {
|
||||
runPendingSlideAnimations()
|
||||
runPendingSlideOutAnimation()
|
||||
}
|
||||
|
||||
private fun runPendingSlideAnimations() {
|
||||
for (slideInfo in pendingSlideAnimations) {
|
||||
val animator = ObjectAnimator.ofFloat(slideInfo.viewHolder.itemView, "translationY", 0f)
|
||||
slideAnimations[slideInfo] = animator
|
||||
for (viewHolder in pendingSlideAnimations) {
|
||||
val animator = ObjectAnimator.ofFloat(viewHolder.itemView, "translationY", 0f)
|
||||
slideAnimations[viewHolder]?.cancel()
|
||||
slideAnimations[viewHolder] = animator
|
||||
animator.duration = 150L
|
||||
animator.addUpdateListener {
|
||||
(slideInfo.viewHolder.itemView.parent as RecyclerView?)?.invalidate()
|
||||
(viewHolder.itemView.parent as RecyclerView?)?.invalidate()
|
||||
}
|
||||
animator.doOnEnd {
|
||||
dispatchAnimationFinished(slideInfo.viewHolder)
|
||||
slideAnimations.remove(slideInfo)
|
||||
viewHolder.itemView.translationY = 0f
|
||||
slideAnimations.remove(viewHolder)
|
||||
dispatchAnimationFinished(viewHolder)
|
||||
dispatchFinishedWhenDone()
|
||||
}
|
||||
animator.start()
|
||||
}
|
||||
|
@ -116,6 +131,29 @@ class MultiselectItemAnimator(
|
|||
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) {
|
||||
endSlideAnimation(item)
|
||||
}
|
||||
|
@ -135,15 +173,16 @@ 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)
|
||||
}
|
||||
slideAnimations[item]?.cancel()
|
||||
}
|
||||
|
||||
fun endSlideAnimations() {
|
||||
slideAnimations.values.forEach { it.end() }
|
||||
slideAnimations.clear()
|
||||
slideAnimations.values.forEach { it.cancel() }
|
||||
}
|
||||
|
||||
private fun dispatchFinishedWhenDone() {
|
||||
if (!isRunning) {
|
||||
dispatchAnimationsFinished()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,12 +26,14 @@
|
|||
<org.thoughtcrime.securesms.conversation.mutiselect.MultiselectRecyclerView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:cacheColorHint="@color/signal_background_primary"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="2dp"
|
||||
android:scrollbars="vertical" />
|
||||
android:scrollbars="vertical"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scroll_date_header"
|
||||
|
|
Ładowanie…
Reference in New Issue