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_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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Ładowanie…
Reference in New Issue