Render emoji's properly in quote bubbles.

Unfortunately, the change wasn't as simple as just switching to use our
EmojiTextView. That view only supported single-line text. I added
support for multi-line text.

Fixes #7704.
This commit is contained in:
Greyson Parrelli 2018-04-20 13:53:23 -07:00 committed by Moxie Marlinspike
parent cbe394025d
commit 6fbbc9d078
3 changed files with 32 additions and 44 deletions

View File

@ -61,7 +61,7 @@
tools:text="Photo" tools:text="Photo"
tools:visibility="visible" /> tools:visibility="visible" />
<TextView <org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/quote_text" android:id="@+id/quote_text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -2,27 +2,27 @@ package org.thoughtcrime.securesms.components.emoji;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.widget.TextViewCompat;
import android.support.v7.widget.AppCompatTextView; import android.support.v7.widget.AppCompatTextView;
import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.ViewTreeObserver;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable; import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser; import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
public class EmojiTextView extends AppCompatTextView { public class EmojiTextView extends AppCompatTextView {
private final boolean scaleEmojis; private final boolean scaleEmojis;
private CharSequence source; private CharSequence source;
private boolean needsEllipsizing;
private float originalFontSize; private float originalFontSize;
public EmojiTextView(Context context) { public EmojiTextView(Context context) {
@ -67,43 +67,45 @@ public class EmojiTextView extends AppCompatTextView {
} }
source = EmojiProvider.getInstance(getContext()).emojify(candidates, text, this); source = EmojiProvider.getInstance(getContext()).emojify(candidates, text, this);
setTextEllipsized(source); super.setText(source, BufferType.SPANNABLE);
// Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688)
// We ellipsize them ourselves by manually truncating the appropriate section.
if (getEllipsize() == TextUtils.TruncateAt.END) {
post(() -> {
int maxLines = TextViewCompat.getMaxLines(EmojiTextView.this);
if (maxLines <= 0) {
return;
}
int lineCount = getLineCount();
if (lineCount > maxLines) {
int overflowStart = getLayout().getLineStart(maxLines - 1);
CharSequence overflow = getText().subSequence(overflowStart, getText().length());
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth(), TextUtils.TruncateAt.END);
SpannableStringBuilder newContent = new SpannableStringBuilder();
newContent.append(getText().subSequence(0, overflowStart))
.append(ellipsized);
super.setText(newContent);
}
});
}
} }
private boolean useSystemEmoji() { private boolean useSystemEmoji() {
return TextSecurePreferences.isSystemEmojiPreferred(getContext()); return TextSecurePreferences.isSystemEmojiPreferred(getContext());
} }
private void setTextEllipsized(final @Nullable CharSequence source) {
super.setText(needsEllipsizing ? ViewUtil.ellipsize(source, this) : source, BufferType.SPANNABLE);
}
@Override public void invalidateDrawable(@NonNull Drawable drawable) { @Override public void invalidateDrawable(@NonNull Drawable drawable) {
if (drawable instanceof EmojiDrawable) invalidate(); if (drawable instanceof EmojiDrawable) invalidate();
else super.invalidateDrawable(drawable); else super.invalidateDrawable(drawable);
} }
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int size = MeasureSpec.getSize(widthMeasureSpec);
final int mode = MeasureSpec.getMode(widthMeasureSpec);
if (!useSystemEmoji() &&
getEllipsize() == TruncateAt.END &&
!TextUtils.isEmpty(source) &&
(mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) &&
getPaint().breakText(source, 0, source.length()-1, true, size, null) != source.length())
{
needsEllipsizing = true;
FontMetricsInt font = getPaint().getFontMetricsInt();
super.onMeasure(MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(Math.abs(font.top - font.bottom), MeasureSpec.EXACTLY));
} else {
needsEllipsizing = false;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (changed && !useSystemEmoji()) setTextEllipsized(source); if (changed && !useSystemEmoji()) {
super.setText(source, BufferType.SPANNABLE);
}
super.onLayout(changed, left, top, right, bottom); super.onLayout(changed, left, top, right, bottom);
} }

View File

@ -28,9 +28,6 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.util.DisplayMetrics;
import android.view.Gravity; import android.view.Gravity;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -97,17 +94,6 @@ public class ViewUtil {
parent.addView(toAdd, childIndex > -1 ? childIndex : defaultIndex); parent.addView(toAdd, childIndex > -1 ? childIndex : defaultIndex);
} }
public static CharSequence ellipsize(@Nullable CharSequence text, @NonNull TextView view) {
if (TextUtils.isEmpty(text) || view.getWidth() == 0 || view.getEllipsize() != TruncateAt.END) {
return text;
} else {
return TextUtils.ellipsize(text,
view.getPaint(),
view.getWidth() - view.getPaddingRight() - view.getPaddingLeft(),
TruncateAt.END);
}
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static <T extends View> T inflateStub(@NonNull View parent, @IdRes int stubId) { public static <T extends View> T inflateStub(@NonNull View parent, @IdRes int stubId) {
return (T)((ViewStub)parent.findViewById(stubId)).inflate(); return (T)((ViewStub)parent.findViewById(stubId)).inflate();