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_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);

Wyświetl plik

@ -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) {

Wyświetl plik

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

Wyświetl plik

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