Add support for jumbo emoji.

This commit is contained in:
Cody Henthorne
2022-01-06 10:24:08 -05:00
committed by Alex Hart
parent 449acaf9df
commit 34f679b10b
20 changed files with 401 additions and 163 deletions

View File

@@ -50,7 +50,7 @@ public class SearchView extends androidx.appcompat.widget.SearchView {
result = new InputFilter[1];
}
result[0] = new EmojiFilter(view);
result[0] = new EmojiFilter(view, false);
return result;
}

View File

@@ -33,10 +33,11 @@ public class EmojiEditText extends AppCompatEditText {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
boolean forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
boolean jumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false);
a.recycle();
if (!isInEditMode() && (forceCustom || !SignalStore.settings().isPreferSystemEmoji())) {
setFilters(appendEmojiFilter(this.getFilters()));
setFilters(appendEmojiFilter(this.getFilters(), jumboEmoji));
}
}
@@ -54,7 +55,7 @@ public class EmojiEditText extends AppCompatEditText {
else super.invalidateDrawable(drawable);
}
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters) {
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters, boolean jumboEmoji) {
InputFilter[] result;
if (originalFilters != null) {
@@ -64,7 +65,7 @@ public class EmojiEditText extends AppCompatEditText {
result = new InputFilter[1];
}
result[0] = new EmojiFilter(this);
result[0] = new EmojiFilter(this, jumboEmoji);
return result;
}

View File

@@ -8,9 +8,11 @@ import android.widget.TextView;
public class EmojiFilter implements InputFilter {
private TextView view;
private boolean jumboEmoji;
public EmojiFilter(TextView view) {
this.view = view;
public EmojiFilter(TextView view, boolean jumboEmoji) {
this.view = view;
this.jumboEmoji = jumboEmoji;
}
@Override
@@ -19,7 +21,7 @@ public class EmojiFilter implements InputFilter {
char[] v = new char[end - start];
TextUtils.getChars(source, start, end, v, 0);
Spannable emojified = EmojiProvider.emojify(new String(v), view);
Spannable emojified = EmojiProvider.emojify(new String(v), view, jumboEmoji);
if (source instanceof Spanned && emojified != null) {
TextUtils.copySpansFrom((Spanned) source, start, end, null, emojified, 0);

View File

@@ -19,15 +19,17 @@ import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.emoji.EmojiFiles;
import org.thoughtcrime.securesms.emoji.EmojiPageCache;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.util.DeviceProperties;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
public class EmojiProvider {
@@ -39,23 +41,24 @@ public class EmojiProvider {
return new EmojiParser(EmojiSource.getLatest().getEmojiTree()).findCandidates(text);
}
static @Nullable Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv) {
static @Nullable Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv, boolean jumboEmoji) {
if (tv.isInEditMode()) {
return null;
} else {
return emojify(getCandidates(text), text, tv);
return emojify(getCandidates(text), text, tv, jumboEmoji);
}
}
static @Nullable Spannable emojify(@Nullable EmojiParser.CandidateList matches,
@Nullable CharSequence text,
@NonNull TextView tv)
@NonNull TextView tv,
boolean jumboEmoji)
{
if (matches == null || text == null || tv.isInEditMode()) return null;
SpannableStringBuilder builder = new SpannableStringBuilder(text);
for (EmojiParser.Candidate candidate : matches) {
Drawable drawable = getEmojiDrawable(tv.getContext(), candidate.getDrawInfo(), tv::requestLayout);
Drawable drawable = getEmojiDrawable(tv.getContext(), candidate.getDrawInfo(), tv::requestLayout, jumboEmoji);
if (drawable != null) {
builder.setSpan(new EmojiSpan(drawable, tv), candidate.getStartIndex(), candidate.getEndIndex(),
@@ -70,7 +73,8 @@ public class EmojiProvider {
@Nullable EmojiParser.CandidateList matches,
@Nullable CharSequence text,
@NonNull Paint paint,
boolean synchronous)
boolean synchronous,
boolean jumboEmoji)
{
if (matches == null || text == null) return null;
SpannableStringBuilder builder = new SpannableStringBuilder(text);
@@ -78,9 +82,9 @@ public class EmojiProvider {
for (EmojiParser.Candidate candidate : matches) {
Drawable drawable;
if (synchronous) {
drawable = getEmojiDrawableSync(context, candidate.getDrawInfo());
drawable = getEmojiDrawableSync(context, candidate.getDrawInfo(), jumboEmoji);
} else {
drawable = getEmojiDrawable(context, candidate.getDrawInfo(), null);
drawable = getEmojiDrawable(context, candidate.getDrawInfo(), null, jumboEmoji);
}
if (drawable != null) {
@@ -93,8 +97,12 @@ public class EmojiProvider {
}
static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable CharSequence emoji) {
return getEmojiDrawable(context, emoji, false);
}
static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable CharSequence emoji, boolean jumboEmoji) {
EmojiDrawInfo drawInfo = EmojiSource.getLatest().getEmojiTree().getEmoji(emoji, 0, emoji.length());
return getEmojiDrawable(context, drawInfo, null);
return getEmojiDrawable(context, drawInfo, null, jumboEmoji);
}
/**
@@ -104,7 +112,7 @@ public class EmojiProvider {
* @param drawInfo Information about the emoji being displayed
* @param onEmojiLoaded Runnable which will trigger when an emoji is loaded from disk
*/
private static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo, @Nullable Runnable onEmojiLoaded) {
private static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo, @Nullable Runnable onEmojiLoaded, boolean jumboEmoji) {
if (drawInfo == null) {
return null;
}
@@ -112,6 +120,7 @@ public class EmojiProvider {
final int lowMemoryDecodeScale = DeviceProperties.isLowMemoryDevice(context) ? 2 : 1;
final EmojiSource source = EmojiSource.getLatest();
final EmojiDrawable drawable = new EmojiDrawable(source, drawInfo, lowMemoryDecodeScale);
final AtomicBoolean jumboLoaded = new AtomicBoolean(false);
EmojiPageCache.LoadResult loadResult = EmojiPageCache.INSTANCE.load(context, drawInfo.getPage(), lowMemoryDecodeScale);
@@ -122,9 +131,11 @@ public class EmojiProvider {
@Override
public void onSuccess(Bitmap result) {
ThreadUtil.runOnMain(() -> {
drawable.setBitmap(result);
if (onEmojiLoaded != null) {
onEmojiLoaded.run();
if (!jumboLoaded.get()) {
drawable.setBitmap(result);
if (onEmojiLoaded != null) {
onEmojiLoaded.run();
}
}
});
}
@@ -138,6 +149,36 @@ public class EmojiProvider {
throw new IllegalStateException("Unexpected subclass " + loadResult.getClass());
}
if (jumboEmoji) {
JumboEmoji.LoadResult result = JumboEmoji.loadJumboEmoji(context, drawInfo.getRawEmoji());
if (result instanceof JumboEmoji.LoadResult.Immediate) {
ThreadUtil.runOnMain(() -> {
jumboLoaded.set(true);
drawable.setSingleBitmap(((JumboEmoji.LoadResult.Immediate) result).getBitmap());
});
} else if (result instanceof JumboEmoji.LoadResult.Async) {
((JumboEmoji.LoadResult.Async) result).getTask().addListener(new FutureTaskListener<Bitmap>() {
@Override
public void onSuccess(Bitmap result) {
ThreadUtil.runOnMain(() -> {
jumboLoaded.set(true);
drawable.setSingleBitmap(result);
if (onEmojiLoaded != null) {
onEmojiLoaded.run();
}
});
}
@Override
public void onFailure(ExecutionException exception) {
Log.d(TAG, "Failed to load jumbo emoji bitmap resource", exception);
}
});
}
return drawable;
}
return drawable;
}
@@ -147,7 +188,7 @@ public class EmojiProvider {
* @param context Context object used in reading and writing from disk
* @param drawInfo Information about the emoji being displayed
*/
private static @Nullable Drawable getEmojiDrawableSync(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo) {
private static @Nullable Drawable getEmojiDrawableSync(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo, boolean jumboEmoji) {
ThreadUtil.assertNotMainThread();
if (drawInfo == null) {
return null;
@@ -157,24 +198,45 @@ public class EmojiProvider {
final EmojiSource source = EmojiSource.getLatest();
final EmojiDrawable drawable = new EmojiDrawable(source, drawInfo, lowMemoryDecodeScale);
EmojiPageCache.LoadResult loadResult = EmojiPageCache.INSTANCE.load(context, drawInfo.getPage(), lowMemoryDecodeScale);
Bitmap bitmap = null;
Bitmap bitmap = null;
if (loadResult instanceof EmojiPageCache.LoadResult.Immediate) {
Log.d(TAG, "Cached emoji page: " + drawInfo.getPage().getUri().toString());
bitmap = ((EmojiPageCache.LoadResult.Immediate) loadResult).getBitmap();
} else if (loadResult instanceof EmojiPageCache.LoadResult.Async) {
Log.d(TAG, "Loading emoji page: " + drawInfo.getPage().getUri().toString());
try {
bitmap = ((EmojiPageCache.LoadResult.Async) loadResult).getTask().get(2, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException exception) {
Log.d(TAG, "Failed to load emoji bitmap resource", exception);
if (jumboEmoji) {
JumboEmoji.LoadResult result = JumboEmoji.loadJumboEmoji(context, drawInfo.getRawEmoji());
if (result instanceof JumboEmoji.LoadResult.Immediate) {
bitmap = ((JumboEmoji.LoadResult.Immediate) result).getBitmap();
} else if (result instanceof JumboEmoji.LoadResult.Async) {
try {
bitmap = ((JumboEmoji.LoadResult.Async) result).getTask().get(10, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException exception) {
Log.d(TAG, "Failed to load jumbo emoji bitmap resource", exception);
}
}
if (bitmap != null) {
drawable.setSingleBitmap(bitmap);
}
} else {
throw new IllegalStateException("Unexpected subclass " + loadResult.getClass());
}
drawable.setBitmap(bitmap);
if (!jumboEmoji || bitmap == null) {
EmojiPageCache.LoadResult loadResult = EmojiPageCache.INSTANCE.load(context, drawInfo.getPage(), lowMemoryDecodeScale);
if (loadResult instanceof EmojiPageCache.LoadResult.Immediate) {
Log.d(TAG, "Cached emoji page: " + drawInfo.getPage().getUri().toString());
bitmap = ((EmojiPageCache.LoadResult.Immediate) loadResult).getBitmap();
} else if (loadResult instanceof EmojiPageCache.LoadResult.Async) {
Log.d(TAG, "Loading emoji page: " + drawInfo.getPage().getUri().toString());
try {
bitmap = ((EmojiPageCache.LoadResult.Async) loadResult).getTask().get(2, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException exception) {
Log.d(TAG, "Failed to load emoji bitmap resource", exception);
}
} else {
throw new IllegalStateException("Unexpected subclass " + loadResult.getClass());
}
drawable.setBitmap(bitmap);
}
return drawable;
}
@@ -183,7 +245,8 @@ public class EmojiProvider {
private final float intrinsicHeight;
private final Rect emojiBounds;
private Bitmap bmp;
private Bitmap bmp;
private boolean isSingleBitmap;
@Override
public int getIntrinsicWidth() {
@@ -219,12 +282,21 @@ public class EmojiProvider {
}
canvas.drawBitmap(bmp,
emojiBounds,
isSingleBitmap ? null : emojiBounds,
getBounds(),
PAINT);
}
public void setBitmap(Bitmap bitmap) {
setBitmap(bitmap, false);
}
public void setSingleBitmap(Bitmap bitmap) {
setBitmap(bitmap, true);
}
private void setBitmap(Bitmap bitmap, boolean isSingleBitmap) {
this.isSingleBitmap = isSingleBitmap;
if (bmp == null || !bmp.sameAs(bitmap)) {
bmp = bitmap;
invalidateSelf();

View File

@@ -57,6 +57,7 @@ public class EmojiTextView extends AppCompatTextView {
private int lastLineWidth = -1;
private TextDirectionHeuristic textDirection;
private boolean isJumbomoji;
private boolean forceJumboEmoji;
private MentionRendererDelegate mentionRendererDelegate;
@@ -77,6 +78,7 @@ public class EmojiTextView extends AppCompatTextView {
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);
forceJumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false);
a.recycle();
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize});
@@ -112,10 +114,10 @@ public class EmojiTextView extends AppCompatTextView {
int emojis = candidates.size();
float scale = 1.0f;
if (emojis <= 8) scale += 0.25f;
if (emojis <= 6) scale += 0.25f;
if (emojis <= 4) scale += 0.25f;
if (emojis <= 2) scale += 0.25f;
if (emojis <= 8) scale += 0.75f;
if (emojis <= 6) scale += 0.75f;
if (emojis <= 4) scale += 0.75f;
if (emojis <= 2) scale += 0.75f;
isJumbomoji = scale > 1.0f;
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalFontSize * scale);
@@ -137,7 +139,7 @@ public class EmojiTextView extends AppCompatTextView {
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")), BufferType.SPANNABLE);
} else {
CharSequence emojified = EmojiProvider.emojify(candidates, text, this);
CharSequence emojified = EmojiProvider.emojify(candidates, text, this, isJumbomoji || forceJumboEmoji);
super.setText(new SpannableStringBuilder(emojified), BufferType.SPANNABLE);
}
@@ -261,7 +263,7 @@ public class EmojiTextView extends AppCompatTextView {
if (useSystemEmoji || newCandidates == null || newCandidates.size() == 0) {
super.setText(newContent, BufferType.SPANNABLE);
} else {
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this);
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this, isJumbomoji || forceJumboEmoji);
super.setText(emojified, BufferType.SPANNABLE);
}
}
@@ -292,7 +294,7 @@ public class EmojiTextView extends AppCompatTextView {
.append(Optional.fromNullable(overflowText).or(""));
EmojiParser.CandidateList newCandidates = isInEditMode() ? null : EmojiProvider.getCandidates(newContent);
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this);
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this, isJumbomoji || forceJumboEmoji);
super.setText(emojified, BufferType.SPANNABLE);
}

View File

@@ -31,7 +31,7 @@ open class SimpleEmojiTextView @JvmOverloads constructor(
val newText = if (newCandidates == null || newCandidates.size() == 0) {
text
} else {
EmojiProvider.emojify(newCandidates, text, this)
EmojiProvider.emojify(newCandidates, text, this, false)
}
val newContent = if (width == 0 || maxLines == -1) {

View File

@@ -1,33 +0,0 @@
package org.thoughtcrime.securesms.components.emoji.parsing;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.emoji.EmojiPage;
public class EmojiDrawInfo {
private final EmojiPage page;
private final int index;
public EmojiDrawInfo(final @NonNull EmojiPage page, final int index) {
this.page = page;
this.index = index;
}
public @NonNull EmojiPage getPage() {
return page;
}
public int getIndex() {
return index;
}
@Override
public @NonNull String toString() {
return "DrawInfo{" +
"page=" + page +
", index=" + index +
'}';
}
}

View File

@@ -0,0 +1,13 @@
package org.thoughtcrime.securesms.components.emoji.parsing
import org.thoughtcrime.securesms.emoji.EmojiPage
import org.thoughtcrime.securesms.util.Hex
import java.nio.charset.Charset
data class EmojiDrawInfo(val page: EmojiPage, val index: Int, private val emoji: String) {
val rawEmoji: String
get() {
val emojiBytes: ByteArray = emoji.toByteArray(Charset.forName("UTF-16"))
return Hex.toStringCondensed(emojiBytes.slice(2 until emojiBytes.size).toByteArray())
}
}