mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
Add support for jumbo emoji.
This commit is contained in:
committed by
Alex Hart
parent
449acaf9df
commit
34f679b10b
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user