mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-22 02:36:55 +00:00
Use context menu when selecting a message in chat.
This commit is contained in:
committed by
Cody Henthorne
parent
d254d24d77
commit
e4d43ade93
@@ -0,0 +1,91 @@
|
||||
package org.thoughtcrime.securesms.components.menu
|
||||
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
/**
|
||||
* Handles the setup and display of actions shown in a context menu.
|
||||
*/
|
||||
class ContextMenuList(recyclerView: RecyclerView, onItemClick: () -> Unit) {
|
||||
|
||||
private val mappingAdapter = MappingAdapter().apply {
|
||||
registerFactory(DisplayItem::class.java, LayoutFactory({ ItemViewHolder(it, onItemClick) }, R.layout.signal_context_menu_item))
|
||||
}
|
||||
|
||||
init {
|
||||
recyclerView.apply {
|
||||
adapter = mappingAdapter
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
itemAnimator = null
|
||||
}
|
||||
}
|
||||
|
||||
fun setItems(items: List<ActionItem>) {
|
||||
mappingAdapter.submitList(items.toAdapterItems())
|
||||
}
|
||||
|
||||
private fun List<ActionItem>.toAdapterItems(): List<DisplayItem> {
|
||||
return this.mapIndexed { index, item ->
|
||||
val displayType: DisplayType = when {
|
||||
this.size == 1 -> DisplayType.ONLY
|
||||
index == 0 -> DisplayType.TOP
|
||||
index == this.size - 1 -> DisplayType.BOTTOM
|
||||
else -> DisplayType.MIDDLE
|
||||
}
|
||||
|
||||
DisplayItem(item, displayType)
|
||||
}
|
||||
}
|
||||
|
||||
private data class DisplayItem(
|
||||
val item: ActionItem,
|
||||
val displayType: DisplayType
|
||||
) : MappingModel<DisplayItem> {
|
||||
override fun areItemsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
}
|
||||
|
||||
private enum class DisplayType {
|
||||
TOP, BOTTOM, MIDDLE, ONLY
|
||||
}
|
||||
|
||||
private class ItemViewHolder(
|
||||
itemView: View,
|
||||
private val onItemClick: () -> Unit,
|
||||
) : MappingViewHolder<DisplayItem>(itemView) {
|
||||
val icon: ImageView = itemView.findViewById(R.id.signal_context_menu_item_icon)
|
||||
val title: TextView = itemView.findViewById(R.id.signal_context_menu_item_title)
|
||||
|
||||
override fun bind(model: DisplayItem) {
|
||||
icon.setImageResource(model.item.iconRes)
|
||||
title.text = model.item.title
|
||||
itemView.setOnClickListener {
|
||||
model.item.action.run()
|
||||
onItemClick()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
when (model.displayType) {
|
||||
DisplayType.TOP -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_top)
|
||||
DisplayType.BOTTOM -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_bottom)
|
||||
DisplayType.MIDDLE -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_middle)
|
||||
DisplayType.ONLY -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_only)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,18 +6,10 @@ import android.os.Build
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupWindow
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.Factory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
/**
|
||||
* A custom context menu that will show next to an anchor view and display several options. Basically a PopupMenu with custom UI and positioning rules.
|
||||
@@ -42,9 +34,10 @@ class SignalContextMenu private constructor(
|
||||
|
||||
val context: Context = anchor.context
|
||||
|
||||
val mappingAdapter = MappingAdapter().apply {
|
||||
registerFactory(DisplayItem::class.java, ItemViewHolderFactory())
|
||||
}
|
||||
private val contextMenuList = ContextMenuList(
|
||||
recyclerView = contentView.findViewById(R.id.signal_context_menu_list),
|
||||
onItemClick = { dismiss() },
|
||||
)
|
||||
|
||||
init {
|
||||
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.signal_context_menu_background))
|
||||
@@ -59,13 +52,7 @@ class SignalContextMenu private constructor(
|
||||
elevation = 20f
|
||||
}
|
||||
|
||||
contentView.findViewById<RecyclerView>(R.id.signal_context_menu_list).apply {
|
||||
adapter = mappingAdapter
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
itemAnimator = null
|
||||
}
|
||||
|
||||
mappingAdapter.submitList(items.toAdapterItems())
|
||||
contextMenuList.setItems(items)
|
||||
}
|
||||
|
||||
private fun show() {
|
||||
@@ -97,7 +84,7 @@ class SignalContextMenu private constructor(
|
||||
offsetY = baseOffsetY
|
||||
} else if (menuTopBound > screenTopBound) {
|
||||
offsetY = -(anchorRect.height() + contentView.measuredHeight + baseOffsetY)
|
||||
mappingAdapter.submitList(items.reversed().toAdapterItems())
|
||||
contextMenuList.setItems(items.reversed())
|
||||
} else {
|
||||
offsetY = -((anchorRect.height() / 2) + (contentView.measuredHeight / 2) + baseOffsetY)
|
||||
}
|
||||
@@ -122,65 +109,6 @@ class SignalContextMenu private constructor(
|
||||
showAsDropDown(anchor, offsetX, offsetY)
|
||||
}
|
||||
|
||||
private fun List<ActionItem>.toAdapterItems(): List<DisplayItem> {
|
||||
return this.mapIndexed { index, item ->
|
||||
val displayType: DisplayType = when {
|
||||
this.size == 1 -> DisplayType.ONLY
|
||||
index == 0 -> DisplayType.TOP
|
||||
index == this.size - 1 -> DisplayType.BOTTOM
|
||||
else -> DisplayType.MIDDLE
|
||||
}
|
||||
|
||||
DisplayItem(item, displayType)
|
||||
}
|
||||
}
|
||||
|
||||
private data class DisplayItem(
|
||||
val item: ActionItem,
|
||||
val displayType: DisplayType
|
||||
) : MappingModel<DisplayItem> {
|
||||
override fun areItemsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: DisplayItem): Boolean {
|
||||
return this == newItem
|
||||
}
|
||||
}
|
||||
|
||||
private enum class DisplayType {
|
||||
TOP, BOTTOM, MIDDLE, ONLY
|
||||
}
|
||||
|
||||
private inner class ItemViewHolder(itemView: View) : MappingViewHolder<DisplayItem>(itemView) {
|
||||
val icon: ImageView = itemView.findViewById(R.id.signal_context_menu_item_icon)
|
||||
val title: TextView = itemView.findViewById(R.id.signal_context_menu_item_title)
|
||||
|
||||
override fun bind(model: DisplayItem) {
|
||||
icon.setImageResource(model.item.iconRes)
|
||||
title.text = model.item.title
|
||||
itemView.setOnClickListener {
|
||||
model.item.action.run()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
when (model.displayType) {
|
||||
DisplayType.TOP -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_top)
|
||||
DisplayType.BOTTOM -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_bottom)
|
||||
DisplayType.MIDDLE -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_middle)
|
||||
DisplayType.ONLY -> itemView.setBackgroundResource(R.drawable.signal_context_menu_item_background_only)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ItemViewHolderFactory : Factory<DisplayItem> {
|
||||
override fun createViewHolder(parent: ViewGroup): MappingViewHolder<DisplayItem> {
|
||||
return ItemViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.signal_context_menu_item, parent, false))
|
||||
}
|
||||
}
|
||||
|
||||
enum class HorizontalPosition {
|
||||
START, END
|
||||
}
|
||||
|
||||
@@ -191,6 +191,27 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the Media service to resume playback of a given audio slide. If the audio slide is not
|
||||
* currently paused, playback will be started from the beginning.
|
||||
*
|
||||
* @param audioSlideUri The Uri of the desired audio slide
|
||||
* @param messageId The Message id of the given audio slide
|
||||
*/
|
||||
public void resumePlayback(@NonNull Uri audioSlideUri, long messageId) {
|
||||
if (isCurrentTrack(audioSlideUri)) {
|
||||
getMediaController().getTransportControls().play();
|
||||
} else {
|
||||
Bundle extras = new Bundle();
|
||||
extras.putLong(EXTRA_MESSAGE_ID, messageId);
|
||||
extras.putLong(EXTRA_THREAD_ID, -1L);
|
||||
extras.putDouble(EXTRA_PROGRESS, 0.0);
|
||||
extras.putBoolean(EXTRA_PLAY_SINGLE, true);
|
||||
|
||||
getMediaController().getTransportControls().playFromUri(audioSlideUri, extras);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses playback if the given audio slide is playing.
|
||||
*
|
||||
|
||||
@@ -706,6 +706,11 @@ public class ConversationAdapter
|
||||
return getBindable().canPlayContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldProjectContent() {
|
||||
return getBindable().shouldProjectContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) {
|
||||
return getBindable().getColorizerProjections(coordinateRoot);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.PopupWindow
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.ContextMenuList
|
||||
|
||||
/**
|
||||
* The context menu shown after long pressing a message in ConversationActivity.
|
||||
*/
|
||||
class ConversationContextMenu(private val anchor: View, items: List<ActionItem>) : PopupWindow(
|
||||
LayoutInflater.from(anchor.context).inflate(R.layout.signal_context_menu, null),
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
) {
|
||||
|
||||
val context: Context = anchor.context
|
||||
|
||||
private val contextMenuList = ContextMenuList(
|
||||
recyclerView = contentView.findViewById(R.id.signal_context_menu_list),
|
||||
onItemClick = { dismiss() },
|
||||
)
|
||||
|
||||
init {
|
||||
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.signal_context_menu_background))
|
||||
|
||||
isFocusable = false
|
||||
isOutsideTouchable = true
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
elevation = 20f
|
||||
}
|
||||
|
||||
contextMenuList.setItems(items)
|
||||
|
||||
contentView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||
)
|
||||
}
|
||||
|
||||
fun getMaxWidth(): Int = contentView.measuredWidth
|
||||
fun getMaxHeight(): Int = contentView.measuredHeight
|
||||
|
||||
fun show(offsetX: Int, offsetY: Int) {
|
||||
showAsDropDown(anchor, offsetX, offsetY, Gravity.TOP or Gravity.START)
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
@@ -148,6 +149,7 @@ import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.HtmlUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
@@ -180,7 +182,6 @@ import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import kotlin.Unit;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class ConversationFragment extends LoggingFragment implements MultiselectForwardFragment.Callback {
|
||||
@@ -196,7 +197,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
|
||||
private LiveRecipient recipient;
|
||||
private long threadId;
|
||||
private boolean isReacting;
|
||||
private ActionMode actionMode;
|
||||
private Locale locale;
|
||||
private FrameLayout videoContainer;
|
||||
@@ -768,7 +768,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
}
|
||||
|
||||
if (menuState.shouldShowSaveAttachmentAction()) {
|
||||
items.add(new ActionItem(R.drawable.ic_save_24, getResources().getString(R.string.conversation_selection__menu_save), () -> {
|
||||
items.add(new ActionItem(R.drawable.ic_save_24_tinted, getResources().getString(R.string.conversation_selection__menu_save), () -> {
|
||||
handleSaveAttachment((MediaMmsMessageRecord) getSelectedConversationMessage().getMessageRecord());
|
||||
actionMode.finish();
|
||||
}));
|
||||
@@ -810,19 +810,21 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
ViewUtil.animateIn(bottomActionBar, bottomActionBar.getEnterAnimation());
|
||||
listener.onBottomActionBarVisibilityChanged(View.VISIBLE);
|
||||
|
||||
ViewKt.doOnPreDraw(bottomActionBar, new Function1<View, Unit>() {
|
||||
@Override public Unit invoke(View view) {
|
||||
if (view.getHeight() == 0 && view.getVisibility() == View.VISIBLE) {
|
||||
ViewKt.doOnPreDraw(bottomActionBar, this);
|
||||
return Unit.INSTANCE;
|
||||
bottomActionBar.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
if (bottomActionBar.getHeight() == 0 && bottomActionBar.getVisibility() == View.VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int bottomPadding = view.getHeight() + (int) DimensionUnit.DP.toPixels(18);
|
||||
bottomActionBar.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
int bottomPadding = bottomActionBar.getHeight() + (int) DimensionUnit.DP.toPixels(18);
|
||||
list.setPadding(list.getPaddingLeft(), list.getPaddingTop(), list.getPaddingRight(), bottomPadding);
|
||||
|
||||
list.scrollBy(0, -(bottomPadding - additionalScrollOffset));
|
||||
|
||||
return Unit.INSTANCE;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -1314,12 +1316,14 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
void onForwardClicked();
|
||||
void onMessageRequest(@NonNull MessageRequestViewModel viewModel);
|
||||
void handleReaction(@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
|
||||
@NonNull ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener,
|
||||
@NonNull SelectedConversationModel selectedConversationModel,
|
||||
@NonNull ConversationReactionOverlay.OnHideListener onHideListener);
|
||||
void onCursorChanged();
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onVoiceNotePause(@NonNull Uri uri);
|
||||
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress);
|
||||
void onVoiceNoteResume(@NonNull Uri uri, long messageId);
|
||||
void onVoiceNoteSeekTo(@NonNull Uri uri, double progress);
|
||||
void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed);
|
||||
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||
@@ -1430,16 +1434,63 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
multiselectItemDecoration.setFocusedItem(new MultiselectPart.Message(item.getConversationMessage()));
|
||||
list.invalidateItemDecorations();
|
||||
|
||||
isReacting = true;
|
||||
reactionsShade.setVisibility(View.VISIBLE);
|
||||
list.setLayoutFrozen(true);
|
||||
listener.handleReaction(item.getConversationMessage(), new ReactionsToolbarListener(item.getConversationMessage()), () -> {
|
||||
isReacting = false;
|
||||
reactionsShade.setVisibility(View.GONE);
|
||||
list.setLayoutFrozen(false);
|
||||
WindowUtil.setLightStatusBarFromTheme(requireActivity());
|
||||
clearFocusedItem();
|
||||
});
|
||||
|
||||
if (itemView instanceof ConversationItem) {
|
||||
Uri audioUri = getAudioUriForLongClick(messageRecord);
|
||||
if (audioUri != null) {
|
||||
listener.onVoiceNotePause(audioUri);
|
||||
}
|
||||
|
||||
Bitmap videoBitmap = null;
|
||||
int childAdapterPosition = list.getChildAdapterPosition(itemView);
|
||||
|
||||
final GiphyMp4ProjectionPlayerHolder mp4Holder;
|
||||
if (childAdapterPosition != RecyclerView.NO_POSITION) {
|
||||
mp4Holder = giphyMp4ProjectionRecycler.getCurrentHolder(childAdapterPosition);
|
||||
if (mp4Holder != null) {
|
||||
mp4Holder.pause();
|
||||
videoBitmap = mp4Holder.getBitmap();
|
||||
mp4Holder.hide();
|
||||
}
|
||||
} else {
|
||||
mp4Holder = null;
|
||||
}
|
||||
|
||||
ConversationItem conversationItem = (ConversationItem) itemView;
|
||||
Bitmap bitmap = ConversationItemSelection.snapshotView(conversationItem, list, messageRecord, videoBitmap);
|
||||
|
||||
final ConversationItemBodyBubble bodyBubble = conversationItem.bodyBubble;
|
||||
SelectedConversationModel selectedConversationModel = new SelectedConversationModel(bitmap,
|
||||
itemView.getX(),
|
||||
itemView.getY() + list.getTranslationY(),
|
||||
bodyBubble.getX(),
|
||||
bodyBubble.getWidth(),
|
||||
audioUri,
|
||||
messageRecord.isOutgoing());
|
||||
|
||||
bodyBubble.setVisibility(View.INVISIBLE);
|
||||
|
||||
listener.handleReaction(item.getConversationMessage(), new ReactionsToolbarListener(item.getConversationMessage()), selectedConversationModel, () -> {
|
||||
reactionsShade.setVisibility(View.GONE);
|
||||
list.setLayoutFrozen(false);
|
||||
|
||||
if (selectedConversationModel.getAudioUri() != null) {
|
||||
listener.onVoiceNoteResume(selectedConversationModel.getAudioUri(), messageRecord.getId());
|
||||
}
|
||||
|
||||
WindowUtil.setLightStatusBarFromTheme(requireActivity());
|
||||
clearFocusedItem();
|
||||
|
||||
if (mp4Holder != null) {
|
||||
mp4Holder.show();
|
||||
mp4Holder.resume();
|
||||
}
|
||||
|
||||
bodyBubble.setVisibility(View.VISIBLE);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
clearFocusedItem();
|
||||
((ConversationAdapter) list.getAdapter()).toggleSelection(item);
|
||||
@@ -1449,6 +1500,20 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable private Uri getAudioUriForLongClick(@NonNull MessageRecord messageRecord) {
|
||||
VoiceNotePlaybackState playbackState = listener.getVoiceNoteMediaController().getVoiceNotePlaybackState().getValue();
|
||||
if (playbackState == null || !playbackState.isPlaying()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!MessageRecordUtil.hasAudio(messageRecord) || !messageRecord.isMms()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Uri messageUri = ((MmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide().getUri();
|
||||
return playbackState.getUri().equals(messageUri) ? messageUri : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQuoteClicked(MmsMessageRecord messageRecord) {
|
||||
if (messageRecord.getQuote() == null) {
|
||||
@@ -1867,7 +1932,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
}
|
||||
}
|
||||
|
||||
private class ReactionsToolbarListener implements Toolbar.OnMenuItemClickListener {
|
||||
private class ReactionsToolbarListener implements ConversationReactionOverlay.OnActionSelectedListener {
|
||||
|
||||
private final ConversationMessage conversationMessage;
|
||||
|
||||
@@ -1876,16 +1941,32 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_info: handleDisplayDetails(conversationMessage); return true;
|
||||
case R.id.action_delete: handleDeleteMessages(conversationMessage.getMultiselectCollection().toSet()); return true;
|
||||
case R.id.action_copy: handleCopyMessage(conversationMessage.getMultiselectCollection().toSet()); return true;
|
||||
case R.id.action_reply: handleReplyMessage(conversationMessage); return true;
|
||||
case R.id.action_multiselect: handleEnterMultiSelect(conversationMessage); return true;
|
||||
case R.id.action_forward: handleForwardMessageParts(conversationMessage.getMultiselectCollection().toSet()); return true;
|
||||
case R.id.action_download: handleSaveAttachment((MediaMmsMessageRecord) conversationMessage.getMessageRecord()); return true;
|
||||
default: return false;
|
||||
public void onActionSelected(@NonNull ConversationReactionOverlay.Action action) {
|
||||
switch (action) {
|
||||
case REPLY:
|
||||
handleReplyMessage(conversationMessage);
|
||||
break;
|
||||
case FORWARD:
|
||||
handleForwardMessageParts(conversationMessage.getMultiselectCollection().toSet());
|
||||
break;
|
||||
case RESEND:
|
||||
handleResendMessage(conversationMessage.getMessageRecord());
|
||||
break;
|
||||
case DOWNLOAD:
|
||||
handleSaveAttachment((MediaMmsMessageRecord) conversationMessage.getMessageRecord());
|
||||
break;
|
||||
case COPY:
|
||||
handleCopyMessage(conversationMessage.getMultiselectCollection().toSet());
|
||||
break;
|
||||
case MULTISELECT:
|
||||
handleEnterMultiSelect(conversationMessage);
|
||||
break;
|
||||
case VIEW_INFO:
|
||||
handleDisplayDetails(conversationMessage);
|
||||
break;
|
||||
case DELETE:
|
||||
handleDeleteMessages(conversationMessage.getMultiselectCollection().toSet());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +160,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
private static final Rect SWIPE_RECT = new Rect();
|
||||
|
||||
public static final float LONG_PRESS_SCALE_FACTOR = 0.95f;
|
||||
private static final int SHRINK_BUBBLE_DELAY_MILLIS = 100;
|
||||
|
||||
private ConversationMessage conversationMessage;
|
||||
private MessageRecord messageRecord;
|
||||
private Optional<MessageRecord> nextMessageRecord;
|
||||
@@ -224,6 +227,21 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
private float lastYDownRelativeToThis;
|
||||
private ProjectionList colorizerProjections = new ProjectionList(3);
|
||||
|
||||
private final Runnable shrinkBubble = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
bodyBubble.animate()
|
||||
.scaleX(LONG_PRESS_SCALE_FACTOR)
|
||||
.scaleY(LONG_PRESS_SCALE_FACTOR)
|
||||
.setUpdateListener(animation -> {
|
||||
View parent = (View) getParent();
|
||||
if (parent != null) {
|
||||
parent.invalidate();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
public ConversationItem(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
@@ -343,6 +361,20 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
setGroupAuthorColor(messageRecord, hasWallpaper, colorizer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent ev) {
|
||||
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
getHandler().postDelayed(shrinkBubble, SHRINK_BUBBLE_DELAY_MILLIS);
|
||||
} else {
|
||||
getHandler().removeCallbacks(shrinkBubble);
|
||||
bodyBubble.animate()
|
||||
.scaleX(1.0f)
|
||||
.scaleY(1.0f);
|
||||
}
|
||||
|
||||
return super.dispatchTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
@@ -1737,7 +1769,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
@Override
|
||||
public @NonNull Projection getGiphyMp4PlayableProjection(@NonNull ViewGroup recyclerView) {
|
||||
if (mediaThumbnailStub != null && mediaThumbnailStub.isResolvable()) {
|
||||
return Projection.relativeToParent(recyclerView, mediaThumbnailStub.require(), mediaThumbnailStub.require().getCorners())
|
||||
ConversationItemThumbnail thumbnail = mediaThumbnailStub.require();
|
||||
return Projection.relativeToParent(recyclerView, thumbnail, thumbnail.getCorners())
|
||||
.scale(bodyBubble.getScaleX())
|
||||
.translateX(Util.halfOffsetFromScale(thumbnail.getWidth(), bodyBubble.getScaleX()))
|
||||
.translateY(Util.halfOffsetFromScale(thumbnail.getHeight(), bodyBubble.getScaleY()))
|
||||
.translateY(getTranslationY())
|
||||
.translateX(bodyBubble.getTranslationX())
|
||||
.translateX(getTranslationX());
|
||||
@@ -1754,6 +1790,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
return mediaThumbnailStub != null && mediaThumbnailStub.isResolvable() && canPlayContent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldProjectContent() {
|
||||
return canPlayContent() && bodyBubble.getVisibility() == VISIBLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) {
|
||||
colorizerProjections.clear();
|
||||
@@ -1761,34 +1802,70 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
if (messageRecord.isOutgoing() &&
|
||||
!hasNoBubble(messageRecord) &&
|
||||
!messageRecord.isRemoteDelete() &&
|
||||
bodyBubbleCorners != null)
|
||||
bodyBubbleCorners != null &&
|
||||
bodyBubble.getVisibility() == VISIBLE)
|
||||
{
|
||||
Projection bodyBubbleToRoot = Projection.relativeToParent(coordinateRoot, bodyBubble, bodyBubbleCorners).translateX(bodyBubble.getTranslationX());
|
||||
Projection videoToBubble = bodyBubble.getVideoPlayerProjection();
|
||||
|
||||
float translationX = Util.halfOffsetFromScale(bodyBubble.getWidth(), bodyBubble.getScaleX());
|
||||
float translationY = Util.halfOffsetFromScale(bodyBubble.getHeight(), bodyBubble.getScaleY());
|
||||
|
||||
if (videoToBubble != null) {
|
||||
Projection videoToRoot = Projection.translateFromDescendantToParentCoords(videoToBubble, bodyBubble, coordinateRoot);
|
||||
colorizerProjections.addAll(Projection.getCapAndTail(bodyBubbleToRoot, videoToRoot));
|
||||
|
||||
List<Projection> projections = Projection.getCapAndTail(bodyBubbleToRoot, videoToRoot);
|
||||
if (!projections.isEmpty()) {
|
||||
projections.get(0)
|
||||
.scale(bodyBubble.getScaleX())
|
||||
.translateX(translationX)
|
||||
.translateY(translationY);
|
||||
projections.get(1)
|
||||
.scale(bodyBubble.getScaleX())
|
||||
.translateX(translationX)
|
||||
.translateY(-translationY);
|
||||
}
|
||||
|
||||
colorizerProjections.addAll(projections);
|
||||
} else {
|
||||
colorizerProjections.add(bodyBubbleToRoot);
|
||||
colorizerProjections.add(
|
||||
bodyBubbleToRoot.scale(bodyBubble.getScaleX())
|
||||
.translateX(translationX)
|
||||
.translateY(translationY)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (messageRecord.isOutgoing() &&
|
||||
hasNoBubble(messageRecord) &&
|
||||
hasWallpaper)
|
||||
hasWallpaper &&
|
||||
bodyBubble.getVisibility() == VISIBLE)
|
||||
{
|
||||
Projection footerProjection = getActiveFooter(messageRecord).getProjection(coordinateRoot);
|
||||
ConversationItemFooter footer = getActiveFooter(messageRecord);
|
||||
Projection footerProjection = footer.getProjection(coordinateRoot);
|
||||
if (footerProjection != null) {
|
||||
colorizerProjections.add(footerProjection.translateX(bodyBubble.getTranslationX()));
|
||||
colorizerProjections.add(
|
||||
footerProjection.translateX(bodyBubble.getTranslationX())
|
||||
.scale(bodyBubble.getScaleX())
|
||||
.translateX(Util.halfOffsetFromScale(footer.getWidth(), bodyBubble.getScaleX()))
|
||||
.translateY(-Util.halfOffsetFromScale(footer.getHeight(), bodyBubble.getScaleY()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!messageRecord.isOutgoing() &&
|
||||
hasQuote(messageRecord) &&
|
||||
quoteView != null)
|
||||
quoteView != null &&
|
||||
bodyBubble.getVisibility() == VISIBLE)
|
||||
{
|
||||
bodyBubble.setQuoteViewProjection(quoteView.getProjection(bodyBubble));
|
||||
colorizerProjections.add(quoteView.getProjection(coordinateRoot).translateX(bodyBubble.getTranslationX() + this.getTranslationX()));
|
||||
|
||||
float bubbleOffsetFromScale = Util.halfOffsetFromScale(bodyBubble.getHeight(), bodyBubble.getScaleY());
|
||||
Projection cProj = quoteView.getProjection(coordinateRoot)
|
||||
.translateX(bodyBubble.getTranslationX() + this.getTranslationX() + Util.halfOffsetFromScale(quoteView.getWidth(), bodyBubble.getScaleX()))
|
||||
.translateY(bubbleOffsetFromScale - quoteView.getY() + (quoteView.getY() * bodyBubble.getScaleY()))
|
||||
.scale(bodyBubble.getScaleX());
|
||||
colorizerProjections.add(cProj);
|
||||
}
|
||||
|
||||
for (int i = 0; i < colorizerProjections.size(); i++) {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Path
|
||||
import android.view.View
|
||||
import androidx.core.graphics.applyCanvas
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.withClip
|
||||
import androidx.core.graphics.withTranslation
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.util.hasNoBubble
|
||||
|
||||
object ConversationItemSelection {
|
||||
|
||||
@JvmStatic
|
||||
fun snapshotView(
|
||||
conversationItem: ConversationItem,
|
||||
list: RecyclerView,
|
||||
messageRecord: MessageRecord,
|
||||
videoBitmap: Bitmap?,
|
||||
): Bitmap {
|
||||
val isOutgoing = messageRecord.isOutgoing
|
||||
val hasNoBubble = messageRecord.hasNoBubble(conversationItem.context)
|
||||
|
||||
return snapshotMessage(
|
||||
conversationItem = conversationItem,
|
||||
list = list,
|
||||
videoBitmap = videoBitmap,
|
||||
drawConversationItem = !isOutgoing || hasNoBubble,
|
||||
)
|
||||
}
|
||||
|
||||
private fun snapshotMessage(
|
||||
conversationItem: ConversationItem,
|
||||
list: RecyclerView,
|
||||
videoBitmap: Bitmap?,
|
||||
drawConversationItem: Boolean,
|
||||
): Bitmap {
|
||||
val initialReactionVisibility = conversationItem.reactionsView.visibility
|
||||
if (initialReactionVisibility == View.VISIBLE) {
|
||||
conversationItem.reactionsView.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
val originalScale = conversationItem.bodyBubble.scaleX
|
||||
conversationItem.bodyBubble.scaleX = 1.0f
|
||||
conversationItem.bodyBubble.scaleY = 1.0f
|
||||
|
||||
val projections = conversationItem.getColorizerProjections(list)
|
||||
|
||||
val path = Path()
|
||||
|
||||
val yTranslation = -conversationItem.y
|
||||
|
||||
val mp4Projection = conversationItem.getGiphyMp4PlayableProjection(list)
|
||||
var scaledVideoBitmap = videoBitmap
|
||||
if (videoBitmap != null) {
|
||||
scaledVideoBitmap = Bitmap.createScaledBitmap(
|
||||
videoBitmap,
|
||||
(videoBitmap.width / originalScale).toInt(),
|
||||
(videoBitmap.height / originalScale).toInt(),
|
||||
true
|
||||
)
|
||||
|
||||
mp4Projection.translateY(yTranslation)
|
||||
mp4Projection.applyToPath(path)
|
||||
}
|
||||
|
||||
projections.use {
|
||||
it.forEach { p ->
|
||||
p.translateY(yTranslation)
|
||||
p.applyToPath(path)
|
||||
}
|
||||
}
|
||||
|
||||
val distanceToBubbleBottom = conversationItem.bodyBubble.height + conversationItem.bodyBubble.y.toInt()
|
||||
return createBitmap(conversationItem.width, distanceToBubbleBottom).applyCanvas {
|
||||
if (drawConversationItem) {
|
||||
draw(conversationItem)
|
||||
}
|
||||
|
||||
withClip(path) {
|
||||
withTranslation(y = yTranslation) {
|
||||
list.draw(this)
|
||||
|
||||
if (scaledVideoBitmap != null) {
|
||||
drawBitmap(scaledVideoBitmap, mp4Projection.x, mp4Projection.y - yTranslation, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
mp4Projection.release()
|
||||
conversationItem.reactionsView.visibility = initialReactionVisibility
|
||||
conversationItem.bodyBubble.scaleX = originalScale
|
||||
conversationItem.bodyBubble.scaleY = originalScale
|
||||
}
|
||||
}
|
||||
|
||||
private fun Canvas.draw(conversationItem: ConversationItem) {
|
||||
val bodyBubble = conversationItem.bodyBubble
|
||||
withTranslation(bodyBubble.x, bodyBubble.y) {
|
||||
bodyBubble.draw(this@draw)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3664,12 +3664,13 @@ public class ConversationParentFragment extends Fragment
|
||||
|
||||
@Override
|
||||
public void handleReaction(@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
|
||||
@NonNull ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener,
|
||||
@NonNull SelectedConversationModel selectedConversationModel,
|
||||
@NonNull ConversationReactionOverlay.OnHideListener onHideListener)
|
||||
{
|
||||
reactionDelegate.setOnToolbarItemClickedListener(toolbarListener);
|
||||
reactionDelegate.setOnActionSelectedListener(onActionSelectedListener);
|
||||
reactionDelegate.setOnHideListener(onHideListener);
|
||||
reactionDelegate.show(requireActivity(), recipient.get(), conversationMessage, groupViewModel.isNonAdminInAnnouncementGroup());
|
||||
reactionDelegate.show(requireActivity(), recipient.get(), conversationMessage, groupViewModel.isNonAdminInAnnouncementGroup(), selectedConversationModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -3697,6 +3698,11 @@ public class ConversationParentFragment extends Fragment
|
||||
voiceNoteMediaController.startConsecutivePlayback(uri, messageId, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNoteResume(@NonNull Uri uri, long messageId) {
|
||||
voiceNoteMediaController.resumePlayback(uri, messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNoteSeekTo(@NonNull Uri uri, double progress) {
|
||||
voiceNoteMediaController.seekToPosition(uri, progress);
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.graphics.PointF;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -24,7 +23,7 @@ final class ConversationReactionDelegate {
|
||||
private final PointF lastSeenDownPoint = new PointF();
|
||||
|
||||
private ConversationReactionOverlay.OnReactionSelectedListener onReactionSelectedListener;
|
||||
private Toolbar.OnMenuItemClickListener onToolbarItemClickedListener;
|
||||
private ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener;
|
||||
private ConversationReactionOverlay.OnHideListener onHideListener;
|
||||
|
||||
ConversationReactionDelegate(@NonNull Stub<ConversationReactionOverlay> overlayStub) {
|
||||
@@ -38,9 +37,10 @@ final class ConversationReactionDelegate {
|
||||
void show(@NonNull Activity activity,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
@NonNull ConversationMessage conversationMessage,
|
||||
boolean isNonAdminInAnnouncementGroup)
|
||||
boolean isNonAdminInAnnouncementGroup,
|
||||
@NonNull SelectedConversationModel selectedConversationModel)
|
||||
{
|
||||
resolveOverlay().show(activity, conversationRecipient, conversationMessage, lastSeenDownPoint, isNonAdminInAnnouncementGroup);
|
||||
resolveOverlay().show(activity, conversationRecipient, conversationMessage, lastSeenDownPoint, isNonAdminInAnnouncementGroup, selectedConversationModel);
|
||||
}
|
||||
|
||||
void hide() {
|
||||
@@ -59,11 +59,11 @@ final class ConversationReactionDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
void setOnToolbarItemClickedListener(@NonNull Toolbar.OnMenuItemClickListener onToolbarItemClickedListener) {
|
||||
this.onToolbarItemClickedListener = onToolbarItemClickedListener;
|
||||
void setOnActionSelectedListener(@NonNull ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener) {
|
||||
this.onActionSelectedListener = onActionSelectedListener;
|
||||
|
||||
if (overlayStub.resolved()) {
|
||||
overlayStub.get().setOnToolbarItemClickedListener(onToolbarItemClickedListener);
|
||||
overlayStub.get().setOnActionSelectedListener(onActionSelectedListener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ final class ConversationReactionDelegate {
|
||||
overlay.requestFitSystemWindows();
|
||||
|
||||
overlay.setOnHideListener(onHideListener);
|
||||
overlay.setOnToolbarItemClickedListener(onToolbarItemClickedListener);
|
||||
overlay.setOnActionSelectedListener(onActionSelectedListener);
|
||||
overlay.setOnReactionSelectedListener(onReactionSelectedListener);
|
||||
|
||||
return overlay;
|
||||
|
||||
@@ -2,34 +2,41 @@ package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewKt;
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -39,12 +46,15 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
|
||||
private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
|
||||
private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
|
||||
private static final long TRANSITION_Y_DURATION = 150;
|
||||
|
||||
private final Rect emojiViewGlobalRect = new Rect();
|
||||
private final Rect emojiStripViewBounds = new Rect();
|
||||
@@ -54,23 +64,29 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
private final Boundary verticalScrubBoundary = new Boundary();
|
||||
private final PointF deadzoneTouchPoint = new PointF();
|
||||
|
||||
private Activity activity;
|
||||
private Recipient conversationRecipient;
|
||||
private MessageRecord messageRecord;
|
||||
private OverlayState overlayState = OverlayState.HIDDEN;
|
||||
private boolean isNonAdminInAnnouncementGroup;
|
||||
private Activity activity;
|
||||
private Recipient conversationRecipient;
|
||||
private MessageRecord messageRecord;
|
||||
private SelectedConversationModel selectedConversationModel;
|
||||
private OverlayState overlayState = OverlayState.HIDDEN;
|
||||
private boolean isNonAdminInAnnouncementGroup;
|
||||
|
||||
private boolean downIsOurs;
|
||||
private boolean isToolbarTouch;
|
||||
private int selected = -1;
|
||||
private int customEmojiIndex;
|
||||
private int originalStatusBarColor;
|
||||
private int originalNavigationBarColor;
|
||||
|
||||
private View dropdownAnchor;
|
||||
private View toolbarShade;
|
||||
private View inputShade;
|
||||
private View conversationItem;
|
||||
private View backgroundView;
|
||||
private ConstraintLayout foregroundView;
|
||||
private View selectedView;
|
||||
private EmojiImageView[] emojiViews;
|
||||
private Toolbar toolbar;
|
||||
|
||||
private ConversationContextMenu contextMenu;
|
||||
|
||||
private float touchDownDeadZoneSize;
|
||||
private float distanceFromTouchDownPointToTopOfScrubberDeadZone;
|
||||
@@ -78,21 +94,18 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
private int scrubberDistanceFromTouchDown;
|
||||
private int scrubberHeight;
|
||||
private int scrubberWidth;
|
||||
private int actionBarHeight;
|
||||
private int selectedVerticalTranslation;
|
||||
private int scrubberHorizontalMargin;
|
||||
private int animationEmojiStartDelayFactor;
|
||||
private int statusBarHeight;
|
||||
|
||||
private OnReactionSelectedListener onReactionSelectedListener;
|
||||
private Toolbar.OnMenuItemClickListener onToolbarItemClickedListener;
|
||||
private OnActionSelectedListener onActionSelectedListener;
|
||||
private OnHideListener onHideListener;
|
||||
|
||||
private AnimatorSet revealAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet revealMaskAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideAllButMaskAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideMaskAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet revealAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideAnimatorSet = new AnimatorSet();
|
||||
private List<Animator> hideAnimators;
|
||||
|
||||
public ConversationReactionOverlay(@NonNull Context context) {
|
||||
super(context);
|
||||
@@ -106,13 +119,13 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
backgroundView = findViewById(R.id.conversation_reaction_scrubber_background);
|
||||
foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground);
|
||||
selectedView = findViewById(R.id.conversation_reaction_current_selection_indicator);
|
||||
toolbar = findViewById(R.id.conversation_reaction_toolbar);
|
||||
|
||||
toolbar.setOnMenuItemClickListener(this::handleToolbarItemClicked);
|
||||
toolbar.setNavigationOnClickListener(view -> hide());
|
||||
dropdownAnchor = findViewById(R.id.dropdown_anchor);
|
||||
toolbarShade = findViewById(R.id.toolbar_shade);
|
||||
inputShade = findViewById(R.id.input_shade);
|
||||
conversationItem = findViewById(R.id.conversation_item);
|
||||
backgroundView = findViewById(R.id.conversation_reaction_scrubber_background);
|
||||
foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground);
|
||||
selectedView = findViewById(R.id.conversation_reaction_current_selection_indicator);
|
||||
|
||||
emojiViews = new EmojiImageView[] { findViewById(R.id.reaction_1),
|
||||
findViewById(R.id.reaction_2),
|
||||
@@ -131,7 +144,6 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
scrubberDistanceFromTouchDown = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrubber_distance);
|
||||
scrubberHeight = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrubber_height);
|
||||
scrubberWidth = getResources().getDimensionPixelOffset(R.dimen.reaction_scrubber_width);
|
||||
actionBarHeight = (int) ThemeUtil.getThemedDimen(getContext(), R.attr.actionBarSize);
|
||||
selectedVerticalTranslation = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_vertical_translation);
|
||||
scrubberHorizontalMargin = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_horizontal_margin);
|
||||
|
||||
@@ -144,7 +156,8 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
@NonNull Recipient conversationRecipient,
|
||||
@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull PointF lastSeenDownPoint,
|
||||
boolean isNonAdminInAnnouncementGroup)
|
||||
boolean isNonAdminInAnnouncementGroup,
|
||||
@NonNull SelectedConversationModel selectedConversationModel)
|
||||
{
|
||||
if (overlayState != OverlayState.HIDDEN) {
|
||||
return;
|
||||
@@ -152,11 +165,11 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
|
||||
this.messageRecord = conversationMessage.getMessageRecord();
|
||||
this.conversationRecipient = conversationRecipient;
|
||||
this.selectedConversationModel = selectedConversationModel;
|
||||
this.isNonAdminInAnnouncementGroup = isNonAdminInAnnouncementGroup;
|
||||
overlayState = OverlayState.UNINITAILIZED;
|
||||
selected = -1;
|
||||
|
||||
setupToolbarMenuItems(conversationMessage);
|
||||
setupSelectedEmoji();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
@@ -166,63 +179,260 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
statusBarHeight = ViewUtil.getStatusBarHeight(this);
|
||||
}
|
||||
|
||||
final float scrubberTranslationY = Math.max(-scrubberDistanceFromTouchDown + actionBarHeight,
|
||||
lastSeenDownPoint.y - scrubberHeight - scrubberDistanceFromTouchDown - statusBarHeight);
|
||||
ViewGroup.LayoutParams layoutParams = inputShade.getLayoutParams();
|
||||
layoutParams.height = activity.findViewById(R.id.bottom_panel).getHeight();
|
||||
inputShade.setLayoutParams(layoutParams);
|
||||
|
||||
final float halfWidth = scrubberWidth / 2f + scrubberHorizontalMargin;
|
||||
final float screenWidth = getResources().getDisplayMetrics().widthPixels;
|
||||
final float downX = ViewUtil.isLtr(this) ? lastSeenDownPoint.x : screenWidth - lastSeenDownPoint.x;
|
||||
final float scrubberTranslationX = Util.clamp(downX - halfWidth,
|
||||
scrubberHorizontalMargin,
|
||||
screenWidth + scrubberHorizontalMargin - halfWidth * 2) * (ViewUtil.isLtr(this) ? 1 : -1);
|
||||
toolbarShade.setVisibility(VISIBLE);
|
||||
inputShade.setVisibility(VISIBLE);
|
||||
|
||||
backgroundView.setTranslationX(scrubberTranslationX);
|
||||
backgroundView.setTranslationY(scrubberTranslationY);
|
||||
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
|
||||
|
||||
foregroundView.setTranslationX(scrubberTranslationX);
|
||||
foregroundView.setTranslationY(scrubberTranslationY);
|
||||
conversationItem.setLayoutParams(new LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight()));
|
||||
conversationItem.setBackground(new BitmapDrawable(getResources(), conversationItemSnapshot));
|
||||
|
||||
verticalScrubBoundary.update(lastSeenDownPoint.y - distanceFromTouchDownPointToTopOfScrubberDeadZone,
|
||||
lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone);
|
||||
float initialX = selectedConversationModel.getBitmapX();
|
||||
boolean isMessageOnLeft = selectedConversationModel.isOutgoing() ^ ViewUtil.isLtr(this);
|
||||
|
||||
conversationItem.setX(initialX);
|
||||
conversationItem.setY(selectedConversationModel.getBitmapY() - statusBarHeight);
|
||||
|
||||
conversationItem.setScaleX(ConversationItem.LONG_PRESS_SCALE_FACTOR);
|
||||
conversationItem.setScaleY(ConversationItem.LONG_PRESS_SCALE_FACTOR);
|
||||
|
||||
setVisibility(View.INVISIBLE);
|
||||
|
||||
ViewKt.doOnLayout(this, v -> {
|
||||
showAfterLayout(activity, conversationMessage, lastSeenDownPoint, isMessageOnLeft);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
private void showAfterLayout(@NonNull Activity activity,
|
||||
@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull PointF lastSeenDownPoint,
|
||||
boolean isMessageOnLeft) {
|
||||
contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(conversationMessage));
|
||||
|
||||
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
|
||||
boolean isWideLayout = contextMenu.getMaxWidth() + scrubberWidth < getWidth();
|
||||
|
||||
int bubbleWidth = selectedConversationModel.getBubbleWidth();
|
||||
|
||||
float endX = selectedConversationModel.getBitmapX();
|
||||
float endY = conversationItem.getY();
|
||||
float endApparentTop = endY;
|
||||
float endScale = 1f;
|
||||
|
||||
float menuPadding = DimensionUnit.DP.toPixels(12f);
|
||||
int reactionBarHeight = backgroundView.getHeight();
|
||||
|
||||
float reactionBarBackgroundY;
|
||||
|
||||
if (isWideLayout) {
|
||||
boolean everythingFitsVertically = scrubberHeight + conversationItemSnapshot.getHeight() < getHeight();
|
||||
if (everythingFitsVertically) {
|
||||
boolean reactionBarFitsAboveItem = conversationItem.getY() > scrubberHeight;
|
||||
|
||||
if (reactionBarFitsAboveItem) {
|
||||
reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight;
|
||||
} else {
|
||||
endY = reactionBarHeight + menuPadding;
|
||||
reactionBarBackgroundY = 0f;
|
||||
}
|
||||
} else {
|
||||
float spaceAvailableForItem = getHeight() - reactionBarHeight - menuPadding;
|
||||
|
||||
endScale = spaceAvailableForItem / conversationItem.getHeight();
|
||||
endY = reactionBarHeight + menuPadding - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
|
||||
reactionBarBackgroundY = 0f;
|
||||
}
|
||||
} else {
|
||||
boolean everythingFitsVertically = contextMenu.getMaxHeight() + conversationItemSnapshot.getHeight() + menuPadding + reactionBarHeight < getHeight();
|
||||
|
||||
if (everythingFitsVertically) {
|
||||
float itemBottom = selectedConversationModel.getBitmapY() + conversationItemSnapshot.getHeight();
|
||||
boolean menuFitsBelowItem = itemBottom + menuPadding + contextMenu.getMaxHeight() <= getHeight();
|
||||
|
||||
if (menuFitsBelowItem) {
|
||||
reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight;
|
||||
|
||||
if (reactionBarBackgroundY < 0) {
|
||||
endY = backgroundView.getHeight();
|
||||
reactionBarBackgroundY = 0f;
|
||||
}
|
||||
} else {
|
||||
endY = getHeight() - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.getHeight();
|
||||
reactionBarBackgroundY = endY - menuPadding - reactionBarHeight;
|
||||
}
|
||||
|
||||
endApparentTop = endY;
|
||||
} else if (reactionBarHeight + contextMenu.getMaxHeight() + menuPadding < getHeight()) {
|
||||
float spaceAvailableForItem = (float) getHeight() - contextMenu.getMaxHeight() - menuPadding - reactionBarHeight;
|
||||
|
||||
endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight();
|
||||
endY = reactionBarHeight - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
|
||||
reactionBarBackgroundY = 0f;
|
||||
endApparentTop = reactionBarHeight;
|
||||
} else {
|
||||
contextMenu.setHeight(contextMenu.getMaxHeight() / 2);
|
||||
|
||||
int menuHeight = contextMenu.getHeight();
|
||||
boolean fitsVertically = menuHeight + conversationItem.getHeight() + menuPadding + reactionBarHeight < getHeight();
|
||||
|
||||
if (fitsVertically) {
|
||||
endY = getHeight() - menuHeight - menuPadding - conversationItemSnapshot.getHeight();
|
||||
reactionBarBackgroundY = endY - reactionBarHeight;
|
||||
endApparentTop = endY;
|
||||
} else {
|
||||
float spaceAvailableForItem = (float) getHeight() - menuHeight - menuPadding - reactionBarHeight;
|
||||
|
||||
endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight();
|
||||
endY = reactionBarHeight - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale);
|
||||
reactionBarBackgroundY = 0f;
|
||||
endApparentTop = reactionBarHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reactionBarBackgroundY = Math.max(reactionBarBackgroundY, -statusBarHeight);
|
||||
|
||||
hideAnimatorSet.end();
|
||||
toolbar.setVisibility(VISIBLE);
|
||||
hideAnimatorSet = newHideAnimatorSet();
|
||||
setVisibility(View.VISIBLE);
|
||||
revealAnimatorSet.start();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
this.activity = activity;
|
||||
originalStatusBarColor = activity.getWindow().getStatusBarColor();
|
||||
WindowUtil.setStatusBarColor(activity.getWindow(), ContextCompat.getColor(getContext(), R.color.action_mode_status_bar));
|
||||
updateSystemUiOnShow(activity);
|
||||
}
|
||||
|
||||
if (!ThemeUtil.isDarkTheme(getContext())) {
|
||||
WindowUtil.setLightStatusBar(activity.getWindow());
|
||||
}
|
||||
float scrubberX;
|
||||
if (isMessageOnLeft) {
|
||||
scrubberX = scrubberHorizontalMargin;
|
||||
} else {
|
||||
scrubberX = getWidth() - scrubberWidth - scrubberHorizontalMargin;
|
||||
}
|
||||
|
||||
foregroundView.setX(scrubberX);
|
||||
foregroundView.setY(reactionBarBackgroundY + reactionBarHeight / 2f - foregroundView.getHeight() / 2f);
|
||||
|
||||
backgroundView.setX(scrubberX);
|
||||
backgroundView.setY(reactionBarBackgroundY);
|
||||
|
||||
verticalScrubBoundary.update(reactionBarBackgroundY,
|
||||
lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone);
|
||||
|
||||
updateBoundsOnLayoutChanged();
|
||||
|
||||
revealAnimatorSet.start();
|
||||
|
||||
if (isWideLayout) {
|
||||
float scrubberRight = scrubberX + scrubberWidth;
|
||||
float offsetX = isMessageOnLeft ? scrubberRight + menuPadding : scrubberX - contextMenu.getMaxWidth() - menuPadding;
|
||||
contextMenu.show((int) offsetX, (int) Math.min(backgroundView.getY(), getHeight() - contextMenu.getMaxHeight()));
|
||||
} else {
|
||||
float contentX = selectedConversationModel.getContentX();
|
||||
float offsetX = isMessageOnLeft ? contentX : - contextMenu.getMaxWidth() + contentX + bubbleWidth;
|
||||
|
||||
float menuTop = endApparentTop + (conversationItemSnapshot.getHeight() * endScale);
|
||||
contextMenu.show((int) offsetX, (int) (menuTop + menuPadding));
|
||||
}
|
||||
|
||||
conversationItem.animate()
|
||||
.x(endX)
|
||||
.scaleX(endScale)
|
||||
.scaleY(endScale);
|
||||
|
||||
conversationItem.animate()
|
||||
.y(endY)
|
||||
.setDuration(TRANSITION_Y_DURATION);
|
||||
}
|
||||
|
||||
@RequiresApi(api = 21)
|
||||
private void updateSystemUiOnShow(@NonNull Activity activity) {
|
||||
Window window = activity.getWindow();
|
||||
int barColor = ContextCompat.getColor(getContext(), R.color.conversation_item_selected_system_ui);
|
||||
|
||||
originalStatusBarColor = window.getStatusBarColor();
|
||||
WindowUtil.setStatusBarColor(window, barColor);
|
||||
|
||||
originalNavigationBarColor = window.getNavigationBarColor();
|
||||
WindowUtil.setNavigationBarColor(window, barColor);
|
||||
|
||||
if (!ThemeUtil.isDarkTheme(getContext())) {
|
||||
WindowUtil.clearLightStatusBar(window);
|
||||
}
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
hideInternal(hideAnimatorSet, onHideListener);
|
||||
hideInternal(onHideListener);
|
||||
}
|
||||
|
||||
public void hideForReactWithAny() {
|
||||
hideInternal(hideAnimatorSet, null);
|
||||
hideInternal(onHideListener);
|
||||
}
|
||||
|
||||
private void hideInternal(@NonNull AnimatorSet hideAnimatorSet, @Nullable OnHideListener onHideListener) {
|
||||
private void hideInternal(@Nullable OnHideListener onHideListener) {
|
||||
overlayState = OverlayState.HIDDEN;
|
||||
|
||||
int duration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration);
|
||||
|
||||
List<Animator> hides = new ArrayList<>(hideAnimators);
|
||||
|
||||
ObjectAnimator itemScaleXAnim = new ObjectAnimator();
|
||||
itemScaleXAnim.setProperty(View.SCALE_X);
|
||||
itemScaleXAnim.setFloatValues(1f);
|
||||
itemScaleXAnim.setTarget(conversationItem);
|
||||
itemScaleXAnim.setDuration(duration);
|
||||
hides.add(itemScaleXAnim);
|
||||
|
||||
ObjectAnimator itemScaleYAnim = new ObjectAnimator();
|
||||
itemScaleYAnim.setProperty(View.SCALE_Y);
|
||||
itemScaleYAnim.setFloatValues(1f);
|
||||
itemScaleYAnim.setTarget(conversationItem);
|
||||
itemScaleYAnim.setDuration(duration);
|
||||
hides.add(itemScaleYAnim);
|
||||
|
||||
ObjectAnimator itemXAnim = new ObjectAnimator();
|
||||
itemXAnim.setProperty(View.X);
|
||||
itemXAnim.setFloatValues(selectedConversationModel.getBitmapX());
|
||||
itemXAnim.setTarget(conversationItem);
|
||||
itemXAnim.setDuration(duration);
|
||||
hides.add(itemXAnim);
|
||||
|
||||
ObjectAnimator itemYAnim = new ObjectAnimator();
|
||||
itemYAnim.setProperty(View.Y);
|
||||
itemYAnim.setFloatValues(selectedConversationModel.getBitmapY() - statusBarHeight);
|
||||
itemYAnim.setTarget(conversationItem);
|
||||
itemYAnim.setDuration(TRANSITION_Y_DURATION);
|
||||
hides.add(itemYAnim);
|
||||
|
||||
hideAnimatorSet.playTogether(hides);
|
||||
|
||||
revealAnimatorSet.end();
|
||||
hideAnimatorSet.start();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21 && activity != null) {
|
||||
WindowUtil.setStatusBarColor(activity.getWindow(), originalStatusBarColor);
|
||||
WindowUtil.clearLightStatusBar(activity.getWindow());
|
||||
activity = null;
|
||||
}
|
||||
hideAnimatorSet.addListener(new AnimationCompleteListener() {
|
||||
@Override public void onAnimationEnd(Animator animation) {
|
||||
hideAnimatorSet.removeListener(this);
|
||||
|
||||
if (onHideListener != null) {
|
||||
onHideListener.onHide();
|
||||
toolbarShade.setVisibility(INVISIBLE);
|
||||
inputShade.setVisibility(INVISIBLE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21 && activity != null) {
|
||||
WindowUtil.setStatusBarColor(activity.getWindow(), originalStatusBarColor);
|
||||
WindowUtil.setNavigationBarColor(activity.getWindow(), originalNavigationBarColor);
|
||||
activity = null;
|
||||
}
|
||||
|
||||
if (onHideListener != null) {
|
||||
onHideListener.onHide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (contextMenu != null) {
|
||||
contextMenu.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +448,10 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
super.onLayout(changed, l, t, r, b);
|
||||
|
||||
updateBoundsOnLayoutChanged();
|
||||
}
|
||||
|
||||
private void updateBoundsOnLayoutChanged() {
|
||||
backgroundView.getGlobalVisibleRect(emojiStripViewBounds);
|
||||
emojiViews[0].getGlobalVisibleRect(emojiViewGlobalRect);
|
||||
emojiStripViewBounds.left = getStart(emojiViewGlobalRect);
|
||||
@@ -300,24 +514,10 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
}
|
||||
}
|
||||
|
||||
if (isToolbarTouch) {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_CANCEL || motionEvent.getAction() == MotionEvent.ACTION_UP) {
|
||||
isToolbarTouch = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (motionEvent.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
selected = getSelectedIndexViaDownEvent(motionEvent);
|
||||
|
||||
if (selected == -1) {
|
||||
if (motionEvent.getY() < toolbar.getHeight() + statusBarHeight) {
|
||||
isToolbarTouch = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
deadzoneTouchPoint.set(motionEvent.getX(), motionEvent.getY());
|
||||
overlayState = OverlayState.DEADZONE;
|
||||
downIsOurs = true;
|
||||
@@ -454,8 +654,8 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
this.onReactionSelectedListener = onReactionSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnToolbarItemClickedListener(@Nullable Toolbar.OnMenuItemClickListener onToolbarItemClickedListener) {
|
||||
this.onToolbarItemClickedListener = onToolbarItemClickedListener;
|
||||
public void setOnActionSelectedListener(@Nullable OnActionSelectedListener onActionSelectedListener) {
|
||||
this.onActionSelectedListener = onActionSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnHideListener(@Nullable OnHideListener onHideListener) {
|
||||
@@ -474,24 +674,48 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private void setupToolbarMenuItems(@NonNull ConversationMessage conversationMessage) {
|
||||
private @NonNull List<ActionItem> getMenuActionItems(@NonNull ConversationMessage conversationMessage) {
|
||||
MenuState menuState = MenuState.getMenuState(conversationRecipient, conversationMessage.getMultiselectCollection().toSet(), false, isNonAdminInAnnouncementGroup);
|
||||
|
||||
toolbar.getMenu().findItem(R.id.action_copy).setVisible(menuState.shouldShowCopyAction());
|
||||
toolbar.getMenu().findItem(R.id.action_download).setVisible(menuState.shouldShowSaveAttachmentAction());
|
||||
toolbar.getMenu().findItem(R.id.action_forward).setVisible(menuState.shouldShowForwardAction());
|
||||
toolbar.getMenu().findItem(R.id.action_reply).setVisible(menuState.shouldShowReplyAction());
|
||||
}
|
||||
List<ActionItem> items = new ArrayList<>();
|
||||
|
||||
private boolean handleToolbarItemClicked(@NonNull MenuItem menuItem) {
|
||||
|
||||
hide();
|
||||
|
||||
if (onToolbarItemClickedListener == null) {
|
||||
return false;
|
||||
if (menuState.shouldShowReplyAction()) {
|
||||
items.add(new ActionItem(R.drawable.ic_reply_24_tinted, getResources().getString(R.string.conversation_selection__menu_reply), () -> handleActionItemClicked(Action.REPLY)));
|
||||
}
|
||||
|
||||
return onToolbarItemClickedListener.onMenuItemClick(menuItem);
|
||||
if (menuState.shouldShowForwardAction()) {
|
||||
items.add(new ActionItem(R.drawable.ic_forward_24_tinted, getResources().getString(R.string.conversation_selection__menu_forward), () -> handleActionItemClicked(Action.FORWARD)));
|
||||
}
|
||||
|
||||
if (menuState.shouldShowResendAction()) {
|
||||
items.add(new ActionItem(R.drawable.ic_retry_24, getResources().getString(R.string.conversation_selection__menu_resend_message), () -> handleActionItemClicked(Action.RESEND)));
|
||||
}
|
||||
|
||||
if (menuState.shouldShowSaveAttachmentAction()) {
|
||||
items.add(new ActionItem(R.drawable.ic_save_24_tinted, getResources().getString(R.string.conversation_selection__menu_save), () -> handleActionItemClicked(Action.DOWNLOAD)));
|
||||
}
|
||||
|
||||
if (menuState.shouldShowCopyAction()) {
|
||||
items.add(new ActionItem(R.drawable.ic_copy_24_tinted, getResources().getString(R.string.conversation_selection__menu_copy), () -> handleActionItemClicked(Action.COPY)));
|
||||
}
|
||||
|
||||
items.add(new ActionItem(R.drawable.ic_select_24_tinted, getResources().getString(R.string.conversation_selection__menu_multi_select), () -> handleActionItemClicked(Action.MULTISELECT)));
|
||||
items.add(new ActionItem(R.drawable.ic_info_tinted_24, getResources().getString(R.string.conversation_selection__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO)));
|
||||
items.add(new ActionItem(R.drawable.ic_delete_tinted_24, getResources().getString(R.string.conversation_selection__menu_delete), () -> handleActionItemClicked(Action.DELETE)));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private void handleActionItemClicked(@NonNull Action action) {
|
||||
hideInternal(() -> {
|
||||
if (onHideListener != null) {
|
||||
onHideListener.onHide();
|
||||
}
|
||||
|
||||
if (onActionSelectedListener != null) {
|
||||
onActionSelectedListener.onActionSelected(action);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initAnimators() {
|
||||
@@ -521,64 +745,47 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
selectedRevealAnim.setDuration(duration);
|
||||
reveals.add(selectedRevealAnim);
|
||||
|
||||
Animator toolbarRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in);
|
||||
toolbarRevealAnim.setTarget(toolbar);
|
||||
toolbarRevealAnim.setDuration(duration);
|
||||
reveals.add(toolbarRevealAnim);
|
||||
|
||||
revealAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
revealAnimatorSet.playTogether(reveals);
|
||||
|
||||
revealMaskAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
revealMaskAnimatorSet.playTogether(overlayRevealAnim);
|
||||
|
||||
List<Animator> hides = Stream.of(emojiViews)
|
||||
.mapIndexed((idx, v) -> {
|
||||
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);
|
||||
anim.setTarget(v);
|
||||
anim.setStartDelay(idx * animationEmojiStartDelayFactor);
|
||||
return anim;
|
||||
})
|
||||
.toList();
|
||||
hideAnimators = Stream.of(emojiViews)
|
||||
.mapIndexed((idx, v) -> {
|
||||
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);
|
||||
anim.setTarget(v);
|
||||
anim.setStartDelay(idx * animationEmojiStartDelayFactor);
|
||||
return anim;
|
||||
})
|
||||
.toList();
|
||||
|
||||
Animator overlayHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
|
||||
overlayHideAnim.setDuration(duration);
|
||||
hideAnimators.add(overlayHideAnim);
|
||||
|
||||
Animator backgroundHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
|
||||
backgroundHideAnim.setTarget(backgroundView);
|
||||
backgroundHideAnim.setDuration(duration);
|
||||
hides.add(backgroundHideAnim);
|
||||
hideAnimators.add(backgroundHideAnim);
|
||||
|
||||
Animator selectedHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
|
||||
selectedHideAnim.setTarget(selectedView);
|
||||
selectedHideAnim.setDuration(duration);
|
||||
hides.add(selectedHideAnim);
|
||||
hideAnimators.add(selectedHideAnim);
|
||||
|
||||
Animator toolbarHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
|
||||
toolbarHideAnim.setTarget(toolbar);
|
||||
toolbarHideAnim.setDuration(duration);
|
||||
hides.add(toolbarHideAnim);
|
||||
hideAnimatorSet = newHideAnimatorSet();
|
||||
}
|
||||
|
||||
AnimationCompleteListener hideListener = new AnimationCompleteListener() {
|
||||
private @NonNull AnimatorSet newHideAnimatorSet() {
|
||||
AnimatorSet set = new AnimatorSet();
|
||||
|
||||
set.addListener(new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
setVisibility(View.GONE);
|
||||
}
|
||||
};
|
||||
});
|
||||
set.setInterpolator(INTERPOLATOR);
|
||||
|
||||
List<Animator> hideAllAnimators = new LinkedList<>(hides);
|
||||
hideAllAnimators.add(overlayHideAnim);
|
||||
|
||||
hideAnimatorSet.addListener(hideListener);
|
||||
hideAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
hideAnimatorSet.playTogether(hideAllAnimators);
|
||||
|
||||
hideAllButMaskAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
hideAllButMaskAnimatorSet.playTogether(hides);
|
||||
|
||||
hideMaskAnimatorSet.addListener(hideListener);
|
||||
hideMaskAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
hideMaskAnimatorSet.playTogether(overlayHideAnim);
|
||||
return set;
|
||||
}
|
||||
|
||||
public interface OnHideListener {
|
||||
@@ -590,6 +797,10 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
void onCustomReactionSelected(@NonNull MessageRecord messageRecord, boolean hasAddedCustomEmoji);
|
||||
}
|
||||
|
||||
public interface OnActionSelectedListener {
|
||||
void onActionSelected(@NonNull Action action);
|
||||
}
|
||||
|
||||
private static class Boundary {
|
||||
private float min;
|
||||
private float max;
|
||||
@@ -621,4 +832,15 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
SCRUB,
|
||||
TAP
|
||||
}
|
||||
|
||||
public enum Action {
|
||||
REPLY,
|
||||
FORWARD,
|
||||
RESEND,
|
||||
DOWNLOAD,
|
||||
COPY,
|
||||
MULTISELECT,
|
||||
VIEW_INFO,
|
||||
DELETE,
|
||||
}
|
||||
}
|
||||
@@ -220,6 +220,11 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldProjectContent() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) {
|
||||
return EMPTY_PROJECTION_LIST;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
|
||||
/**
|
||||
* Contains information on a single selected conversation item. This is used when transitioning
|
||||
* between selected and unselected states.
|
||||
*/
|
||||
data class SelectedConversationModel(
|
||||
val bitmap: Bitmap,
|
||||
val bitmapX: Float,
|
||||
val bitmapY: Float,
|
||||
val contentX: Float,
|
||||
val bubbleWidth: Int,
|
||||
val audioUri: Uri? = null,
|
||||
val isOutgoing: Boolean,
|
||||
)
|
||||
@@ -79,6 +79,7 @@ class RecyclerViewColorizer(private val recyclerView: RecyclerView) {
|
||||
}
|
||||
|
||||
private val colorPaint = Paint()
|
||||
private val outOfBoundsPaint = Paint()
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
outRect.setEmpty()
|
||||
@@ -122,6 +123,8 @@ class RecyclerViewColorizer(private val recyclerView: RecyclerView) {
|
||||
mask.setXfermode(colorPaint.xfermode)
|
||||
mask.setBounds(0, 0, parent.width, parent.height)
|
||||
mask.draw(canvas)
|
||||
|
||||
outOfBoundsPaint.color = chatColors.getColors().last()
|
||||
} else {
|
||||
colorPaint.color = chatColors.asSingleColor()
|
||||
canvas.drawRect(
|
||||
@@ -131,7 +134,13 @@ class RecyclerViewColorizer(private val recyclerView: RecyclerView) {
|
||||
parent.height.toFloat(),
|
||||
colorPaint
|
||||
)
|
||||
|
||||
outOfBoundsPaint.color = chatColors.asSingleColor()
|
||||
}
|
||||
|
||||
canvas.drawRect(
|
||||
0f, parent.height.toFloat(), parent.width.toFloat(), parent.height * 2f, outOfBoundsPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@ class MultiselectItemDecoration(
|
||||
}
|
||||
}
|
||||
|
||||
if (child.canPlayContent()) {
|
||||
if (child.canPlayContent() && child.shouldProjectContent()) {
|
||||
val mp4GifProjection = child.getGiphyMp4PlayableProjection(child.rootView as ViewGroup)
|
||||
path.op(mp4GifProjection.path, Path.Op.DIFFERENCE)
|
||||
mp4GifProjection.release()
|
||||
|
||||
@@ -51,4 +51,10 @@ public interface GiphyMp4Playable {
|
||||
* Specifies whether the content can start playing.
|
||||
*/
|
||||
boolean canPlayContent();
|
||||
|
||||
/**
|
||||
* Specifies whether the projection from {@link #getGiphyMp4PlayableProjection(ViewGroup)} should
|
||||
* be used to project into a view.
|
||||
*/
|
||||
boolean shouldProjectContent();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -98,10 +99,18 @@ public final class GiphyMp4ProjectionPlayerHolder implements Player.Listener, De
|
||||
container.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
player.pause();
|
||||
}
|
||||
|
||||
public void show() {
|
||||
container.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
player.play();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(int playbackState) {
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
@@ -177,4 +186,8 @@ public final class GiphyMp4ProjectionPlayerHolder implements Player.Listener, De
|
||||
public void setCorners(@Nullable Projection.Corners corners) {
|
||||
player.setCorners(corners);
|
||||
}
|
||||
|
||||
public @Nullable Bitmap getBitmap() {
|
||||
return player.getBitmap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ public final class GiphyMp4ProjectionRecycler implements GiphyMp4PlaybackControl
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable GiphyMp4ProjectionPlayerHolder getCurrentHolder(int adapterPosition) {
|
||||
public @Nullable GiphyMp4ProjectionPlayerHolder getCurrentHolder(int adapterPosition) {
|
||||
if (playing.get(adapterPosition) != null) {
|
||||
return playing.get(adapterPosition);
|
||||
} else if (notPlaying.get(adapterPosition) != null) {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.TextureView;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -103,6 +106,12 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
||||
}
|
||||
}
|
||||
|
||||
void pause() {
|
||||
if (exoPlayer != null) {
|
||||
exoPlayer.pause();
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (exoPlayer != null) {
|
||||
exoPlayer.stop();
|
||||
@@ -122,4 +131,13 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
||||
void setResizeMode(@AspectRatioFrameLayout.ResizeMode int resizeMode) {
|
||||
exoView.setResizeMode(resizeMode);
|
||||
}
|
||||
|
||||
@Nullable Bitmap getBitmap() {
|
||||
final View view = exoView.getVideoSurfaceView();
|
||||
if (view instanceof TextureView) {
|
||||
return ((TextureView) view).getBitmap();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,11 @@ final class GiphyMp4ViewHolder extends MappingViewHolder<GiphyImage> implements
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldProjectContent() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void loadPlaceholderImage(@NonNull GiphyImage giphyImage) {
|
||||
GlideApp.with(itemView)
|
||||
.load(new ChunkedImageUrl(giphyImage.getStillUrl()))
|
||||
|
||||
@@ -16,13 +16,11 @@ import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.ClipProjectionDrawable;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
|
||||
@@ -41,10 +39,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import java.sql.Date;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable, Colorizable {
|
||||
private final TextView sentDate;
|
||||
@@ -250,6 +245,11 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements G
|
||||
return conversationItem.canPlayContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldProjectContent() {
|
||||
return conversationItem.shouldProjectContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) {
|
||||
return conversationItem.getColorizerProjections(coordinateRoot);
|
||||
|
||||
@@ -114,6 +114,14 @@ public final class Projection {
|
||||
return set(x, y + yTranslation, width, height, corners);
|
||||
}
|
||||
|
||||
public @NonNull Projection scale(float scale) {
|
||||
Corners newCorners = new Corners(this.corners.topLeft * scale,
|
||||
this.corners.topRight * scale,
|
||||
this.corners.bottomRight * scale,
|
||||
this.corners.bottomLeft * scale);
|
||||
return set(x, y, (int) (width * scale), (int) (height * scale), newCorners);
|
||||
}
|
||||
|
||||
public static @NonNull Projection relativeToParent(@NonNull ViewGroup parent, @NonNull View view, @Nullable Corners corners) {
|
||||
Rect viewBounds = new Rect();
|
||||
|
||||
|
||||
@@ -444,6 +444,15 @@ public class Util {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns half of the difference between the given length, and the length when scaled by the
|
||||
* given scale.
|
||||
*/
|
||||
public static float halfOffsetFromScale(int length, float scale) {
|
||||
float scaledLength = length * scale;
|
||||
return (length - scaledLength) / 2;
|
||||
}
|
||||
|
||||
public static @Nullable String readTextFromClipboard(@NonNull Context context) {
|
||||
{
|
||||
ClipboardManager clipboardManager = (ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
||||
@@ -35,6 +35,12 @@ public final class WindowUtil {
|
||||
setSystemUiFlags(window, View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
|
||||
}
|
||||
|
||||
public static void setNavigationBarColor(@NonNull Window window, @ColorInt int color) {
|
||||
if (Build.VERSION.SDK_INT < 21) return;
|
||||
|
||||
window.setNavigationBarColor(color);
|
||||
}
|
||||
|
||||
public static void setLightStatusBarFromTheme(@NonNull Activity activity) {
|
||||
if (Build.VERSION.SDK_INT < 23) return;
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
/**
|
||||
* AdaptiveActionsToolbar behaves like a normal {@link Toolbar} except in that it ignores the
|
||||
* showAsAlways attributes of menu items added via menu inflation, opting for an adaptive algorithm
|
||||
* instead. This algorithm will display as many icons as it can up to a specific percentage of the
|
||||
* screen.
|
||||
*
|
||||
* Each ActionView icon is expected to occupy 48dp of space, including padding. Items are stacked one
|
||||
* after the next with no margins.
|
||||
*
|
||||
* This view can be customized via attributes:
|
||||
*
|
||||
* aat_max_shown -- controls the max number of items to display.
|
||||
* aat_percent_for_actions -- controls the max percent of screen width the buttons can occupy.
|
||||
*/
|
||||
public class AdaptiveActionsToolbar extends Toolbar {
|
||||
|
||||
private static final int NAVIGATION_DP = 56;
|
||||
private static final int ACTION_VIEW_WIDTH_DP = 48;
|
||||
private static final int OVERFLOW_VIEW_WIDTH_DP = 36;
|
||||
|
||||
private int maxShown;
|
||||
|
||||
public AdaptiveActionsToolbar(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public AdaptiveActionsToolbar(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, R.attr.toolbarStyle);
|
||||
}
|
||||
|
||||
public AdaptiveActionsToolbar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.AdaptiveActionsToolbar);
|
||||
|
||||
maxShown = array.getInteger(R.styleable.AdaptiveActionsToolbar_aat_max_shown, 100);
|
||||
|
||||
array.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
adjustMenuActions(getMenu(), maxShown, getMeasuredWidth());
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
public static void adjustMenuActions(@NonNull Menu menu, int maxToShow, int toolbarWidthPx) {
|
||||
int menuSize = 0;
|
||||
|
||||
for (int i = 0; i < menu.size(); i++) {
|
||||
if (menu.getItem(i).isVisible()) {
|
||||
menuSize++;
|
||||
}
|
||||
}
|
||||
|
||||
int widthAllowed = toolbarWidthPx - ViewUtil.dpToPx(NAVIGATION_DP);
|
||||
int nItemsToShow = Math.min(maxToShow, widthAllowed / ViewUtil.dpToPx(ACTION_VIEW_WIDTH_DP));
|
||||
|
||||
if (nItemsToShow < menuSize) {
|
||||
widthAllowed -= ViewUtil.dpToPx(OVERFLOW_VIEW_WIDTH_DP);
|
||||
}
|
||||
|
||||
nItemsToShow = Math.min(maxToShow, widthAllowed / ViewUtil.dpToPx(ACTION_VIEW_WIDTH_DP));
|
||||
|
||||
for (int i = 0; i < menu.size(); i++) {
|
||||
MenuItem item = menu.getItem(i);
|
||||
if (item.isVisible() && nItemsToShow > 0) {
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
nItemsToShow--;
|
||||
} else {
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user