From 7e91132e7ed62a58b5f347de252ab1eda3f43b90 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 29 Sep 2021 13:38:34 -0300 Subject: [PATCH] Fix multiple chatcolors issues from beta feedback. - Fix issue where custom color would come out as black - Completely remove mask view in favour of using the item decoration. - Fix issue where video gifs wouldn't "cut through" bubble. - Fix issue where multiselect shade would only appear if bottom or top item was not visible --- .../securesms/BindableConversationItem.java | 2 +- .../securesms/components/MaskView.java | 152 ------------------ .../conversation/ConversationActivity.java | 24 +-- .../conversation/ConversationFragment.java | 48 +++--- .../conversation/ConversationItem.java | 15 +- .../ConversationItemBodyBubble.java | 4 + .../ConversationItemMaskTarget.java | 66 -------- .../ConversationReactionDelegate.java | 23 +-- .../ConversationReactionOverlay.java | 34 ---- .../colors/RecyclerViewColorizer.kt | 1 + .../mutiselect/MultiselectItemDecoration.kt | 58 ++++++- .../mutiselect/Multiselectable.kt | 3 +- .../giph/mp4/GiphyMp4ProjectionRecycler.java | 14 +- .../securesms/util/Projection.java | 39 +++++ .../layout/conversation_reaction_scrubber.xml | 7 - 15 files changed, 153 insertions(+), 337 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/MaskView.java delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemMaskTarget.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index 5ce5c952c..e23dc0571 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -66,7 +66,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, void onAddToContactsClicked(@NonNull Contact contact); void onMessageSharedContactClicked(@NonNull List choices); void onInviteSharedContactClicked(@NonNull List choices); - void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms); + void onReactionClicked(@NonNull MultiselectPart multiselectPart, long messageId, boolean isMms); void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId); void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord); void onMessageWithRecaptchaNeededClicked(@NonNull MessageRecord messageRecord); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/MaskView.java b/app/src/main/java/org/thoughtcrime/securesms/components/MaskView.java deleted file mode 100644 index 69ba95ce2..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/MaskView.java +++ /dev/null @@ -1,152 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffXfermode; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -public class MaskView extends View { - - private MaskTarget maskTarget; - private ViewGroup activityContentView; - private Paint maskPaint; - private Rect drawingRect = new Rect(); - private float targetParentTranslationY; - - private final ViewTreeObserver.OnDrawListener onDrawListener = this::invalidate; - - public MaskView(@NonNull Context context) { - super(context); - } - - public MaskView(@NonNull Context context, @Nullable AttributeSet attributeSet) { - super(context, attributeSet); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - - setLayerType(LAYER_TYPE_HARDWARE, maskPaint); - - maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - activityContentView = getRootView().findViewById(android.R.id.content); - } - - public void setTarget(@Nullable MaskTarget maskTarget) { - if (this.maskTarget != null) { - removeOnDrawListener(this.maskTarget, onDrawListener); - } - - this.maskTarget = maskTarget; - - if (this.maskTarget != null) { - addOnDrawListener(maskTarget, onDrawListener); - } - - invalidate(); - } - - public void setTargetParentTranslationY(float targetParentTranslationY) { - this.targetParentTranslationY = targetParentTranslationY; - } - - @Override - protected void onDraw(@NonNull Canvas canvas) { - super.onDraw(canvas); - - if (nothingToMask(maskTarget)) { - return; - } - - maskTarget.getPrimaryTarget().getDrawingRect(drawingRect); - activityContentView.offsetDescendantRectToMyCoords(maskTarget.getPrimaryTarget(), drawingRect); - - drawingRect.top += targetParentTranslationY; - drawingRect.bottom += targetParentTranslationY; - - Bitmap mask = Bitmap.createBitmap(maskTarget.getPrimaryTarget().getWidth(), drawingRect.height(), Bitmap.Config.ARGB_8888); - Canvas maskCanvas = new Canvas(mask); - - maskTarget.draw(maskCanvas); - - canvas.clipRect(drawingRect.left, Math.max(drawingRect.top, getTop() + getPaddingTop()), drawingRect.right, Math.min(drawingRect.bottom, getBottom() - getPaddingBottom())); - - ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) maskTarget.getPrimaryTarget().getLayoutParams(); - canvas.drawBitmap(mask, params.leftMargin, drawingRect.top, maskPaint); - - mask.recycle(); - } - - private static void removeOnDrawListener(@NonNull MaskTarget maskTarget, @NonNull ViewTreeObserver.OnDrawListener onDrawListener) { - for (View view : maskTarget.getAllTargets()) { - if (view != null) { - view.getViewTreeObserver().removeOnDrawListener(onDrawListener); - } - } - } - - private static void addOnDrawListener(@NonNull MaskTarget maskTarget, @NonNull ViewTreeObserver.OnDrawListener onDrawListener) { - for (View view : maskTarget.getAllTargets()) { - if (view != null) { - view.getViewTreeObserver().addOnDrawListener(onDrawListener); - } - } - } - - private static boolean nothingToMask(@Nullable MaskTarget maskTarget) { - if (maskTarget == null) { - return true; - } - - for (View view : maskTarget.getAllTargets()) { - if (view == null || !view.isAttachedToWindow()) { - return true; - } - } - - return false; - } - - public static class MaskTarget { - - private final View primaryTarget; - - public MaskTarget(@NonNull View primaryTarget) { - this.primaryTarget = primaryTarget; - } - - final @NonNull View getPrimaryTarget() { - return primaryTarget; - } - - protected @NonNull List getAllTargets() { - return Collections.singletonList(primaryTarget); - } - - protected void draw(@NonNull Canvas canvas) { - primaryTarget.draw(canvas); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 66f44c3ba..8db9c8482 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -115,7 +115,6 @@ import org.thoughtcrime.securesms.components.HidingLinearLayout; import org.thoughtcrime.securesms.components.InputAwareLayout; import org.thoughtcrime.securesms.components.InputPanel; import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener; -import org.thoughtcrime.securesms.components.MaskView; import org.thoughtcrime.securesms.components.SendButton; import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.components.TypingStatusSender; @@ -158,8 +157,6 @@ import org.thoughtcrime.securesms.database.DraftDatabase; import org.thoughtcrime.securesms.database.DraftDatabase.Draft; import org.thoughtcrime.securesms.database.DraftDatabase.Drafts; import org.thoughtcrime.securesms.database.GroupDatabase; -import org.thoughtcrime.securesms.database.IdentityDatabase; -import org.thoughtcrime.securesms.database.model.IdentityRecord; import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus; import org.thoughtcrime.securesms.database.MentionUtil; import org.thoughtcrime.securesms.database.MentionUtil.UpdatedBodyAndMentions; @@ -168,6 +165,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.identity.IdentityRecordList; +import org.thoughtcrime.securesms.database.model.IdentityRecord; import org.thoughtcrime.securesms.database.model.Mention; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; @@ -2430,11 +2428,12 @@ public class ConversationActivity extends PassphraseRequiredActivity @Override public void onReactWithAnyEmojiDialogDismissed() { - reactionDelegate.hideMask(); + reactionDelegate.hide(); } @Override public void onReactWithAnyEmojiSelected(@NonNull String emoji) { + reactionDelegate.hide(); } @Override @@ -3334,7 +3333,7 @@ public class ConversationActivity extends PassphraseRequiredActivity @Override public void onReactionsDialogDismissed() { - reactionDelegate.hideMask(); + fragment.clearFocusedItem(); } @Override @@ -3669,19 +3668,13 @@ public class ConversationActivity extends PassphraseRequiredActivity } @Override - public void handleReaction(@NonNull MaskView.MaskTarget maskTarget, - @NonNull ConversationMessage conversationMessage, + public void handleReaction(@NonNull ConversationMessage conversationMessage, @NonNull Toolbar.OnMenuItemClickListener toolbarListener, @NonNull ConversationReactionOverlay.OnHideListener onHideListener) { reactionDelegate.setOnToolbarItemClickedListener(toolbarListener); reactionDelegate.setOnHideListener(onHideListener); - reactionDelegate.show(this, maskTarget, recipient.get(), conversationMessage, inputAreaHeight(), groupViewModel.isNonAdminInAnnouncementGroup()); - } - - @Override - public void onListVerticalTranslationChanged(float translationY) { - reactionDelegate.setListVerticalTranslation(translationY); + reactionDelegate.show(this, recipient.get(), conversationMessage, groupViewModel.isNonAdminInAnnouncementGroup()); } @Override @@ -3699,11 +3692,6 @@ public class ConversationActivity extends PassphraseRequiredActivity } } - @Override - public void handleReactionDetails(@NonNull MaskView.MaskTarget maskTarget) { - reactionDelegate.showMask(maskTarget, titleView.getMeasuredHeight(), inputAreaHeight()); - } - @Override public void onVoiceNotePause(@NonNull Uri uri) { voiceNoteMediaController.pausePlayback(uri); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index b7fdedb1b..34f8a2a23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -75,7 +75,6 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.VerifyIdentityActivity; import org.thoughtcrime.securesms.components.ConversationScrollToView; import org.thoughtcrime.securesms.components.ConversationTypingView; -import org.thoughtcrime.securesms.components.MaskView; import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.components.TypingStatusRepository; import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager; @@ -224,6 +223,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect private GiphyMp4ProjectionRecycler giphyMp4ProjectionRecycler; private Colorizer colorizer; private ConversationUpdateTick conversationUpdateTick; + private MultiselectItemDecoration multiselectItemDecoration; public static void prepare(@NonNull Context context) { FrameLayout parent = new FrameLayout(context); @@ -275,22 +275,21 @@ public class ConversationFragment extends LoggingFragment implements Multiselect return adapter.getSelectedItems().contains(multiselectPart); } }); - MultiselectItemDecoration multiselectItemDecoration = new MultiselectItemDecoration(requireContext(), - () -> conversationViewModel.getWallpaper().getValue(), - multiselectItemAnimator::getSelectedProgressForPart, - multiselectItemAnimator::isInitialAnimation); + multiselectItemDecoration = new MultiselectItemDecoration(requireContext(), + () -> conversationViewModel.getWallpaper().getValue(), + multiselectItemAnimator::getSelectedProgressForPart, + multiselectItemAnimator::isInitialAnimation); list.setHasFixedSize(false); list.setLayoutManager(layoutManager); + + RecyclerViewColorizer recyclerViewColorizer = new RecyclerViewColorizer(list); + list.addItemDecoration(multiselectItemDecoration); list.setItemAnimator(multiselectItemAnimator); getViewLifecycleOwner().getLifecycle().addObserver(multiselectItemDecoration); - if (Build.VERSION.SDK_INT >= 31) { - list.setOverScrollMode(View.OVER_SCROLL_NEVER); - } - snapToTopDataObserver = new ConversationSnapToTopDataObserver(list, new ConversationScrollRequestValidator()); conversationBanner = (ConversationBannerView) inflater.inflate(R.layout.conversation_item_banner, container, false); topLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false); @@ -319,6 +318,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect this.messageCountsViewModel = ViewModelProviders.of(requireActivity()).get(MessageCountsViewModel.class); this.conversationViewModel = ViewModelProviders.of(requireActivity(), new ConversationViewModel.Factory()).get(ConversationViewModel.class); + conversationViewModel.getChatColors().observe(getViewLifecycleOwner(), recyclerViewColorizer::setChatColors); conversationViewModel.getMessages().observe(getViewLifecycleOwner(), messages -> { ConversationAdapter adapter = getListAdapter(); if (adapter != null) { @@ -350,9 +350,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect updateToolbarDependentMargins(); colorizer = new Colorizer(); - RecyclerViewColorizer recyclerViewColorizer = new RecyclerViewColorizer(list); - - conversationViewModel.getChatColors().observe(getViewLifecycleOwner(), recyclerViewColorizer::setChatColors); conversationViewModel.getNameColorsMap().observe(getViewLifecycleOwner(), nameColorsMap -> { colorizer.onNameColorsChanged(nameColorsMap); @@ -387,11 +384,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect return callback; } - private @NonNull MaskView.MaskTarget getMaskTarget(@NonNull View itemView) { - int adapterPosition = list.getChildAdapterPosition(itemView); - View videoPlayer = giphyMp4ProjectionRecycler.getVideoPlayerAtAdapterPosition(adapterPosition); - - return new ConversationItemMaskTarget((ConversationItem) itemView, videoPlayer); + public void clearFocusedItem() { + multiselectItemDecoration.setFocusedItem(null); + list.invalidateItemDecorations(); } private void setupListLayoutListeners() { @@ -419,9 +414,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect list.setTranslationY(Math.min(0, -chTop)); list.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER); } - - int offset = WindowUtil.isStatusBarPresent(requireActivity().getWindow()) ? ViewUtil.getStatusBarHeight(list) : 0; - listener.onListVerticalTranslationChanged(list.getTranslationY() - offset); } private void updateConversationItemTimestamps() { @@ -1285,14 +1277,11 @@ public class ConversationFragment extends LoggingFragment implements Multiselect void onMessageActionToolbarOpened(); void onForwardClicked(); void onMessageRequest(@NonNull MessageRequestViewModel viewModel); - void handleReaction(@NonNull MaskView.MaskTarget maskTarget, - @NonNull ConversationMessage conversationMessage, + void handleReaction(@NonNull ConversationMessage conversationMessage, @NonNull Toolbar.OnMenuItemClickListener toolbarListener, @NonNull ConversationReactionOverlay.OnHideListener onHideListener); void onCursorChanged(); - void onListVerticalTranslationChanged(float translationY); void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord); - void handleReactionDetails(@NonNull MaskView.MaskTarget maskTarget); void onVoiceNotePause(@NonNull Uri uri); void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress); void onVoiceNoteSeekTo(@NonNull Uri uri, double progress); @@ -1402,14 +1391,19 @@ public class ConversationFragment extends LoggingFragment implements Multiselect (!recipient.get().isGroup() || recipient.get().isActiveGroup()) && ((ConversationAdapter) list.getAdapter()).getSelectedItems().isEmpty()) { + multiselectItemDecoration.setFocusedItem(new MultiselectPart.Message(item.getConversationMessage())); + list.invalidateItemDecorations(); + isReacting = true; list.setLayoutFrozen(true); - listener.handleReaction(getMaskTarget(itemView), item.getConversationMessage(), new ReactionsToolbarListener(item.getConversationMessage()), () -> { + listener.handleReaction(item.getConversationMessage(), new ReactionsToolbarListener(item.getConversationMessage()), () -> { isReacting = false; list.setLayoutFrozen(false); WindowUtil.setLightStatusBarFromTheme(requireActivity()); + clearFocusedItem(); }); } else { + clearFocusedItem(); ((ConversationAdapter) list.getAdapter()).toggleSelection(item); list.getAdapter().notifyDataSetChanged(); @@ -1550,10 +1544,10 @@ public class ConversationFragment extends LoggingFragment implements Multiselect } @Override - public void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms) { + public void onReactionClicked(@NonNull MultiselectPart multiselectPart, long messageId, boolean isMms) { if (getContext() == null) return; - listener.handleReactionDetails(getMaskTarget(reactionTarget)); + multiselectItemDecoration.setFocusedItem(multiselectPart); ReactionsBottomSheetDialogFragment.create(messageId, isMms).show(requireFragmentManager(), null); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 74e255af2..9948b2b4f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -142,6 +142,7 @@ import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; /** * A view that displays an individual conversation item within a conversation @@ -1368,7 +1369,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo reactionsView.setOnClickListener(v -> { if (eventListener == null) return; - eventListener.onReactionClicked(this, current.getId(), current.isMms()); + eventListener.onReactionClicked(new MultiselectPart.Message(conversationMessage), current.getId(), current.isMms()); }); } @@ -1720,10 +1721,16 @@ public final class ConversationItem extends RelativeLayout implements BindableCo if (messageRecord.isOutgoing() && !hasNoBubble(messageRecord) && !messageRecord.isRemoteDelete() && - bodyBubbleCorners != null && - bodyBubble.getProjections().isEmpty()) + bodyBubbleCorners != null) { - projections.add(Projection.relativeToViewRoot(bodyBubble, bodyBubbleCorners).translateX(bodyBubble.getTranslationX())); + Projection bodyBubbleToRoot = Projection.relativeToViewRoot(bodyBubble, bodyBubbleCorners).translateX(bodyBubble.getTranslationX()); + Projection videoToBubble = bodyBubble.getVideoPlayerProjection(); + if (videoToBubble != null) { + Projection videoToRoot = Projection.translateFromDescendantToParentCoords(videoToBubble, bodyBubble, (ViewGroup) getRootView()); + projections.addAll(Projection.getCapAndTail(bodyBubbleToRoot, videoToRoot)); + } else { + projections.add(bodyBubbleToRoot); + } } if (messageRecord.isOutgoing() && diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemBodyBubble.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemBodyBubble.java index 5b1d871c4..bde706acd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemBodyBubble.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemBodyBubble.java @@ -69,6 +69,10 @@ public class ConversationItemBodyBubble extends LinearLayout { clipProjectionDrawable.setProjections(getProjections()); } + public @Nullable Projection getVideoPlayerProjection() { + return videoPlayerProjection; + } + public @NonNull Set getProjections() { return Stream.of(quoteViewProjection, videoPlayerProjection) .filterNot(Objects::isNull) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemMaskTarget.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemMaskTarget.java deleted file mode 100644 index fa69e41f6..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemMaskTarget.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.thoughtcrime.securesms.conversation; - -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; - -import com.annimon.stream.Stream; - -import org.thoughtcrime.securesms.components.MaskView; -import org.thoughtcrime.securesms.util.Projection; - -import java.util.Arrays; -import java.util.List; - -/** - * Masking area to ensure proper rendering of Reactions overlay. - */ -public final class ConversationItemMaskTarget extends MaskView.MaskTarget { - - private final ConversationItem conversationItem; - private final View videoContainer; - private final Paint paint; - - public ConversationItemMaskTarget(@NonNull ConversationItem conversationItem, - @Nullable View videoContainer) - { - super(conversationItem); - this.conversationItem = conversationItem; - this.videoContainer = videoContainer; - this.paint = new Paint(Paint.ANTI_ALIAS_FLAG); - - paint.setColor(Color.BLACK); - paint.setStyle(Paint.Style.FILL); - } - - @Override - protected @NonNull List getAllTargets() { - if (videoContainer == null) { - return super.getAllTargets(); - } else { - return Arrays.asList(conversationItem, videoContainer); - } - } - - @Override - protected void draw(@NonNull Canvas canvas) { - super.draw(canvas); - - List projections = Stream.of(conversationItem.getColorizerProjections()).map(p -> - Projection.translateFromRootToDescendantCoords(p, conversationItem) - ).toList(); - - if (videoContainer != null) { - projections.add(conversationItem.getGiphyMp4PlayableProjection((RecyclerView) conversationItem.getParent())); - } - - for (Projection projection : projections) { - canvas.drawPath(projection.getPath(), paint); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionDelegate.java index d931c01c4..140ff8ac2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionDelegate.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionDelegate.java @@ -7,7 +7,6 @@ import android.view.MotionEvent; import androidx.annotation.NonNull; import androidx.appcompat.widget.Toolbar; -import org.thoughtcrime.securesms.components.MaskView; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.views.Stub; @@ -27,7 +26,6 @@ final class ConversationReactionDelegate { private ConversationReactionOverlay.OnReactionSelectedListener onReactionSelectedListener; private Toolbar.OnMenuItemClickListener onToolbarItemClickedListener; private ConversationReactionOverlay.OnHideListener onHideListener; - private float translationY; ConversationReactionDelegate(@NonNull Stub overlayStub) { this.overlayStub = overlayStub; @@ -38,17 +36,11 @@ final class ConversationReactionDelegate { } void show(@NonNull Activity activity, - @NonNull MaskView.MaskTarget maskTarget, @NonNull Recipient conversationRecipient, @NonNull ConversationMessage conversationMessage, - int maskPaddingBottom, boolean isNonAdminInAnnouncementGroup) { - resolveOverlay().show(activity, maskTarget, conversationRecipient, conversationMessage, maskPaddingBottom, lastSeenDownPoint, isNonAdminInAnnouncementGroup); - } - - void showMask(@NonNull MaskView.MaskTarget maskTarget, int maskPaddingTop, int maskPaddingBottom) { - resolveOverlay().showMask(maskTarget, maskPaddingTop, maskPaddingBottom); + resolveOverlay().show(activity, conversationRecipient, conversationMessage, lastSeenDownPoint, isNonAdminInAnnouncementGroup); } void hide() { @@ -59,10 +51,6 @@ final class ConversationReactionDelegate { overlayStub.get().hideForReactWithAny(); } - void hideMask() { - overlayStub.get().hideMask(); - } - void setOnReactionSelectedListener(@NonNull ConversationReactionOverlay.OnReactionSelectedListener onReactionSelectedListener) { this.onReactionSelectedListener = onReactionSelectedListener; @@ -87,14 +75,6 @@ final class ConversationReactionDelegate { } } - void setListVerticalTranslation(float translationY) { - this.translationY = translationY; - - if (overlayStub.resolved()) { - overlayStub.get().setListVerticalTranslation(translationY); - } - } - @NonNull MessageRecord getMessageRecord() { if (!overlayStub.resolved()) { throw new IllegalStateException("Cannot call getMessageRecord right now."); @@ -118,7 +98,6 @@ final class ConversationReactionDelegate { ConversationReactionOverlay overlay = overlayStub.get(); overlay.requestFitSystemWindows(); - overlay.setListVerticalTranslation(translationY); overlay.setOnHideListener(onHideListener); overlay.setOnToolbarItemClickedListener(onToolbarItemClickedListener); overlay.setOnReactionSelectedListener(onReactionSelectedListener); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java index 67e508707..d9d01357d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java @@ -28,7 +28,6 @@ import com.annimon.stream.Stream; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.animation.AnimationCompleteListener; -import org.thoughtcrime.securesms.components.MaskView; import org.thoughtcrime.securesms.components.emoji.EmojiImageView; import org.thoughtcrime.securesms.components.emoji.EmojiUtil; import org.thoughtcrime.securesms.database.model.MessageRecord; @@ -40,7 +39,6 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.WindowUtil; -import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -72,7 +70,6 @@ public final class ConversationReactionOverlay extends RelativeLayout { private ConstraintLayout foregroundView; private View selectedView; private EmojiImageView[] emojiViews; - private MaskView maskView; private Toolbar toolbar; private float touchDownDeadZoneSize; @@ -112,7 +109,6 @@ public final class ConversationReactionOverlay extends RelativeLayout { 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); - maskView = findViewById(R.id.conversation_reaction_mask); toolbar = findViewById(R.id.conversation_reaction_toolbar); toolbar.setOnMenuItemClickListener(this::handleToolbarItemClicked); @@ -144,15 +140,9 @@ public final class ConversationReactionOverlay extends RelativeLayout { initAnimators(); } - public void setListVerticalTranslation(float translationY) { - maskView.setTargetParentTranslationY(translationY); - } - public void show(@NonNull Activity activity, - @NonNull MaskView.MaskTarget maskTarget, @NonNull Recipient conversationRecipient, @NonNull ConversationMessage conversationMessage, - int maskPaddingBottom, @NonNull PointF lastSeenDownPoint, boolean isNonAdminInAnnouncementGroup) { @@ -195,9 +185,6 @@ public final class ConversationReactionOverlay extends RelativeLayout { verticalScrubBoundary.update(lastSeenDownPoint.y - distanceFromTouchDownPointToTopOfScrubberDeadZone, lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone); - maskView.setPadding(0, 0, 0, maskPaddingBottom); - maskView.setTarget(maskTarget); - hideAnimatorSet.end(); toolbar.setVisibility(VISIBLE); setVisibility(View.VISIBLE); @@ -214,18 +201,7 @@ public final class ConversationReactionOverlay extends RelativeLayout { } } - public void showMask(@NonNull MaskView.MaskTarget maskTarget, int maskPaddingTop, int maskPaddingBottom) { - maskView.setPadding(0, maskPaddingTop, 0, maskPaddingBottom); - maskView.setTarget(maskTarget); - - hideAnimatorSet.end(); - toolbar.setVisibility(GONE); - setVisibility(VISIBLE); - revealMaskAnimatorSet.start(); - } - public void hide() { - maskView.setTarget(null); hideInternal(hideAnimatorSet, onHideListener); } @@ -233,14 +209,6 @@ public final class ConversationReactionOverlay extends RelativeLayout { hideInternal(hideAnimatorSet, null); } - public void hideMask() { - hideMaskAnimatorSet.start(); - - if (onHideListener != null) { - onHideListener.onHide(); - } - } - private void hideInternal(@NonNull AnimatorSet hideAnimatorSet, @Nullable OnHideListener onHideListener) { overlayState = OverlayState.HIDDEN; @@ -540,7 +508,6 @@ public final class ConversationReactionOverlay extends RelativeLayout { .toList(); Animator overlayRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in); - overlayRevealAnim.setTarget(maskView); overlayRevealAnim.setDuration(duration); reveals.add(overlayRevealAnim); @@ -575,7 +542,6 @@ public final class ConversationReactionOverlay extends RelativeLayout { .toList(); Animator overlayHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out); - overlayHideAnim.setTarget(maskView); overlayHideAnim.setDuration(duration); Animator backgroundHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/RecyclerViewColorizer.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/RecyclerViewColorizer.kt index 80c1f5acb..087a2b8dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/RecyclerViewColorizer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/RecyclerViewColorizer.kt @@ -118,6 +118,7 @@ class RecyclerViewColorizer(private val recyclerView: RecyclerView) { mask.setBounds(0, 0, parent.width, parent.height) mask.draw(canvas) } else { + colorPaint.color = chatColors.asSingleColor() canvas.drawRect( 0f, 0f, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt index 4f0b57e3e..3ce1ef45a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt @@ -9,9 +9,11 @@ import android.graphics.Path import android.graphics.Rect import android.graphics.Region import android.view.View +import android.view.ViewGroup import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.view.children +import androidx.core.view.forEach import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.RecyclerView @@ -54,6 +56,12 @@ class MultiselectItemDecoration( private var checkedBitmap: Bitmap? = null + private var focusedItem: MultiselectPart? = null + + fun setFocusedItem(multiselectPart: MultiselectPart?) { + this.focusedItem = multiselectPart + } + override fun onCreate(owner: LifecycleOwner) { val bitmap = Bitmap.createBitmap(circleRadius * 2, circleRadius * 2, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) @@ -67,6 +75,8 @@ class MultiselectItemDecoration( checkedBitmap = null } + private val shadeColor = ContextCompat.getColor(context, R.color.reactions_screen_shade_color) + private val unselectedPaint = Paint().apply { isAntiAlias = true strokeWidth = 1.5f @@ -102,6 +112,7 @@ class MultiselectItemDecoration( val adapter = parent.adapter as ConversationAdapter if (adapter.selectedItems.isEmpty()) { + drawFocusShadeUnderIfNecessary(canvas, parent) return } @@ -116,7 +127,7 @@ class MultiselectItemDecoration( val parts: MultiselectCollection = child.conversationMessage.multiselectCollection - val projections: List = child.colorizerProjections + val projections: List = child.colorizerProjections + if (child.canPlayContent()) listOf(child.getGiphyMp4PlayableProjection(child.rootView as ViewGroup)) else emptyList() path.reset() projections.forEach { it.applyToPath(path) } @@ -148,6 +159,7 @@ class MultiselectItemDecoration( override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { val adapter = parent.adapter as ConversationAdapter if (adapter.selectedItems.isEmpty()) { + drawFocusShadeOverIfNecessary(canvas, parent) return } @@ -285,4 +297,48 @@ class MultiselectItemDecoration( child.translationX = 0f } } + + private fun drawFocusShadeUnderIfNecessary(canvas: Canvas, parent: RecyclerView) { + val inFocus = focusedItem + if (inFocus != null) { + path.reset() + canvas.save() + + parent.forEach { child -> + if (child is Multiselectable && child.conversationMessage == inFocus.conversationMessage) { + path.addRect(child.left.toFloat(), child.top.toFloat(), child.right.toFloat(), child.bottom.toFloat(), Path.Direction.CW) + child.colorizerProjections.forEach { + path.op(it.path, Path.Op.DIFFERENCE) + } + + if (child.canPlayContent()) { + val mp4GifProjection = child.getGiphyMp4PlayableProjection(child.rootView as ViewGroup) + path.op(mp4GifProjection.path, Path.Op.DIFFERENCE) + } + } + } + + canvas.clipPath(path) + canvas.drawColor(shadeColor) + canvas.restore() + } + } + + private fun drawFocusShadeOverIfNecessary(canvas: Canvas, parent: RecyclerView) { + val inFocus = focusedItem + if (inFocus != null) { + path.reset() + canvas.save() + + parent.forEach { child -> + if (child is Multiselectable && child.conversationMessage == inFocus.conversationMessage) { + path.addRect(child.left.toFloat(), child.top.toFloat(), child.right.toFloat(), child.bottom.toFloat(), Path.Direction.CW) + } + } + + canvas.clipPath(path, Region.Op.DIFFERENCE) + canvas.drawColor(shadeColor) + canvas.restore() + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/Multiselectable.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/Multiselectable.kt index 9b437f8d0..21512dfad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/Multiselectable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/Multiselectable.kt @@ -3,8 +3,9 @@ package org.thoughtcrime.securesms.conversation.mutiselect import android.view.View import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.conversation.colors.Colorizable +import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable -interface Multiselectable : Colorizable { +interface Multiselectable : Colorizable, GiphyMp4Playable { val conversationMessage: ConversationMessage fun getTopBoundaryOfMultiselectPart(multiselectPart: MultiselectPart): Int diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ProjectionRecycler.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ProjectionRecycler.java index 860d1901d..3dad6001a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ProjectionRecycler.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ProjectionRecycler.java @@ -39,7 +39,7 @@ public final class GiphyMp4ProjectionRecycler implements GiphyMp4PlaybackControl for (final GiphyMp4Playable holder : holders) { if (playbackSet.contains(holder.getAdapterPosition())) { - startPlayback(acquireHolderForPosition(holder.getAdapterPosition()), holder); + startPlayback(recyclerView, acquireHolderForPosition(holder.getAdapterPosition()), holder); } else { holder.showProjectionArea(); } @@ -107,16 +107,22 @@ public final class GiphyMp4ProjectionRecycler implements GiphyMp4PlaybackControl holder.setCorners(projection.getCorners()); } - private void startPlayback(@NonNull GiphyMp4ProjectionPlayerHolder holder, @NonNull GiphyMp4Playable giphyMp4Playable) { + private void startPlayback(@NonNull RecyclerView parent, @NonNull GiphyMp4ProjectionPlayerHolder holder, @NonNull GiphyMp4Playable giphyMp4Playable) { if (!Objects.equals(holder.getMediaItem(), giphyMp4Playable.getMediaItem())) { holder.setOnPlaybackReady(null); giphyMp4Playable.showProjectionArea(); holder.show(); - holder.setOnPlaybackReady(giphyMp4Playable::hideProjectionArea); + holder.setOnPlaybackReady(() -> { + giphyMp4Playable.hideProjectionArea(); + parent.invalidateItemDecorations(); + }); holder.playContent(giphyMp4Playable.getMediaItem(), giphyMp4Playable.getPlaybackPolicyEnforcer()); } else { - holder.setOnPlaybackReady(giphyMp4Playable::hideProjectionArea); + holder.setOnPlaybackReady(() -> { + giphyMp4Playable.hideProjectionArea(); + parent.invalidateItemDecorations(); + }); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Projection.java b/app/src/main/java/org/thoughtcrime/securesms/util/Projection.java index 453dcd1bd..1bc935cb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Projection.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Projection.java @@ -14,6 +14,9 @@ import androidx.recyclerview.widget.RecyclerView; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.components.CornerMask; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Objects; /** @@ -159,6 +162,42 @@ public final class Projection { return new Projection(viewBounds.left, viewBounds.top, descendantProjection.width, descendantProjection.height, descendantProjection.corners); } + public static @NonNull List getCapAndTail(@NonNull Projection parentProjection, @NonNull Projection childProjection) { + if (parentProjection.equals(childProjection)) { + return Collections.emptyList(); + } + + float topX = parentProjection.x; + float topY = parentProjection.y; + int topWidth = parentProjection.getWidth(); + int topHeight = (int) (childProjection.y - parentProjection.y); + + final Corners topCorners; + Corners parentCorners = parentProjection.getCorners(); + if (parentCorners != null) { + topCorners = new Corners(parentCorners.topLeft, parentCorners.topRight, 0f, 0f); + } else { + topCorners = null; + } + + float bottomX = parentProjection.x; + float bottomY = parentProjection.y + topHeight + childProjection.getHeight(); + int bottomWidth = parentProjection.getWidth(); + int bottomHeight = (int) ((parentProjection.y + parentProjection.getHeight()) - bottomY); + + final Corners bottomCorners; + if (parentCorners != null) { + bottomCorners = new Corners(0f, 0f, parentCorners.bottomRight, parentCorners.bottomLeft); + } else { + bottomCorners = null; + } + + return Arrays.asList( + new Projection(topX, topY, topWidth, topHeight, topCorners), + new Projection(bottomX, bottomY, bottomWidth, bottomHeight, bottomCorners) + ); + } + public static final class Corners { private final float topLeft; private final float topRight; diff --git a/app/src/main/res/layout/conversation_reaction_scrubber.xml b/app/src/main/res/layout/conversation_reaction_scrubber.xml index ff33416cf..df01b641a 100644 --- a/app/src/main/res/layout/conversation_reaction_scrubber.xml +++ b/app/src/main/res/layout/conversation_reaction_scrubber.xml @@ -13,13 +13,6 @@ android:visibility="gone" tools:visibility="visible"> - -