From fe8fcb1394df8b0c8beba7c1472cc3d076ad52ef Mon Sep 17 00:00:00 2001 From: Lucio Maciel Date: Thu, 12 Aug 2021 14:25:52 -0300 Subject: [PATCH] Implement single line timestamps on conversation items. --- .../components/emoji/EmojiTextView.java | 67 +++++++++++++++---- .../conversation/ConversationItem.java | 65 +++++++++++++++--- .../thoughtcrime/securesms/util/ViewUtil.java | 4 ++ .../conversation_item_received_multimedia.xml | 7 +- .../conversation_item_received_text_only.xml | 4 +- .../conversation_item_sent_multimedia.xml | 17 +++-- .../conversation_item_sent_text_only.xml | 3 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/dimens.xml | 2 + 9 files changed, 134 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java index 568ac8961..7990127c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java @@ -1,12 +1,16 @@ package org.thoughtcrime.securesms.components.emoji; +import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.text.Annotation; +import android.text.Layout; import android.text.SpannableStringBuilder; import android.text.Spanned; +import android.text.TextDirectionHeuristic; +import android.text.TextDirectionHeuristics; import android.text.TextUtils; import android.util.AttributeSet; import android.util.TypedValue; @@ -19,6 +23,7 @@ import androidx.appcompat.widget.AppCompatTextView; import androidx.core.content.ContextCompat; import androidx.core.widget.TextViewCompat; +import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser; import org.thoughtcrime.securesms.components.mention.MentionAnnotation; @@ -36,16 +41,19 @@ public class EmojiTextView extends AppCompatTextView { private static final char ELLIPSIS = '…'; - private boolean forceCustom; - private CharSequence previousText; - private BufferType previousBufferType; - private float originalFontSize; - private boolean useSystemEmoji; - private boolean sizeChangeInProgress; - private int maxLength; - private CharSequence overflowText; - private CharSequence previousOverflowText; - private boolean renderMentions; + private boolean forceCustom; + private CharSequence previousText; + private BufferType previousBufferType; + private float originalFontSize; + private boolean useSystemEmoji; + private boolean sizeChangeInProgress; + private int maxLength; + private CharSequence overflowText; + private CharSequence previousOverflowText; + private boolean renderMentions; + private boolean measureLastLine; + private int lastLineWidth = -1; + private TextDirectionHeuristic textDirection; private MentionRendererDelegate mentionRendererDelegate; @@ -61,10 +69,11 @@ public class EmojiTextView extends AppCompatTextView { super(context, attrs, defStyleAttr); TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0); - scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false); - maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1); - forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false); - renderMentions = a.getBoolean(R.styleable.EmojiTextView_emoji_renderMentions, true); + scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false); + maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1); + forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false); + renderMentions = a.getBoolean(R.styleable.EmojiTextView_emoji_renderMentions, true); + measureLastLine = a.getBoolean(R.styleable.EmojiTextView_measureLastLine, false); a.recycle(); a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize}); @@ -74,6 +83,8 @@ public class EmojiTextView extends AppCompatTextView { if (renderMentions) { mentionRendererDelegate = new MentionRendererDelegate(getContext(), ContextCompat.getColor(getContext(), R.color.transparent_black_20)); } + + textDirection = getLayoutDirection() == LAYOUT_DIRECTION_LTR ? TextDirectionHeuristics.FIRSTSTRONG_RTL : TextDirectionHeuristics.ANYRTL_LTR; } @Override @@ -139,6 +150,34 @@ public class EmojiTextView extends AppCompatTextView { } } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + CharSequence text = getText(); + if (!measureLastLine || text == null || text.length() == 0) { + lastLineWidth = -1; + } else { + Layout layout = getLayout(); + int lines = layout.getLineCount(); + int start = layout.getLineStart(lines - 1); + int count = text.length() - start; + + if ((getLayoutDirection() == LAYOUT_DIRECTION_LTR && textDirection.isRtl(text, start, count)) || + (getLayoutDirection() == LAYOUT_DIRECTION_RTL && !textDirection.isRtl(text, start, count))) { + lastLineWidth = getMeasuredWidth(); + } else { + lastLineWidth = (int) getPaint().measureText(text, start, text.length()); + } + } + } + + public int getLastLineWidth() { + return lastLineWidth; + } + + public boolean isSingleLine() { + return getLayout().getLineCount() == 1; + } + public void setOverflowText(@Nullable CharSequence overflowText) { this.overflowText = overflowText; setText(previousText, BufferType.SPANNABLE); 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 08451ccca..9b9eda095 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -166,13 +166,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo private static final Rect SWIPE_RECT = new Rect(); - private ConversationMessage conversationMessage; - private MessageRecord messageRecord; - private Locale locale; - private boolean groupThread; - private LiveRecipient recipient; - private GlideRequests glideRequests; - private ValueAnimator pulseOutlinerAlphaAnimator; + private ConversationMessage conversationMessage; + private MessageRecord messageRecord; + private Optional nextMessageRecord; + private Locale locale; + private boolean groupThread; + private LiveRecipient recipient; + private GlideRequests glideRequests; + private ValueAnimator pulseOutlinerAlphaAnimator; protected ConversationItemBodyBubble bodyBubble; protected View reply; @@ -202,9 +203,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo private Stub revealableStub; private @Nullable EventListener eventListener; - private int defaultBubbleColor; - private int defaultBubbleColorForWallpaper; - private int measureCalls; + private int defaultBubbleColor; + private int defaultBubbleColorForWallpaper; + private int measureCalls; + private boolean updatingFooter; private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener(); private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener(); @@ -300,6 +302,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo this.conversationMessage = conversationMessage; this.messageRecord = conversationMessage.getMessageRecord(); + this.nextMessageRecord = nextMessageRecord; this.locale = locale; this.glideRequests = glideRequests; this.batchSelected = batchSelected; @@ -386,6 +389,35 @@ public final class ConversationItem extends RelativeLayout implements BindableCo } } + if (!updatingFooter && !isCaptionlessMms(messageRecord) && !isViewOnceMessage(messageRecord) && isFooterVisible(messageRecord, nextMessageRecord, groupThread)) { + int footerWidth = footer.getMeasuredWidth(); + int availableWidth = getAvailableMessageBubbleWidth(bodyText); + if (bodyText.isSingleLine()) { + int maxBubbleWidth = hasBigImageLinkPreview(messageRecord) || hasThumbnail(messageRecord) ? readDimen(R.dimen.media_bubble_max_width) : getMaxBubbleWidth(); + int bodyMargins = ViewUtil.getLeftMargin(bodyText) + ViewUtil.getRightMargin(bodyText); + int sizeWithMargins = bodyText.getMeasuredWidth() + ViewUtil.dpToPx(6) + footerWidth + bodyMargins; + int minSize = Math.min(maxBubbleWidth, Math.max(bodyText.getMeasuredWidth() + ViewUtil.dpToPx(6) + footerWidth + bodyMargins, bodyBubble.getMeasuredWidth())); + if (hasQuote(messageRecord) && sizeWithMargins < availableWidth) { + ViewUtil.setTopMargin(footer, readDimen(R.dimen.message_bubble_same_line_footer_bottom_margin)); + needsMeasure = true; + updatingFooter = true; + } else if (sizeWithMargins != bodyText.getMeasuredWidth() && sizeWithMargins <= minSize) { + bodyBubble.getLayoutParams().width = minSize; + ViewUtil.setTopMargin(footer, readDimen(R.dimen.message_bubble_same_line_footer_bottom_margin)); + needsMeasure = true; + updatingFooter = true; + } + } + if (!updatingFooter && bodyText.getLastLineWidth() + ViewUtil.dpToPx(6) + footerWidth <= bodyText.getMeasuredWidth()) { + ViewUtil.setTopMargin(footer, readDimen(R.dimen.message_bubble_same_line_footer_bottom_margin)); + updatingFooter = true; + needsMeasure = true; + } else if (!updatingFooter && ViewUtil.getTopMargin(footer) == readDimen(R.dimen.message_bubble_same_line_footer_bottom_margin)) { + ViewUtil.setTopMargin(footer, readDimen(R.dimen.message_bubble_default_footer_bottom_margin)); + needsMeasure = true; + } + } + if (hasSharedContact(messageRecord)) { int contactWidth = sharedContactStub.get().getMeasuredWidth(); int availableWidth = getAvailableMessageBubbleWidth(sharedContactStub.get()); @@ -396,7 +428,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo } } - if (!hasNoBubble(messageRecord)) { + if (hasAudio(messageRecord)) { ConversationItemFooter activeFooter = getActiveFooter(messageRecord); int availableWidth = getAvailableMessageBubbleWidth(footer); @@ -415,6 +447,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo } } else { measureCalls = 0; + updatingFooter = false; } } @@ -449,6 +482,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo return availableWidth; } + private int getMaxBubbleWidth() { + int paddings = getPaddingLeft() + getPaddingRight() + ViewUtil.getLeftMargin(bodyBubble) + ViewUtil.getRightMargin(bodyBubble); + if (groupThread && !messageRecord.isOutgoing()) { + paddings += contactPhoto.getLayoutParams().width + ViewUtil.getLeftMargin(contactPhoto) + ViewUtil.getRightMargin(contactPhoto); + } + return getMeasuredWidth() - paddings; + } + private void initializeAttributes() { defaultBubbleColor = ContextCompat.getColor(context, R.color.signal_background_secondary); defaultBubbleColorForWallpaper = ContextCompat.getColor(context, R.color.conversation_item_wallpaper_bubble_color); @@ -532,6 +573,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo private void setBubbleState(MessageRecord messageRecord, @NonNull Recipient recipient, boolean hasWallpaper, @NonNull Colorizer colorizer) { this.hasWallpaper = hasWallpaper; + ViewUtil.updateLayoutParams(bodyBubble, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); bodyText.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_text_primary)); bodyText.setLinkTextColor(ContextCompat.getColor(getContext(), R.color.signal_text_primary)); @@ -1312,6 +1354,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo private void setFooter(@NonNull MessageRecord current, @NonNull Optional next, @NonNull Locale locale, boolean isGroupThread, boolean hasWallpaper) { ViewUtil.updateLayoutParams(footer, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + ViewUtil.setTopMargin(footer, readDimen(R.dimen.message_bubble_default_footer_bottom_margin)); footer.setVisibility(GONE); stickerFooter.setVisibility(GONE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java index 14727bef8..c9afd6e37 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java @@ -243,6 +243,10 @@ public final class ViewUtil { return ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).leftMargin; } + public static int getTopMargin(@NonNull View view) { + return ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).topMargin; + } + public static void setLeftMargin(@NonNull View view, int margin) { if (isLtr(view)) { ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).leftMargin = margin; diff --git a/app/src/main/res/layout/conversation_item_received_multimedia.xml b/app/src/main/res/layout/conversation_item_received_multimedia.xml index b636d6d65..cfc11d0bc 100644 --- a/app/src/main/res/layout/conversation_item_received_multimedia.xml +++ b/app/src/main/res/layout/conversation_item_received_multimedia.xml @@ -177,11 +177,12 @@ android:textColorLink="@color/signal_text_primary" app:emoji_maxLength="1000" app:scaleEmojis="true" + app:measureLastLine="true" tools:text="Mango pickle lorem ipsum" /> diff --git a/app/src/main/res/layout/conversation_item_sent_text_only.xml b/app/src/main/res/layout/conversation_item_sent_text_only.xml index 9457fa27f..60a2c9070 100644 --- a/app/src/main/res/layout/conversation_item_sent_text_only.xml +++ b/app/src/main/res/layout/conversation_item_sent_text_only.xml @@ -60,11 +60,12 @@ android:textColorLink="@color/conversation_item_sent_text_primary_color" app:emoji_maxLength="1000" app:scaleEmojis="true" + app:measureLastLine="true" tools:text="Mango pickle lorem ipsum" /> + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index b1ad26268..55d7a7eb5 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -54,6 +54,8 @@ 175dp 240dp 200dp + -4dp + -24dp 175dp 85dp