kopia lustrzana https://github.com/ryukoposting/Signal-Android
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 visiblefork-5.53.8
rodzic
705839068a
commit
7e91132e7e
|
@ -66,7 +66,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
|||
void onAddToContactsClicked(@NonNull Contact contact);
|
||||
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onInviteSharedContactClicked(@NonNull List<Recipient> 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);
|
||||
|
|
|
@ -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<View> getAllTargets() {
|
||||
return Collections.singletonList(primaryTarget);
|
||||
}
|
||||
|
||||
protected void draw(@NonNull Canvas canvas) {
|
||||
primaryTarget.draw(canvas);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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() &&
|
||||
|
|
|
@ -69,6 +69,10 @@ public class ConversationItemBodyBubble extends LinearLayout {
|
|||
clipProjectionDrawable.setProjections(getProjections());
|
||||
}
|
||||
|
||||
public @Nullable Projection getVideoPlayerProjection() {
|
||||
return videoPlayerProjection;
|
||||
}
|
||||
|
||||
public @NonNull Set<Projection> getProjections() {
|
||||
return Stream.of(quoteViewProjection, videoPlayerProjection)
|
||||
.filterNot(Objects::isNull)
|
||||
|
|
|
@ -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<View> getAllTargets() {
|
||||
if (videoContainer == null) {
|
||||
return super.getAllTargets();
|
||||
} else {
|
||||
return Arrays.asList(conversationItem, videoContainer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void draw(@NonNull Canvas canvas) {
|
||||
super.draw(canvas);
|
||||
|
||||
List<Projection> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ConversationReactionOverlay> 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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Projection> = child.colorizerProjections
|
||||
val projections: List<Projection> = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Projection> 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;
|
||||
|
|
|
@ -13,13 +13,6 @@
|
|||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<org.thoughtcrime.securesms.components.MaskView
|
||||
android:id="@+id/conversation_reaction_mask"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0"
|
||||
android:background="@color/reactions_screen_shade_color" />
|
||||
|
||||
<include
|
||||
android:id="@+id/conversation_reaction_toolbar"
|
||||
layout="@layout/conversation_reaction_long_press_toolbar" />
|
||||
|
|
Ładowanie…
Reference in New Issue