Improve showing context menu with keyboard open.

This commit is contained in:
Rashad Sookram 2022-02-03 17:06:17 -05:00 committed by GitHub
parent f0414922be
commit 24a875c73a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 248 additions and 97 deletions

View file

@ -1308,25 +1308,26 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
public interface ConversationFragmentListener extends VoiceNoteMediaControllerOwner { public interface ConversationFragmentListener extends VoiceNoteMediaControllerOwner {
void setThreadId(long threadId); boolean isKeyboardOpen();
void handleReplyMessage(ConversationMessage conversationMessage); void setThreadId(long threadId);
void onMessageActionToolbarOpened(); void handleReplyMessage(ConversationMessage conversationMessage);
void onBottomActionBarVisibilityChanged(int visibility); void onMessageActionToolbarOpened();
void onForwardClicked(); void onBottomActionBarVisibilityChanged(int visibility);
void onMessageRequest(@NonNull MessageRequestViewModel viewModel); void onForwardClicked();
void handleReaction(@NonNull ConversationMessage conversationMessage, void onMessageRequest(@NonNull MessageRequestViewModel viewModel);
@NonNull ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener, void handleReaction(@NonNull ConversationMessage conversationMessage,
@NonNull SelectedConversationModel selectedConversationModel, @NonNull ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener,
@NonNull ConversationReactionOverlay.OnHideListener onHideListener); @NonNull SelectedConversationModel selectedConversationModel,
void onCursorChanged(); @NonNull ConversationReactionOverlay.OnHideListener onHideListener);
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord); void onCursorChanged();
void onVoiceNotePause(@NonNull Uri uri); void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress); void onVoiceNotePause(@NonNull Uri uri);
void onVoiceNoteResume(@NonNull Uri uri, long messageId); void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress);
void onVoiceNoteSeekTo(@NonNull Uri uri, double progress); void onVoiceNoteResume(@NonNull Uri uri, long messageId);
void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed); void onVoiceNoteSeekTo(@NonNull Uri uri, double progress);
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver); void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed);
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver); void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
} }
private class ConversationScrollListener extends OnScrollListener { private class ConversationScrollListener extends OnScrollListener {
@ -1460,6 +1461,8 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
ConversationItem conversationItem = (ConversationItem) itemView; ConversationItem conversationItem = (ConversationItem) itemView;
Bitmap bitmap = ConversationItemSelection.snapshotView(conversationItem, list, messageRecord, videoBitmap); Bitmap bitmap = ConversationItemSelection.snapshotView(conversationItem, list, messageRecord, videoBitmap);
View focusedView = listener.isKeyboardOpen() ? conversationItem.getRootView().findFocus() : null;
final ConversationItemBodyBubble bodyBubble = conversationItem.bodyBubble; final ConversationItemBodyBubble bodyBubble = conversationItem.bodyBubble;
SelectedConversationModel selectedConversationModel = new SelectedConversationModel(bitmap, SelectedConversationModel selectedConversationModel = new SelectedConversationModel(bitmap,
itemView.getX(), itemView.getX(),
@ -1468,28 +1471,41 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
bodyBubble.getY(), bodyBubble.getY(),
bodyBubble.getWidth(), bodyBubble.getWidth(),
audioUri, audioUri,
messageRecord.isOutgoing()); messageRecord.isOutgoing(),
focusedView);
bodyBubble.setVisibility(View.INVISIBLE); bodyBubble.setVisibility(View.INVISIBLE);
listener.handleReaction(item.getConversationMessage(), new ReactionsToolbarListener(item.getConversationMessage()), selectedConversationModel, () -> { ViewUtil.hideKeyboard(requireContext(), conversationItem);
reactionsShade.setVisibility(View.GONE);
list.setLayoutFrozen(false);
if (selectedConversationModel.getAudioUri() != null) { listener.handleReaction(item.getConversationMessage(),
listener.onVoiceNoteResume(selectedConversationModel.getAudioUri(), messageRecord.getId()); new ReactionsToolbarListener(item.getConversationMessage()),
} selectedConversationModel,
new ConversationReactionOverlay.OnHideListener() {
@Override public void startHide() {
multiselectItemDecoration.hideShade(list);
}
WindowUtil.setLightStatusBarFromTheme(requireActivity()); @Override public void onHide() {
clearFocusedItem(); reactionsShade.setVisibility(View.GONE);
list.setLayoutFrozen(false);
if (mp4Holder != null) { if (selectedConversationModel.getAudioUri() != null) {
mp4Holder.show(); listener.onVoiceNoteResume(selectedConversationModel.getAudioUri(), messageRecord.getId());
mp4Holder.resume(); }
}
bodyBubble.setVisibility(View.VISIBLE); WindowUtil.setLightStatusBarFromTheme(requireActivity());
}); WindowUtil.setLightNavigationBarFromTheme(requireActivity());
clearFocusedItem();
if (mp4Holder != null) {
mp4Holder.show();
mp4Holder.resume();
}
bodyBubble.setVisibility(View.VISIBLE);
}
});
} }
} else { } else {
clearFocusedItem(); clearFocusedItem();

View file

@ -3,10 +3,12 @@ package org.thoughtcrime.securesms.conversation
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Path import android.graphics.Path
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.applyCanvas import androidx.core.graphics.applyCanvas
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
import androidx.core.graphics.withClip import androidx.core.graphics.withClip
import androidx.core.graphics.withTranslation import androidx.core.graphics.withTranslation
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.util.hasNoBubble import org.thoughtcrime.securesms.util.hasNoBubble
@ -76,6 +78,8 @@ object ConversationItemSelection {
} }
} }
conversationItem.destroyAllDrawingCaches()
return createBitmap(conversationItem.bodyBubble.width, conversationItem.bodyBubble.height).applyCanvas { return createBitmap(conversationItem.bodyBubble.width, conversationItem.bodyBubble.height).applyCanvas {
if (drawConversationItem) { if (drawConversationItem) {
conversationItem.bodyBubble.draw(this) conversationItem.bodyBubble.draw(this)
@ -98,3 +102,13 @@ object ConversationItemSelection {
} }
} }
} }
private fun ViewGroup.destroyAllDrawingCaches() {
children.forEach {
it.destroyDrawingCache()
if (it is ViewGroup) {
it.destroyAllDrawingCaches()
}
}
}

View file

@ -3791,6 +3791,11 @@ public class ConversationParentFragment extends Fragment
}); });
} }
@Override
public boolean isKeyboardOpen() {
return container.isKeyboardOpen();
}
@Override @Override
public void setThreadId(long threadId) { public void setThreadId(long threadId) {
this.threadId = threadId; this.threadId = threadId;

View file

@ -3,8 +3,10 @@ package org.thoughtcrime.securesms.conversation;
import android.animation.Animator; import android.animation.Animator;
import android.animation.AnimatorSet; import android.animation.AnimatorSet;
import android.animation.ObjectAnimator; import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.PointF; import android.graphics.PointF;
import android.graphics.Rect; import android.graphics.Rect;
@ -14,11 +16,10 @@ import android.util.AttributeSet;
import android.view.HapticFeedbackConstants; import android.view.HapticFeedbackConstants;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.Window; import android.view.Window;
import android.view.animation.DecelerateInterpolator; import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator; import android.view.animation.Interpolator;
import android.widget.RelativeLayout; import android.widget.FrameLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -51,7 +52,7 @@ import java.util.List;
import kotlin.Unit; import kotlin.Unit;
public final class ConversationReactionOverlay extends RelativeLayout { public final class ConversationReactionOverlay extends FrameLayout {
private static final Interpolator INTERPOLATOR = new DecelerateInterpolator(); private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
@ -94,6 +95,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
private int scrubberHorizontalMargin; private int scrubberHorizontalMargin;
private int animationEmojiStartDelayFactor; private int animationEmojiStartDelayFactor;
private int statusBarHeight; private int statusBarHeight;
private int bottomNavigationBarHeight;
private OnReactionSelectedListener onReactionSelectedListener; private OnReactionSelectedListener onReactionSelectedListener;
private OnActionSelectedListener onActionSelectedListener; private OnActionSelectedListener onActionSelectedListener;
@ -168,16 +170,24 @@ public final class ConversationReactionOverlay extends RelativeLayout {
if (Build.VERSION.SDK_INT >= 21) { if (Build.VERSION.SDK_INT >= 21) {
View statusBarBackground = activity.findViewById(android.R.id.statusBarBackground); View statusBarBackground = activity.findViewById(android.R.id.statusBarBackground);
statusBarHeight = statusBarBackground == null ? 0 : statusBarBackground.getHeight(); statusBarHeight = statusBarBackground == null ? 0 : statusBarBackground.getHeight();
View navigationBarBackground = activity.findViewById(android.R.id.navigationBarBackground);
bottomNavigationBarHeight = navigationBarBackground == null ? 0 : navigationBarBackground.getHeight();
} else { } else {
statusBarHeight = ViewUtil.getStatusBarHeight(this); statusBarHeight = ViewUtil.getStatusBarHeight(this);
bottomNavigationBarHeight = ViewUtil.getNavigationBarHeight(this);
} }
ViewGroup.LayoutParams layoutParams = inputShade.getLayoutParams(); boolean isLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
layoutParams.height = getInputPanelHeight(activity); if (isLandscape) {
inputShade.setLayoutParams(layoutParams); bottomNavigationBarHeight = 0;
}
toolbarShade.setVisibility(VISIBLE); toolbarShade.setVisibility(VISIBLE);
toolbarShade.setAlpha(1f);
inputShade.setVisibility(VISIBLE); inputShade.setVisibility(VISIBLE);
inputShade.setAlpha(1f);
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap(); Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
@ -191,6 +201,11 @@ public final class ConversationReactionOverlay extends RelativeLayout {
setVisibility(View.INVISIBLE); setVisibility(View.INVISIBLE);
if (Build.VERSION.SDK_INT >= 21) {
this.activity = activity;
updateSystemUiOnShow(activity);
}
ViewKt.doOnLayout(this, v -> { ViewKt.doOnLayout(this, v -> {
showAfterLayout(activity, conversationMessage, lastSeenDownPoint, isMessageOnLeft); showAfterLayout(activity, conversationMessage, lastSeenDownPoint, isMessageOnLeft);
return Unit.INSTANCE; return Unit.INSTANCE;
@ -198,7 +213,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
} }
private int getInputPanelHeight(@NonNull Activity activity) { private int getInputPanelHeight(@NonNull Activity activity) {
View bottomPanel = activity.findViewById(R.id.bottom_panel); View bottomPanel = activity.findViewById(R.id.conversation_activity_panel_parent);
View emojiDrawer = activity.findViewById(R.id.emoji_drawer); View emojiDrawer = activity.findViewById(R.id.emoji_drawer);
return bottomPanel.getHeight() + (emojiDrawer != null && emojiDrawer.getVisibility() == VISIBLE ? emojiDrawer.getHeight() : 0); return bottomPanel.getHeight() + (emojiDrawer != null && emojiDrawer.getVisibility() == VISIBLE ? emojiDrawer.getHeight() : 0);
@ -208,6 +223,11 @@ public final class ConversationReactionOverlay extends RelativeLayout {
@NonNull ConversationMessage conversationMessage, @NonNull ConversationMessage conversationMessage,
@NonNull PointF lastSeenDownPoint, @NonNull PointF lastSeenDownPoint,
boolean isMessageOnLeft) { boolean isMessageOnLeft) {
LayoutParams layoutParams = (LayoutParams) inputShade.getLayoutParams();
layoutParams.bottomMargin = bottomNavigationBarHeight;
layoutParams.height = getInputPanelHeight(activity);
inputShade.setLayoutParams(layoutParams);
contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(conversationMessage)); contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(conversationMessage));
conversationItem.setX(selectedConversationModel.getBubbleX()); conversationItem.setX(selectedConversationModel.getBubbleX());
@ -216,7 +236,8 @@ public final class ConversationReactionOverlay extends RelativeLayout {
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap(); Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
boolean isWideLayout = contextMenu.getMaxWidth() + scrubberWidth < getWidth(); boolean isWideLayout = contextMenu.getMaxWidth() + scrubberWidth < getWidth();
int bubbleWidth = selectedConversationModel.getBubbleWidth(); int overlayHeight = getHeight() - bottomNavigationBarHeight;
int bubbleWidth = selectedConversationModel.getBubbleWidth();
float endX = selectedConversationModel.getBubbleX(); float endX = selectedConversationModel.getBubbleX();
float endY = conversationItem.getY(); float endY = conversationItem.getY();
@ -230,7 +251,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
float reactionBarBackgroundY; float reactionBarBackgroundY;
if (isWideLayout) { if (isWideLayout) {
boolean everythingFitsVertically = reactionBarHeight + menuPadding + reactionBarTopPadding + conversationItemSnapshot.getHeight() < getHeight(); boolean everythingFitsVertically = reactionBarHeight + menuPadding + reactionBarTopPadding + conversationItemSnapshot.getHeight() < overlayHeight;
if (everythingFitsVertically) { if (everythingFitsVertically) {
boolean reactionBarFitsAboveItem = conversationItem.getY() > reactionBarHeight + menuPadding + reactionBarTopPadding; boolean reactionBarFitsAboveItem = conversationItem.getY() > reactionBarHeight + menuPadding + reactionBarTopPadding;
@ -241,7 +262,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
reactionBarBackgroundY = reactionBarTopPadding; reactionBarBackgroundY = reactionBarTopPadding;
} }
} else { } else {
float spaceAvailableForItem = getHeight() - reactionBarHeight - menuPadding * 2; float spaceAvailableForItem = overlayHeight - reactionBarHeight - menuPadding - reactionBarTopPadding;
endScale = spaceAvailableForItem / conversationItem.getHeight(); endScale = spaceAvailableForItem / conversationItem.getHeight();
endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1); endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
@ -249,27 +270,27 @@ public final class ConversationReactionOverlay extends RelativeLayout {
reactionBarBackgroundY = reactionBarTopPadding; reactionBarBackgroundY = reactionBarTopPadding;
} }
} else { } else {
boolean everythingFitsVertically = contextMenu.getMaxHeight() + conversationItemSnapshot.getHeight() + menuPadding + reactionBarHeight + reactionBarTopPadding < getHeight(); boolean everythingFitsVertically = contextMenu.getMaxHeight() + conversationItemSnapshot.getHeight() + menuPadding + reactionBarHeight + reactionBarTopPadding < overlayHeight;
if (everythingFitsVertically) { if (everythingFitsVertically) {
float bubbleBottom = selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight(); float bubbleBottom = selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight();
boolean menuFitsBelowItem = bubbleBottom + menuPadding + contextMenu.getMaxHeight() <= getHeight() + statusBarHeight; boolean menuFitsBelowItem = bubbleBottom + menuPadding + contextMenu.getMaxHeight() <= overlayHeight + statusBarHeight;
if (menuFitsBelowItem) { if (menuFitsBelowItem) {
reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight; reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight;
if (reactionBarBackgroundY < reactionBarTopPadding) { if (reactionBarBackgroundY < reactionBarTopPadding) {
endY = backgroundView.getHeight() + menuPadding; endY = backgroundView.getHeight() + menuPadding + reactionBarTopPadding;
reactionBarBackgroundY = reactionBarTopPadding; reactionBarBackgroundY = reactionBarTopPadding;
} }
} else { } else {
endY = getHeight() - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.getHeight(); endY = overlayHeight - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.getHeight();
reactionBarBackgroundY = endY - menuPadding - reactionBarHeight; reactionBarBackgroundY = endY - menuPadding - reactionBarHeight;
} }
endApparentTop = endY; endApparentTop = endY;
} else if (reactionBarHeight + contextMenu.getMaxHeight() + menuPadding * 2 < getHeight()) { } else if (reactionBarHeight + contextMenu.getMaxHeight() + menuPadding * 2 < overlayHeight) {
float spaceAvailableForItem = (float) getHeight() - contextMenu.getMaxHeight() - menuPadding * 2 - reactionBarHeight - reactionBarTopPadding; float spaceAvailableForItem = (float) overlayHeight - contextMenu.getMaxHeight() - menuPadding * 2 - reactionBarHeight - reactionBarTopPadding;
endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight(); endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight();
endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1); endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
@ -280,11 +301,11 @@ public final class ConversationReactionOverlay extends RelativeLayout {
contextMenu.setHeight(contextMenu.getMaxHeight() / 2); contextMenu.setHeight(contextMenu.getMaxHeight() / 2);
int menuHeight = contextMenu.getHeight(); int menuHeight = contextMenu.getHeight();
boolean fitsVertically = menuHeight + conversationItem.getHeight() + menuPadding * 2 + reactionBarHeight + reactionBarTopPadding < getHeight(); boolean fitsVertically = menuHeight + conversationItem.getHeight() + menuPadding * 2 + reactionBarHeight + reactionBarTopPadding < overlayHeight;
if (fitsVertically) { if (fitsVertically) {
float bubbleBottom = selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight(); float bubbleBottom = selectedConversationModel.getItemY() + selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight();
boolean menuFitsBelowItem = bubbleBottom + menuPadding + menuHeight <= getHeight() + statusBarHeight; boolean menuFitsBelowItem = bubbleBottom + menuPadding + menuHeight <= overlayHeight + statusBarHeight;
if (menuFitsBelowItem) { if (menuFitsBelowItem) {
reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight; reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight;
@ -294,12 +315,12 @@ public final class ConversationReactionOverlay extends RelativeLayout {
reactionBarBackgroundY = reactionBarTopPadding; reactionBarBackgroundY = reactionBarTopPadding;
} }
} else { } else {
endY = getHeight() - menuHeight - menuPadding - conversationItemSnapshot.getHeight(); endY = overlayHeight - menuHeight - menuPadding - conversationItemSnapshot.getHeight();
reactionBarBackgroundY = endY - reactionBarHeight - menuPadding; reactionBarBackgroundY = endY - reactionBarHeight - menuPadding;
} }
endApparentTop = endY; endApparentTop = endY;
} else { } else {
float spaceAvailableForItem = (float) getHeight() - menuHeight - menuPadding * 2 - reactionBarHeight - reactionBarTopPadding; float spaceAvailableForItem = (float) overlayHeight - menuHeight - menuPadding * 2 - reactionBarHeight - reactionBarTopPadding;
endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight(); endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight();
endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1); endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1);
@ -316,11 +337,6 @@ public final class ConversationReactionOverlay extends RelativeLayout {
hideAnimatorSet = newHideAnimatorSet(); hideAnimatorSet = newHideAnimatorSet();
setVisibility(View.VISIBLE); setVisibility(View.VISIBLE);
if (Build.VERSION.SDK_INT >= 21) {
this.activity = activity;
updateSystemUiOnShow(activity);
}
float scrubberX; float scrubberX;
if (isMessageOnLeft) { if (isMessageOnLeft) {
scrubberX = scrubberHorizontalMargin; scrubberX = scrubberHorizontalMargin;
@ -344,7 +360,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
if (isWideLayout) { if (isWideLayout) {
float scrubberRight = scrubberX + scrubberWidth; float scrubberRight = scrubberX + scrubberWidth;
float offsetX = isMessageOnLeft ? scrubberRight + menuPadding : scrubberX - contextMenu.getMaxWidth() - menuPadding; float offsetX = isMessageOnLeft ? scrubberRight + menuPadding : scrubberX - contextMenu.getMaxWidth() - menuPadding;
contextMenu.show((int) offsetX, (int) Math.min(backgroundView.getY(), getHeight() - contextMenu.getMaxHeight())); contextMenu.show((int) offsetX, (int) Math.min(backgroundView.getY(), overlayHeight - contextMenu.getMaxHeight()));
} else { } else {
float contentX = selectedConversationModel.getBubbleX(); float contentX = selectedConversationModel.getBubbleX();
float offsetX = isMessageOnLeft ? contentX : -contextMenu.getMaxWidth() + contentX + bubbleWidth; float offsetX = isMessageOnLeft ? contentX : -contextMenu.getMaxWidth() + contentX + bubbleWidth;
@ -376,6 +392,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
if (!ThemeUtil.isDarkTheme(getContext())) { if (!ThemeUtil.isDarkTheme(getContext())) {
WindowUtil.clearLightStatusBar(window); WindowUtil.clearLightStatusBar(window);
WindowUtil.clearLightNavigationBar(window);
} }
} }
@ -422,11 +439,49 @@ public final class ConversationReactionOverlay extends RelativeLayout {
itemYAnim.setDuration(duration); itemYAnim.setDuration(duration);
hides.add(itemYAnim); hides.add(itemYAnim);
ObjectAnimator toolbarShadeAnim = new ObjectAnimator();
toolbarShadeAnim.setProperty(View.ALPHA);
toolbarShadeAnim.setFloatValues(0f);
toolbarShadeAnim.setTarget(toolbarShade);
toolbarShadeAnim.setDuration(duration);
hides.add(toolbarShadeAnim);
ObjectAnimator inputShadeAnim = new ObjectAnimator();
inputShadeAnim.setProperty(View.ALPHA);
inputShadeAnim.setFloatValues(0f);
inputShadeAnim.setTarget(inputShade);
inputShadeAnim.setDuration(duration);
hides.add(inputShadeAnim);
if (Build.VERSION.SDK_INT >= 21 && activity != null) {
ValueAnimator statusBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalStatusBarColor);
statusBarAnim.setDuration(duration);
statusBarAnim.addUpdateListener(animation -> {
WindowUtil.setStatusBarColor(activity.getWindow(), (int) animation.getAnimatedValue());
});
hides.add(statusBarAnim);
ValueAnimator navigationBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalNavigationBarColor);
navigationBarAnim.setDuration(duration);
navigationBarAnim.addUpdateListener(animation -> {
WindowUtil.setNavigationBarColor(activity.getWindow(), (int) animation.getAnimatedValue());
});
hides.add(navigationBarAnim);
}
hideAnimatorSet.playTogether(hides); hideAnimatorSet.playTogether(hides);
revealAnimatorSet.end(); revealAnimatorSet.end();
hideAnimatorSet.start(); hideAnimatorSet.start();
if (onHideListener != null) {
onHideListener.startHide();
}
if (selectedConversationModel.getFocusedView() != null) {
ViewUtil.focusAndShowKeyboard(selectedConversationModel.getFocusedView());
}
hideAnimatorSet.addListener(new AnimationCompleteListener() { hideAnimatorSet.addListener(new AnimationCompleteListener() {
@Override public void onAnimationEnd(Animator animation) { @Override public void onAnimationEnd(Animator animation) {
hideAnimatorSet.removeListener(this); hideAnimatorSet.removeListener(this);
@ -435,8 +490,6 @@ public final class ConversationReactionOverlay extends RelativeLayout {
inputShade.setVisibility(INVISIBLE); inputShade.setVisibility(INVISIBLE);
if (Build.VERSION.SDK_INT >= 21 && activity != null) { if (Build.VERSION.SDK_INT >= 21 && activity != null) {
WindowUtil.setStatusBarColor(activity.getWindow(), originalStatusBarColor);
WindowUtil.setNavigationBarColor(activity.getWindow(), originalNavigationBarColor);
activity = null; activity = null;
} }
@ -729,13 +782,21 @@ public final class ConversationReactionOverlay extends RelativeLayout {
} }
private void handleActionItemClicked(@NonNull Action action) { private void handleActionItemClicked(@NonNull Action action) {
hideInternal(() -> { hideInternal(new OnHideListener() {
if (onHideListener != null) { @Override public void startHide() {
onHideListener.onHide(); if (onHideListener != null) {
onHideListener.startHide();
}
} }
if (onActionSelectedListener != null) { @Override public void onHide() {
onActionSelectedListener.onActionSelected(action); if (onHideListener != null) {
onHideListener.onHide();
}
if (onActionSelectedListener != null) {
onActionSelectedListener.onActionSelected(action);
}
} }
}); });
} }
@ -749,7 +810,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
.mapIndexed((idx, v) -> { .mapIndexed((idx, v) -> {
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_reveal); Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_reveal);
anim.setTarget(v); anim.setTarget(v);
anim.setStartDelay(revealOffset + idx * animationEmojiStartDelayFactor); anim.setStartDelay(idx * animationEmojiStartDelayFactor);
return anim; return anim;
}) })
.toList(); .toList();
@ -773,7 +834,6 @@ public final class ConversationReactionOverlay extends RelativeLayout {
.mapIndexed((idx, v) -> { .mapIndexed((idx, v) -> {
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide); Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);
anim.setTarget(v); anim.setTarget(v);
anim.setStartDelay(idx * animationEmojiStartDelayFactor);
return anim; return anim;
}) })
.toList(); .toList();
@ -812,6 +872,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
} }
public interface OnHideListener { public interface OnHideListener {
void startHide();
void onHide(); void onHide();
} }

View file

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.view.View
/** /**
* Contains information on a single selected conversation item. This is used when transitioning * Contains information on a single selected conversation item. This is used when transitioning
@ -16,4 +17,5 @@ data class SelectedConversationModel(
val bubbleWidth: Int, val bubbleWidth: Int,
val audioUri: Uri? = null, val audioUri: Uri? = null,
val isOutgoing: Boolean, val isOutgoing: Boolean,
val focusedView: View?,
) )

View file

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.conversation.mutiselect package org.thoughtcrime.securesms.conversation.mutiselect
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
@ -12,6 +13,7 @@ import android.graphics.Region
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.forEach import androidx.core.view.forEach
@ -54,6 +56,7 @@ class MultiselectItemDecoration(
private val selectedParts: MutableSet<MultiselectPart> = mutableSetOf() private val selectedParts: MutableSet<MultiselectPart> = mutableSetOf()
private var enterExitAnimation: ValueAnimator? = null private var enterExitAnimation: ValueAnimator? = null
private var hideShadeAnimation: ValueAnimator? = null
private val multiselectPartAnimatorMap: MutableMap<MultiselectPart, ValueAnimator> = mutableMapOf() private val multiselectPartAnimatorMap: MutableMap<MultiselectPart, ValueAnimator> = mutableMapOf()
private var checkedBitmap: Bitmap? = null private var checkedBitmap: Bitmap? = null
@ -77,7 +80,10 @@ class MultiselectItemDecoration(
checkedBitmap = null checkedBitmap = null
} }
private val shadeColor = ContextCompat.getColor(context, R.color.reactions_screen_shade_color) private val darkShadeColor = ContextCompat.getColor(context, R.color.reactions_screen_dark_shade_color)
private val lightShadeColor = ContextCompat.getColor(context, R.color.reactions_screen_light_shade_color)
private val argbEvaluator = ArgbEvaluator()
private val unselectedPaint = Paint().apply { private val unselectedPaint = Paint().apply {
isAntiAlias = true isAntiAlias = true
@ -371,7 +377,7 @@ class MultiselectItemDecoration(
} }
canvas.clipPath(path) canvas.clipPath(path)
canvas.drawColor(shadeColor) canvas.drawShade()
canvas.restore() canvas.restore()
} }
} }
@ -389,11 +395,39 @@ class MultiselectItemDecoration(
} }
canvas.clipPath(path, Region.Op.DIFFERENCE) canvas.clipPath(path, Region.Op.DIFFERENCE)
canvas.drawColor(shadeColor) canvas.drawShade()
canvas.restore() canvas.restore()
} }
} }
private fun Canvas.drawShade() {
val progress = hideShadeAnimation?.animatedValue as? Float
if (progress == null) {
drawColor(lightShadeColor)
drawColor(darkShadeColor)
return
}
drawColor(argbEvaluator.evaluate(progress, lightShadeColor, Color.TRANSPARENT) as Int)
drawColor(argbEvaluator.evaluate(progress, darkShadeColor, Color.TRANSPARENT) as Int)
}
fun hideShade(list: RecyclerView) {
hideShadeAnimation = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 150L
addUpdateListener {
invalidateIfAnimatorsAreRunning(list)
}
doOnEnd {
hideShadeAnimation = null
}
start()
}
}
private fun isInitialAnimation(): Boolean { private fun isInitialAnimation(): Boolean {
return (enterExitAnimation?.animatedFraction ?: 0f) < 1f return (enterExitAnimation?.animatedFraction ?: 0f) < 1f
} }
@ -441,7 +475,10 @@ class MultiselectItemDecoration(
} }
private fun invalidateIfAnimatorsAreRunning(parent: RecyclerView) { private fun invalidateIfAnimatorsAreRunning(parent: RecyclerView) {
if (enterExitAnimation?.isRunning == true || multiselectPartAnimatorMap.values.any { it.isRunning }) { if (enterExitAnimation?.isRunning == true ||
multiselectPartAnimatorMap.values.any { it.isRunning } ||
hideShadeAnimation?.isRunning == true
) {
parent.invalidate() parent.invalidate()
} }
} }

View file

@ -341,6 +341,15 @@ public final class ViewUtil {
return result; return result;
} }
public static int getNavigationBarHeight(@NonNull View view) {
int result = 0;
int resourceId = view.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId > 0) {
result = view.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
public static void hideKeyboard(@NonNull Context context, @NonNull View view) { public static void hideKeyboard(@NonNull Context context, @NonNull View view) {
InputMethodManager inputManager = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE); InputMethodManager inputManager = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromWindow(view.getWindowToken(), 0); inputManager.hideSoftInputFromWindow(view.getWindowToken(), 0);

View file

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"> <set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator <objectAnimator
android:duration="@integer/reaction_scrubber_reveal_emoji_duration" android:duration="@integer/reaction_scrubber_hide_duration"
android:interpolator="@android:anim/decelerate_interpolator" android:interpolator="@android:anim/decelerate_interpolator"
android:propertyName="translationY" android:propertyName="translationY"
android:valueTo="@dimen/reaction_scrubber_anim_start_translation_y" android:valueTo="@dimen/reaction_scrubber_anim_start_translation_y"
android:valueFrom="@dimen/reaction_scrubber_anim_end_translation_y" /> android:valueFrom="@dimen/reaction_scrubber_anim_end_translation_y" />
<objectAnimator <objectAnimator
android:duration="@integer/reaction_scrubber_reveal_emoji_duration" android:duration="@integer/reaction_scrubber_hide_duration"
android:interpolator="@android:anim/decelerate_interpolator" android:interpolator="@android:anim/decelerate_interpolator"
android:propertyName="alpha" android:propertyName="alpha"
android:valueTo="0" android:valueTo="0"

View file

@ -261,7 +261,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toTopOf="@+id/status_bar_guideline" app:layout_constraintTop_toTopOf="@+id/status_bar_guideline"
app:layout_constraintBottom_toBottomOf="@+id/navigation_bar_guideline" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/parent_start_guideline" app:layout_constraintStart_toStartOf="@+id/parent_start_guideline"
app:layout_constraintEnd_toEndOf="@+id/parent_end_guideline" app:layout_constraintEnd_toEndOf="@+id/parent_end_guideline"
android:inflatedId="@+id/conversation_reaction_scrubber" android:inflatedId="@+id/conversation_reaction_scrubber"

View file

@ -27,11 +27,12 @@
android:overScrollMode="ifContentScrolls" android:overScrollMode="ifContentScrolls"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<View <FrameLayout
android:id="@+id/reactions_shade" android:id="@+id/reactions_shade"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/reactions_screen_shade_color" android:background="@color/reactions_screen_light_shade_color"
android:foreground="@color/reactions_screen_dark_shade_color"
android:visibility="gone" android:visibility="gone"
app:layout_constrainedHeight="true" app:layout_constrainedHeight="true"
app:layout_constraintTop_toBottomOf="@android:id/list" /> app:layout_constraintTop_toBottomOf="@android:id/list" />

View file

@ -6,8 +6,8 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toTopOf="@+id/status_bar_guideline" app:layout_constraintTop_toTopOf="@+id/status_bar_guideline"
app:layout_constraintBottom_toBottomOf="@+id/navigation_bar_guideline"
app:layout_constraintStart_toStartOf="@+id/parent_start_guideline" app:layout_constraintStart_toStartOf="@+id/parent_start_guideline"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/parent_end_guideline" app:layout_constraintEnd_toEndOf="@+id/parent_end_guideline"
android:elevation="1000dp" android:elevation="1000dp"
android:visibility="gone" android:visibility="gone"
@ -17,22 +17,23 @@
android:id="@+id/dropdown_anchor" android:id="@+id/dropdown_anchor"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_alignParentLeft="true" android:layout_gravity="left"
tools:ignore="RtlHardcoded" /> tools:ignore="RtlHardcoded" />
<View <FrameLayout
android:id="@+id/toolbar_shade" android:id="@+id/toolbar_shade"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?actionBarSize" android:layout_height="?actionBarSize"
android:background="@color/reactions_screen_shade_color" android:background="@color/reactions_screen_light_shade_color"
android:layout_alignParentTop="true" /> android:foreground="@color/reactions_screen_dark_shade_color" />
<View <FrameLayout
android:id="@+id/input_shade" android:id="@+id/input_shade"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="48dp"
android:background="@color/reactions_screen_shade_color" android:layout_gravity="bottom"
android:layout_alignParentBottom="true" /> android:background="@color/reactions_screen_light_shade_color"
android:foreground="@color/reactions_screen_dark_shade_color" />
<View <View
android:id="@+id/conversation_item" android:id="@+id/conversation_item"

View file

@ -58,12 +58,13 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_reactions_fragment_scrubber" /> app:layout_constraintTop_toBottomOf="@+id/edit_reactions_fragment_scrubber" />
<View <FrameLayout
android:id="@+id/edit_reactions_fragment_reaction_mask" android:id="@+id/edit_reactions_fragment_reaction_mask"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:alpha="0" android:alpha="0"
android:background="@color/reactions_screen_shade_color" android:background="@color/reactions_screen_light_shade_color"
android:foreground="@color/reactions_screen_dark_shade_color"
app:elevation="4dp" app:elevation="4dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:background="@color/reactions_screen_shade_color" android:background="@color/reactions_screen_light_shade_color"
android:foreground="@color/reactions_screen_dark_shade_color"
android:fitsSystemWindows="true" /> android:fitsSystemWindows="true" />

View file

@ -126,8 +126,9 @@
<color name="reactions_pill_text_color">@color/core_grey_35</color> <color name="reactions_pill_text_color">@color/core_grey_35</color>
<color name="reactions_pill_selected_text_color">@color/core_grey_15</color> <color name="reactions_pill_selected_text_color">@color/core_grey_15</color>
<color name="reactions_screen_shade_color">@color/transparent_black_60</color> <color name="reactions_screen_dark_shade_color">@color/transparent_black_70</color>
<color name="reactions_status_bar_shade">#070707</color> <color name="reactions_screen_light_shade_color">#df5e5e5e</color>
<color name="reactions_status_bar_shade">#191919</color>
<color name="recipient_contact_button_color">@color/core_grey_80</color> <color name="recipient_contact_button_color">@color/core_grey_80</color>

View file

@ -14,6 +14,7 @@
<color name="transparent_black_40">#66000000</color> <color name="transparent_black_40">#66000000</color>
<color name="transparent_black_50">#80000000</color> <color name="transparent_black_50">#80000000</color>
<color name="transparent_black_60">#99000000</color> <color name="transparent_black_60">#99000000</color>
<color name="transparent_black_70">#b3000000</color>
<color name="transparent_black_80">#CC000000</color> <color name="transparent_black_80">#CC000000</color>
<color name="transparent_black_90">#e6000000</color> <color name="transparent_black_90">#e6000000</color>
@ -26,12 +27,13 @@
<color name="transparent_white_40">#66ffffff</color> <color name="transparent_white_40">#66ffffff</color>
<color name="transparent_white_60">#99ffffff</color> <color name="transparent_white_60">#99ffffff</color>
<color name="transparent_white_80">#ccffffff</color> <color name="transparent_white_80">#ccffffff</color>
<color name="transparent_white_87">#dfffffff</color>
<color name="transparent_white_90">#e6ffffff</color> <color name="transparent_white_90">#e6ffffff</color>
<color name="transparent_white_95">#f3ffffff</color> <color name="transparent_white_95">#f3ffffff</color>
<color name="conversation_compose_divider">#32000000</color> <color name="conversation_compose_divider">#32000000</color>
<color name="conversation_item_selected_system_ui">@color/core_grey_60</color> <color name="conversation_item_selected_system_ui">#4d4d4d</color>
<color name="touch_highlight">#400099cc</color> <color name="touch_highlight">#400099cc</color>
<color name="device_link_item_background_light">#ffffffff</color> <color name="device_link_item_background_light">#ffffffff</color>

View file

@ -3,6 +3,5 @@
<integer name="reaction_scrubber_reveal_duration">200</integer> <integer name="reaction_scrubber_reveal_duration">200</integer>
<integer name="reaction_scrubber_reveal_offset">100</integer> <integer name="reaction_scrubber_reveal_offset">100</integer>
<integer name="reaction_scrubber_hide_duration">150</integer> <integer name="reaction_scrubber_hide_duration">150</integer>
<integer name="reaction_scrubber_reveal_emoji_duration">380</integer>
<integer name="reaction_scrubber_emoji_reveal_duration_start_delay_factor">10</integer> <integer name="reaction_scrubber_emoji_reveal_duration_start_delay_factor">10</integer>
</resources> </resources>

View file

@ -126,8 +126,9 @@
<color name="reactions_pill_text_color">@color/core_grey_60</color> <color name="reactions_pill_text_color">@color/core_grey_60</color>
<color name="reactions_pill_selected_text_color">@color/core_grey_75</color> <color name="reactions_pill_selected_text_color">@color/core_grey_75</color>
<color name="reactions_screen_shade_color">@color/transparent_black_50</color> <color name="reactions_screen_dark_shade_color">@color/transparent_black_70</color>
<color name="reactions_status_bar_shade">#808080</color> <color name="reactions_screen_light_shade_color">@color/transparent_white_87</color>
<color name="reactions_status_bar_shade">#4d4d4d</color>
<color name="recipient_contact_button_color">@color/core_grey_02</color> <color name="recipient_contact_button_color">@color/core_grey_02</color>