Updated emoji set

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2016-12-10 16:53:10 -08:00
parent 1b44bdcd3c
commit f7474362ff
53 changed files with 625 additions and 298 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 922 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 KiB

BIN
assets/emoji/Activity.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

BIN
assets/emoji/Flags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 KiB

BIN
assets/emoji/Foods.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

BIN
assets/emoji/Nature.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 KiB

BIN
assets/emoji/Objects.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 KiB

BIN
assets/emoji/People.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

BIN
assets/emoji/Places.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

BIN
assets/emoji/Symbols.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:enterFadeDuration="200"
android:exitFadeDuration="300">
<item android:state_selected="true">
<bitmap android:src="@drawable/ic_directions_bike_white_24dp"
android:tint="@color/grey_400"/>
</item>
<item>
<bitmap android:src="@drawable/ic_directions_bike_white_24dp"
android:tint="@color/grey_700"/>
</item>
</selector>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:enterFadeDuration="200"
android:exitFadeDuration="300">
<item android:state_selected="true">
<bitmap android:src="@drawable/ic_directions_bike_white_24dp"
android:tint="@color/grey_800"/>
</item>
<item>
<bitmap android:src="@drawable/ic_directions_bike_white_24dp"
android:tint="@color/grey_400"/>
</item>
</selector>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:enterFadeDuration="200"
android:exitFadeDuration="300">
<item android:state_selected="true">
<bitmap android:src="@drawable/ic_flag_white_24dp"
android:tint="@color/grey_400"/>
</item>
<item>
<bitmap android:src="@drawable/ic_flag_white_24dp"
android:tint="@color/grey_700"/>
</item>
</selector>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:enterFadeDuration="200"
android:exitFadeDuration="300">
<item android:state_selected="true">
<bitmap android:src="@drawable/ic_flag_white_24dp"
android:tint="@color/grey_800"/>
</item>
<item>
<bitmap android:src="@drawable/ic_flag_white_24dp"
android:tint="@color/grey_400"/>
</item>
</selector>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:enterFadeDuration="200"
android:exitFadeDuration="300">
<item android:state_selected="true">
<bitmap android:src="@drawable/ic_restaurant_white_24dp"
android:tint="@color/grey_400"/>
</item>
<item>
<bitmap android:src="@drawable/ic_restaurant_white_24dp"
android:tint="@color/grey_700"/>
</item>
</selector>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:enterFadeDuration="200"
android:exitFadeDuration="300">
<item android:state_selected="true">
<bitmap android:src="@drawable/ic_restaurant_white_24dp"
android:tint="@color/grey_800"/>
</item>
<item>
<bitmap android:src="@drawable/ic_restaurant_white_24dp"
android:tint="@color/grey_400"/>
</item>
</selector>

View File

@ -1,7 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" <selector xmlns:android="http://schemas.android.com/apk/res/android"
android:enterFadeDuration="200" android:enterFadeDuration="200"
android:exitFadeDuration="300"> android:exitFadeDuration="300">
<item android:state_selected="true" android:drawable="@drawable/ic_emoji_objects_activated_dark" /> <item android:state_selected="true">
<item android:drawable="@drawable/ic_emoji_objects_normal_dark" /> <bitmap android:src="@drawable/ic_lightbulb_outline_white_24dp"
android:tint="@color/grey_400"/>
</item>
<item>
<bitmap android:src="@drawable/ic_lightbulb_outline_white_24dp"
android:tint="@color/grey_700"/>
</item>
</selector> </selector>

View File

@ -1,7 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" <selector xmlns:android="http://schemas.android.com/apk/res/android"
android:enterFadeDuration="200" android:enterFadeDuration="200"
android:exitFadeDuration="300"> android:exitFadeDuration="300">
<item android:state_selected="true" android:drawable="@drawable/ic_emoji_objects_activated_light" /> <item android:state_selected="true">
<item android:drawable="@drawable/ic_emoji_objects_normal_light" /> <bitmap android:src="@drawable/ic_lightbulb_outline_white_24dp"
android:tint="@color/grey_800"/>
</item>
<item>
<bitmap android:src="@drawable/ic_lightbulb_outline_white_24dp"
android:tint="@color/grey_400"/>
</item>
</selector> </selector>

View File

@ -52,10 +52,13 @@
<attr name="emoji_category_recent" format="reference"/> <attr name="emoji_category_recent" format="reference"/>
<attr name="emoji_category_people" format="reference"/> <attr name="emoji_category_people" format="reference"/>
<attr name="emoji_category_objects" format="reference"/>
<attr name="emoji_category_nature" format="reference"/> <attr name="emoji_category_nature" format="reference"/>
<attr name="emoji_category_foods" format="reference"/>
<attr name="emoji_category_activity" format="reference"/>
<attr name="emoji_category_places" format="reference"/> <attr name="emoji_category_places" format="reference"/>
<attr name="emoji_category_objects" format="reference"/>
<attr name="emoji_category_symbol" format="reference"/> <attr name="emoji_category_symbol" format="reference"/>
<attr name="emoji_category_flags" format="reference"/>
<attr name="emoji_category_emoticons" format="reference"/> <attr name="emoji_category_emoticons" format="reference"/>
<attr name="quick_camera_icon" format="reference"/> <attr name="quick_camera_icon" format="reference"/>
<attr name="quick_mic_icon" format="reference"/> <attr name="quick_mic_icon" format="reference"/>

View File

@ -137,10 +137,13 @@
<item name="emoji_category_recent">@drawable/emoji_category_recent_light</item> <item name="emoji_category_recent">@drawable/emoji_category_recent_light</item>
<item name="emoji_category_people">@drawable/emoji_category_people_light</item> <item name="emoji_category_people">@drawable/emoji_category_people_light</item>
<item name="emoji_category_objects">@drawable/emoji_category_objects_light</item>
<item name="emoji_category_nature">@drawable/emoji_category_nature_light</item> <item name="emoji_category_nature">@drawable/emoji_category_nature_light</item>
<item name="emoji_category_foods">@drawable/emoji_category_foods_light</item>
<item name="emoji_category_activity">@drawable/emoji_category_activity_light</item>
<item name="emoji_category_places">@drawable/emoji_category_places_light</item> <item name="emoji_category_places">@drawable/emoji_category_places_light</item>
<item name="emoji_category_objects">@drawable/emoji_category_objects_light</item>
<item name="emoji_category_symbol">@drawable/emoji_category_symbol_light</item> <item name="emoji_category_symbol">@drawable/emoji_category_symbol_light</item>
<item name="emoji_category_flags">@drawable/emoji_category_flags_light</item>
<item name="emoji_category_emoticons">@drawable/emoji_category_emoticons_light</item> <item name="emoji_category_emoticons">@drawable/emoji_category_emoticons_light</item>
@ -280,10 +283,13 @@
<item name="emoji_category_recent">@drawable/emoji_category_recent_dark</item> <item name="emoji_category_recent">@drawable/emoji_category_recent_dark</item>
<item name="emoji_category_people">@drawable/emoji_category_people_dark</item> <item name="emoji_category_people">@drawable/emoji_category_people_dark</item>
<item name="emoji_category_objects">@drawable/emoji_category_objects_dark</item>
<item name="emoji_category_nature">@drawable/emoji_category_nature_dark</item> <item name="emoji_category_nature">@drawable/emoji_category_nature_dark</item>
<item name="emoji_category_foods">@drawable/emoji_category_foods_dark</item>
<item name="emoji_category_activity">@drawable/emoji_category_activity_dark</item>
<item name="emoji_category_places">@drawable/emoji_category_places_dark</item> <item name="emoji_category_places">@drawable/emoji_category_places_dark</item>
<item name="emoji_category_objects">@drawable/emoji_category_objects_dark</item>
<item name="emoji_category_symbol">@drawable/emoji_category_symbol_dark</item> <item name="emoji_category_symbol">@drawable/emoji_category_symbol_dark</item>
<item name="emoji_category_flags">@drawable/emoji_category_flags_dark</item>
<item name="emoji_category_emoticons">@drawable/emoji_category_emoticons_dark</item> <item name="emoji_category_emoticons">@drawable/emoji_category_emoticons_dark</item>
<item name="quick_camera_icon">@drawable/quick_camera_dark</item> <item name="quick_camera_icon">@drawable/quick_camera_dark</item>

View File

@ -68,7 +68,8 @@ public class EmojiDrawer extends LinearLayout implements InputView {
RepeatableImageKey backspace = (RepeatableImageKey)v.findViewById(R.id.backspace); RepeatableImageKey backspace = (RepeatableImageKey)v.findViewById(R.id.backspace);
backspace.setOnKeyEventListener(new KeyEventListener() { backspace.setOnKeyEventListener(new KeyEventListener() {
@Override public void onKeyEvent() { @Override
public void onKeyEvent() {
if (listener != null) listener.onKeyEvent(DELETE_KEY_EVENT); if (listener != null) listener.onKeyEvent(DELETE_KEY_EVENT);
} }
}); });
@ -101,7 +102,8 @@ public class EmojiDrawer extends LinearLayout implements InputView {
pager.setAdapter(new EmojiPagerAdapter(getContext(), pager.setAdapter(new EmojiPagerAdapter(getContext(),
models, models,
new EmojiSelectionListener() { new EmojiSelectionListener() {
@Override public void onEmojiSelected(String emoji) { @Override
public void onEmojiSelected(String emoji) {
Log.w("EmojiDrawer", "onEmojiSelected()"); Log.w("EmojiDrawer", "onEmojiSelected()");
recentModel.onCodePointSelected(emoji); recentModel.onCodePointSelected(emoji);
if (listener != null) listener.onEmojiSelected(emoji); if (listener != null) listener.onEmojiSelected(emoji);
@ -143,7 +145,8 @@ public class EmojiDrawer extends LinearLayout implements InputView {
return pages.size(); return pages.size();
} }
@Override public Object instantiateItem(ViewGroup container, int position) { @Override
public Object instantiateItem(ViewGroup container, int position) {
EmojiPageView page = new EmojiPageView(context); EmojiPageView page = new EmojiPageView(context);
page.setModel(pages.get(position)); page.setModel(pages.get(position));
page.setEmojiSelectedListener(listener); page.setEmojiSelectedListener(listener);
@ -151,22 +154,26 @@ public class EmojiDrawer extends LinearLayout implements InputView {
return page; return page;
} }
@Override public void destroyItem(ViewGroup container, int position, Object object) { @Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View)object); container.removeView((View)object);
} }
@Override public void setPrimaryItem(ViewGroup container, int position, Object object) { @Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
EmojiPageView current = (EmojiPageView) object; EmojiPageView current = (EmojiPageView) object;
current.onSelected(); current.onSelected();
super.setPrimaryItem(container, position, object); super.setPrimaryItem(container, position, object);
} }
@Override public boolean isViewFromObject(View view, Object object) { @Override
public boolean isViewFromObject(View view, Object object) {
return view == object; return view == object;
} }
@Override public View getCustomTabView(ViewGroup viewGroup, int i) { @Override
ImageView image = new ImageView(context); public View getCustomTabView(ViewGroup viewGroup, int i) {
ImageView image = new ImageView(context);
image.setScaleType(ScaleType.CENTER_INSIDE); image.setScaleType(ScaleType.CENTER_INSIDE);
image.setImageResource(ResUtil.getDrawableRes(context, pages.get(i).getIconAttr())); image.setImageResource(ResUtil.getDrawableRes(context, pages.get(i).getIconAttr()));
return image; return image;

View File

@ -13,15 +13,18 @@ public class EmojiFilter implements InputFilter {
this.view = view; this.view = view;
} }
@Override public CharSequence filter(CharSequence source, int start, int end, @Override
Spanned dest, int dstart, int dend) public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)
{ {
char[] v = new char[end - start]; char[] v = new char[end - start];
TextUtils.getChars(source, start, end, v, 0); TextUtils.getChars(source, start, end, v, 0);
Spannable emojified = EmojiProvider.getInstance(view.getContext()).emojify(new String(v), view); Spannable emojified = EmojiProvider.getInstance(view.getContext()).emojify(new String(v), view);
if (source instanceof Spanned && emojified != null) { if (source instanceof Spanned && emojified != null) {
TextUtils.copySpansFrom((Spanned) source, start, end, null, emojified, 0); TextUtils.copySpansFrom((Spanned) source, start, end, null, emojified, 0);
} }
return emojified; return emojified;
} }
} }

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,6 @@ import android.graphics.Paint;
import android.graphics.PixelFormat; import android.graphics.PixelFormat;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Build.VERSION; import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES; import android.os.Build.VERSION_CODES;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
@ -17,42 +16,33 @@ import android.support.annotation.Nullable;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.util.Log; import android.util.Log;
import android.util.SparseArray;
import android.widget.TextView; import android.widget.TextView;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo;
import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.components.emoji.parsing.EmojiPageBitmap;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiTree;
import org.thoughtcrime.securesms.util.FutureTaskListener; import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import java.io.IOException; import java.util.List;
import java.lang.ref.SoftReference;
import java.util.concurrent.Callable; class EmojiProvider {
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class EmojiProvider {
private static final String TAG = EmojiProvider.class.getSimpleName(); private static final String TAG = EmojiProvider.class.getSimpleName();
private static volatile EmojiProvider instance = null; private static volatile EmojiProvider instance = null;
private static final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); private static final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
private final SparseArray<DrawInfo> offsets = new SparseArray<>(); private final EmojiTree emojiTree = new EmojiTree();
@SuppressWarnings("MalformedRegex") private static final int EMOJI_RAW_HEIGHT = 64;
// 0x203c,0x2049 0x20a0-0x32ff 0x1f00-0x1fff 0xfe4e5-0xfe4ee private static final int EMOJI_RAW_WIDTH = 64;
// |== !!, ?! ==||==== misc ====||======== emoticons ========||========= flags ==========| private static final int EMOJI_VERT_PAD = 0;
private static final Pattern EMOJI_RANGE = Pattern.compile("[\\u203c\\u2049\\u20a0-\\u32ff\\ud83c\\udc00-\\ud83d\\udeff\\udbb9\\udce5-\\udbb9\\udcee]"); private static final int EMOJI_PER_ROW = 32;
public static final int EMOJI_RAW_HEIGHT = 64; private final float decodeScale;
public static final int EMOJI_RAW_WIDTH = 64; private final float verticalPad;
public static final int EMOJI_VERT_PAD = 0;
public static final int EMOJI_PER_ROW = 32;
private final Context context;
private final float decodeScale;
private final float verticalPad;
public static EmojiProvider getInstance(Context context) { public static EmojiProvider getInstance(Context context) {
if (instance == null) { if (instance == null) {
@ -66,29 +56,31 @@ public class EmojiProvider {
} }
private EmojiProvider(Context context) { private EmojiProvider(Context context) {
this.context = context.getApplicationContext();
this.decodeScale = Math.min(1f, context.getResources().getDimension(R.dimen.emoji_drawer_size) / EMOJI_RAW_HEIGHT); this.decodeScale = Math.min(1f, context.getResources().getDimension(R.dimen.emoji_drawer_size) / EMOJI_RAW_HEIGHT);
this.verticalPad = EMOJI_VERT_PAD * this.decodeScale; this.verticalPad = EMOJI_VERT_PAD * this.decodeScale;
for (EmojiPageModel page : EmojiPages.PAGES) { for (EmojiPageModel page : EmojiPages.PAGES) {
if (page.hasSpriteMap()) { if (page.hasSpriteMap()) {
final EmojiPageBitmap pageBitmap = new EmojiPageBitmap(page); EmojiPageBitmap pageBitmap = new EmojiPageBitmap(context, page, decodeScale);
for (int i=0; i < page.getEmoji().length; i++) {
offsets.put(Character.codePointAt(page.getEmoji()[i], 0), new DrawInfo(pageBitmap, i)); for (int i=0;i<page.getEmoji().length;i++) {
emojiTree.add(page.getEmoji()[i], new EmojiDrawInfo(pageBitmap, i));
} }
} }
} }
} }
public @Nullable Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv) { @Nullable Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv) {
if (text == null) return null; if (text == null) return null;
Matcher matches = EMOJI_RANGE.matcher(text);
SpannableStringBuilder builder = new SpannableStringBuilder(text);
while (matches.find()) { List<EmojiParser.Candidate> matches = new EmojiParser(emojiTree).findCandidates(text);
int codePoint = matches.group().codePointAt(0); SpannableStringBuilder builder = new SpannableStringBuilder(text);
Drawable drawable = getEmojiDrawable(codePoint);
for (EmojiParser.Candidate candidate : matches) {
Drawable drawable = getEmojiDrawable(candidate.getDrawInfo());
if (drawable != null) { if (drawable != null) {
builder.setSpan(new EmojiSpan(drawable, tv), matches.start(), matches.end(), builder.setSpan(new EmojiSpan(drawable, tv), candidate.getStartIndex(), candidate.getEndIndex(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
} }
@ -96,17 +88,18 @@ public class EmojiProvider {
return builder; return builder;
} }
public Drawable getEmojiDrawable(int emojiCode) { @Nullable Drawable getEmojiDrawable(CharSequence emoji) {
return getEmojiDrawable(offsets.get(emojiCode)); EmojiDrawInfo drawInfo = emojiTree.getEmoji(emoji, 0, emoji.length());
return getEmojiDrawable(drawInfo);
} }
private Drawable getEmojiDrawable(DrawInfo drawInfo) { private @Nullable Drawable getEmojiDrawable(@Nullable EmojiDrawInfo drawInfo) {
if (drawInfo == null) { if (drawInfo == null) {
return null; return null;
} }
final EmojiDrawable drawable = new EmojiDrawable(drawInfo, decodeScale); final EmojiDrawable drawable = new EmojiDrawable(drawInfo, decodeScale);
drawInfo.page.get().addListener(new FutureTaskListener<Bitmap>() { drawInfo.getPage().get().addListener(new FutureTaskListener<Bitmap>() {
@Override public void onSuccess(final Bitmap result) { @Override public void onSuccess(final Bitmap result) {
Util.runOnMain(new Runnable() { Util.runOnMain(new Runnable() {
@Override public void run() { @Override public void run() {
@ -122,34 +115,36 @@ public class EmojiProvider {
return drawable; return drawable;
} }
public class EmojiDrawable extends Drawable { class EmojiDrawable extends Drawable {
private final DrawInfo info; private final EmojiDrawInfo info;
private Bitmap bmp; private Bitmap bmp;
private float intrinsicWidth; private float intrinsicWidth;
private float intrinsicHeight; private float intrinsicHeight;
@Override public int getIntrinsicWidth() { @Override
public int getIntrinsicWidth() {
return (int)intrinsicWidth; return (int)intrinsicWidth;
} }
@Override public int getIntrinsicHeight() { @Override
public int getIntrinsicHeight() {
return (int)intrinsicHeight; return (int)intrinsicHeight;
} }
public EmojiDrawable(DrawInfo info, float decodeScale) { EmojiDrawable(EmojiDrawInfo info, float decodeScale) {
this.info = info; this.info = info;
this.intrinsicWidth = EMOJI_RAW_WIDTH * decodeScale; this.intrinsicWidth = EMOJI_RAW_WIDTH * decodeScale;
this.intrinsicHeight = EMOJI_RAW_HEIGHT * decodeScale; this.intrinsicHeight = EMOJI_RAW_HEIGHT * decodeScale;
} }
@Override @Override
public void draw(Canvas canvas) { public void draw(@NonNull Canvas canvas) {
if (bmp == null) { if (bmp == null) {
return; return;
} }
final int row = info.index / EMOJI_PER_ROW; final int row = info.getIndex() / EMOJI_PER_ROW;
final int row_index = info.index % EMOJI_PER_ROW; final int row_index = info.getIndex() % EMOJI_PER_ROW;
canvas.drawBitmap(bmp, canvas.drawBitmap(bmp,
new Rect((int)(row_index * intrinsicWidth), new Rect((int)(row_index * intrinsicWidth),
@ -181,85 +176,4 @@ public class EmojiProvider {
public void setColorFilter(ColorFilter cf) { } public void setColorFilter(ColorFilter cf) { }
} }
private static class DrawInfo {
EmojiPageBitmap page;
int index;
public DrawInfo(final EmojiPageBitmap page, final int index) {
this.page = page;
this.index = index;
}
@Override
public String toString() {
return "DrawInfo{" +
"page=" + page +
", index=" + index +
'}';
}
}
private class EmojiPageBitmap {
private EmojiPageModel model;
private SoftReference<Bitmap> bitmapReference;
private ListenableFutureTask<Bitmap> task;
public EmojiPageBitmap(EmojiPageModel model) {
this.model = model;
}
private ListenableFutureTask<Bitmap> get() {
Util.assertMainThread();
if (bitmapReference != null && bitmapReference.get() != null) {
return new ListenableFutureTask<>(bitmapReference.get());
} else if (task != null) {
return task;
} else {
Callable<Bitmap> callable = new Callable<Bitmap>() {
@Override public Bitmap call() throws Exception {
try {
Log.w(TAG, "loading page " + model.getSprite());
return loadPage();
} catch (IOException ioe) {
Log.w(TAG, ioe);
}
return null;
}
};
task = new ListenableFutureTask<>(callable);
new AsyncTask<Void, Void, Void>() {
@Override protected Void doInBackground(Void... params) {
task.run();
return null;
}
@Override protected void onPostExecute(Void aVoid) {
task = null;
}
}.execute();
}
return task;
}
private Bitmap loadPage() throws IOException {
if (bitmapReference != null && bitmapReference.get() != null) return bitmapReference.get();
try {
final Bitmap bitmap = BitmapUtil.createScaledBitmap(context,
"file:///android_asset/" + model.getSprite(),
decodeScale);
bitmapReference = new SoftReference<>(bitmap);
Log.w(TAG, "onPageLoaded(" + model.getSprite() + ")");
return bitmap;
} catch (BitmapDecodingException e) {
Log.w(TAG, e);
throw new IOException(e);
}
}
@Override public String toString() {
return model.getSprite();
}
}
} }

View File

@ -32,7 +32,7 @@ public class EmojiView extends View implements Drawable.Callback {
public void setEmoji(String emoji) { public void setEmoji(String emoji) {
this.emoji = emoji; this.emoji = emoji;
this.drawable = EmojiProvider.getInstance(getContext()) this.drawable = EmojiProvider.getInstance(getContext())
.getEmojiDrawable(Character.codePointAt(emoji, 0)); .getEmojiDrawable(emoji);
postInvalidate(); postInvalidate();
} }

View File

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

View File

@ -0,0 +1,90 @@
package org.thoughtcrime.securesms.components.emoji.parsing;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.util.Log;
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.concurrent.Callable;
public class EmojiPageBitmap {
private static final String TAG = EmojiPageBitmap.class.getName();
private final Context context;
private final EmojiPageModel model;
private final float decodeScale;
private SoftReference<Bitmap> bitmapReference;
private ListenableFutureTask<Bitmap> task;
public EmojiPageBitmap(@NonNull Context context, @NonNull EmojiPageModel model, float decodeScale) {
this.context = context.getApplicationContext();
this.model = model;
this.decodeScale = decodeScale;
}
public ListenableFutureTask<Bitmap> get() {
Util.assertMainThread();
if (bitmapReference != null && bitmapReference.get() != null) {
return new ListenableFutureTask<>(bitmapReference.get());
} else if (task != null) {
return task;
} else {
Callable<Bitmap> callable = new Callable<Bitmap>() {
@Override public Bitmap call() throws Exception {
try {
Log.w(TAG, "loading page " + model.getSprite());
return loadPage();
} catch (IOException ioe) {
Log.w(TAG, ioe);
}
return null;
}
};
task = new ListenableFutureTask<>(callable);
new AsyncTask<Void, Void, Void>() {
@Override protected Void doInBackground(Void... params) {
task.run();
return null;
}
@Override protected void onPostExecute(Void aVoid) {
task = null;
}
}.execute();
}
return task;
}
private Bitmap loadPage() throws IOException {
if (bitmapReference != null && bitmapReference.get() != null) return bitmapReference.get();
try {
final Bitmap bitmap = BitmapUtil.createScaledBitmap(context,
"file:///android_asset/" + model.getSprite(),
decodeScale);
bitmapReference = new SoftReference<>(bitmap);
Log.w(TAG, "onPageLoaded(" + model.getSprite() + ")");
return bitmap;
} catch (BitmapDecodingException e) {
Log.w(TAG, e);
throw new IOException(e);
}
}
@Override
public String toString() {
return model.getSprite();
}
}

View File

@ -0,0 +1,108 @@
/**
* Copyright (c) 2014-present Vincent DURMONT vdurmont@gmail.com
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.thoughtcrime.securesms.components.emoji.parsing;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.LinkedList;
import java.util.List;
/**
* Based in part on code from emoji-java
*/
public class EmojiParser {
private final EmojiTree emojiTree;
public EmojiParser(EmojiTree emojiTree) {
this.emojiTree = emojiTree;
}
public @NonNull List<Candidate> findCandidates(@Nullable CharSequence text) {
List<Candidate> results = new LinkedList<>();
if (text == null) return results;
for (int i = 0; i < text.length(); i++) {
int emojiEnd = getEmojiEndPos(text, i);
if (emojiEnd != -1) {
EmojiDrawInfo drawInfo = emojiTree.getEmoji(text, i, emojiEnd);
if (emojiEnd + 2 <= text.length()) {
if (Fitzpatrick.fitzpatrickFromUnicode(text, emojiEnd) != null) {
emojiEnd += 2;
}
}
results.add(new Candidate(i, emojiEnd, drawInfo));
i = emojiEnd - 1;
}
}
return results;
}
private int getEmojiEndPos(CharSequence text, int startPos) {
int best = -1;
for (int j = startPos + 1; j <= text.length(); j++) {
EmojiTree.Matches status = emojiTree.isEmoji(text, startPos, j);
if (status.exactMatch()) {
best = j;
} else if (status.impossibleMatch()) {
return best;
}
}
return best;
}
public class Candidate {
private final int startIndex;
private final int endIndex;
private final EmojiDrawInfo drawInfo;
Candidate(int startIndex, int endIndex, EmojiDrawInfo drawInfo) {
this.startIndex = startIndex;
this.endIndex = endIndex;
this.drawInfo = drawInfo;
}
public EmojiDrawInfo getDrawInfo() {
return drawInfo;
}
public int getEndIndex() {
return endIndex;
}
public int getStartIndex() {
return startIndex;
}
}
}

View File

@ -0,0 +1,128 @@
/**
* Copyright (c) 2014-present Vincent DURMONT vdurmont@gmail.com
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package org.thoughtcrime.securesms.components.emoji.parsing;
import android.support.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* Based in part on code from emoji-java
*/
public class EmojiTree {
private final EmojiTreeNode root = new EmojiTreeNode();
public void add(String emojiEncoding, EmojiDrawInfo emoji) {
EmojiTreeNode tree = root;
for (char c: emojiEncoding.toCharArray()) {
if (!tree.hasChild(c)) {
tree.addChild(c);
}
tree = tree.getChild(c);
}
tree.setEmoji(emoji);
}
public Matches isEmoji(CharSequence sequence, int startPosition, int endPosition) {
if (sequence == null) {
return Matches.POSSIBLY;
}
EmojiTreeNode tree = root;
for (int i=startPosition; i<endPosition; i++) {
char character = sequence.charAt(i);
if (!tree.hasChild(character)) {
return Matches.IMPOSSIBLE;
}
tree = tree.getChild(character);
}
return tree.isEndOfEmoji() ? Matches.EXACTLY : Matches.POSSIBLY;
}
public @Nullable EmojiDrawInfo getEmoji(CharSequence unicode, int startPosition, int endPostiion) {
EmojiTreeNode tree = root;
for (int i=startPosition; i<endPostiion; i++) {
char character = unicode.charAt(i);
if (!tree.hasChild(character)) {
return null;
}
tree = tree.getChild(character);
}
return tree.getEmoji();
}
private static class EmojiTreeNode {
private Map<Character, EmojiTreeNode> children = new HashMap<>();
private EmojiDrawInfo emoji;
public void setEmoji(EmojiDrawInfo emoji) {
this.emoji = emoji;
}
public @Nullable EmojiDrawInfo getEmoji() {
return emoji;
}
boolean hasChild(char child) {
return children.containsKey(child);
}
void addChild(char child) {
children.put(child, new EmojiTreeNode());
}
EmojiTreeNode getChild(char child) {
return children.get(child);
}
boolean isEndOfEmoji() {
return emoji != null;
}
}
public enum Matches {
EXACTLY, POSSIBLY, IMPOSSIBLE;
public boolean exactMatch() {
return this == EXACTLY;
}
public boolean impossibleMatch() {
return this == IMPOSSIBLE;
}
}
}

View File

@ -0,0 +1,64 @@
package org.thoughtcrime.securesms.components.emoji.parsing;
public enum Fitzpatrick {
/**
* Fitzpatrick modifier of type 1/2 (pale white/white)
*/
TYPE_1_2("\uD83C\uDFFB"),
/**
* Fitzpatrick modifier of type 3 (cream white)
*/
TYPE_3("\uD83C\uDFFC"),
/**
* Fitzpatrick modifier of type 4 (moderate brown)
*/
TYPE_4("\uD83C\uDFFD"),
/**
* Fitzpatrick modifier of type 5 (dark brown)
*/
TYPE_5("\uD83C\uDFFE"),
/**
* Fitzpatrick modifier of type 6 (black)
*/
TYPE_6("\uD83C\uDFFF");
/**
* The unicode representation of the Fitzpatrick modifier
*/
public final String unicode;
Fitzpatrick(String unicode) {
this.unicode = unicode;
}
public static Fitzpatrick fitzpatrickFromUnicode(CharSequence unicode, int index) {
for (Fitzpatrick v : values()) {
boolean match = true;
for (int i=0;i<v.unicode.toCharArray().length;i++) {
if (v.unicode.toCharArray()[i] != unicode.charAt(index + i)) {
match = false;
}
}
if (match) return v;
}
return null;
}
public static Fitzpatrick fitzpatrickFromType(String type) {
try {
return Fitzpatrick.valueOf(type.toUpperCase());
} catch (IllegalArgumentException e) {
return null;
}
}
}