欢迎Follow我的GitHub, 关注我的CSDN, 精彩不断!

本文的合集已经编著成书,高级Android开发强化实战,欢迎各位读友的建议和指导。在京东即可购买:https://item.jd.com/12385680.html

Emoji (絵文字 或 えもじ; 日语发音: [emodʑi]) 是日本无线通讯中所使用的视觉情感符号, 代表图形, 文字是图形本身的隐喻. 用于输入者表达情感信息, 如笑脸就代表开心, 蛋糕就代表食物等. 形象生动, 在文字中出现图片, 更容易实现情感的表述.

CSDN: http://blog.csdn.net/caroline_wendy/article/details/68485011

Emoji起初只能在日本使用, 如今相当一部分的Emoji字符集已经被收入Unicode编码, 使其能被广泛应用. Android系统对于Emoji的原生支持从4.4版本开始. 对于文字输入型应用而言, 自定义的Emoji表情会大幅提升用户体验, 增强用户对于应用的辨识度, 也使输入更加有趣. 原生的Emoji表情由于需要适配多款机型, 节省存储空间, 所以设计得较为粗糙. 优秀美工重绘的Emoji表情, 一般都会更加符合用户的视觉习惯, 这就是QQ和微信大量重绘Emoji的原因.

本文介绍Emoji表情的实现方式, 具体效果参考春雨医生的在线问诊页面.


下载Emoji列表

Emoji表情数据的存储方式有两种, 第一种在本地, 随着应用一起分发; 第二种在远程, 访问服务器获取. 显然第二种更为合理, 易于修改和替换, 方便重绘Emoji表情的后续扩容. 从远程服务器中获取Emoji数据时, 注意需要使用有序列表, 因为根据用户的使用习惯不同, 有些常用表情在先, 有些不常用在后. 考虑列表的有序性, 选择ArrayList-Pair数据结构传输, 而非Map, 因为列表是有序的, 而Map是无序的, 也可以选择LinkedHashMap.

本例Emoji数据集的数据结构是ArrayList<Pair<String, String>>, 其中Pair的Key是Emoji的Unicode字符, Value是Emoji表情的下载地址.

// 下载Emoji表情并缓存
ArrayList<Pair<String, String>> pairs = remoteData.getChunyuEmoji();
if (pairs != null) {saveEmoji(context, pairs);
}

在获取Emoji表情集合的全部表情下载地址后, 将这些表情缓存至本地, 统一更新, 减少访问远程服务器的次数, 节省流量和电量. 表情集合存储在BitmapLruCache类中, 即LRU缓存类, 其缓存模块使用内存(Memory)与本地硬盘(Disk)的二级缓存. 注意下载过程需要在非UI线程中进行, 即EmojiDownloadAsyncTasks.

/*** 下载并缓存Emoji标签** @param context 上下文* @param pairs   表情对[Emoji符号, Emoji下载地址]*/
private void saveEmoji(@NonNull Context context, @NonNull ArrayList<Pair<String, String>> pairs) {// 当未提供数据时, 不刷新Emoji的数据if (pairs.size() == 0) {return;}ArrayList<String> urls = new ArrayList<>();for (Pair<String, String> pair : pairs) {urls.add(pair.second);}new EmojiDownloadAsyncTasks(context, urls).execute();
}// Emoji表情的异步下载链接, 存储至缓存
public static class EmojiDownloadAsyncTasks extends AsyncTask<Void, Void, Void> {private final Context mContext;private final ArrayList<String> mUrls;public EmojiDownloadAsyncTasks(final @NonNull Context context,final @NonNull ArrayList<String> urls) {mContext = context.getApplicationContext();mUrls = urls;}@Overrideprotected @Nullable Void doInBackground(Void... params) {BitmapLruCache cache = BitmapLruCache.getInstance(mContext);for (int i = 0; i < mUrls.size(); ++i) {try {cache.addBitmapToCache(mUrls.get(i));} catch (IOException e) {e.printStackTrace();}}return null;}
}

缓存Emoji数据

为了快速地访问Emoji表情, 为其添加图片缓存必不可少. 本例的缓存类是BitmapLruCache, 其内部使用常见的二级缓存, 即内存缓存和硬盘缓存.

注意: 为了加快开发和减少错误, 尽量选择复用已有的轮子. 内存缓存使用Android系统自带的LruCache; 外存缓存使用DiskLruCache(Jake Wharton).

private static final String EMOJI_FOLDER = "bitmap"; // Bitmap的缓存文件夹
private static final int CACHE_VERSION = 1; // 缓存文件版本
private static final int CACHE_SIZE = 1024 * 1024 * 20; // 缓存文件大小private LruCache<String, Bitmap> mMemoryCache; // 内存缓存
private DiskLruCache mDiskCache; // DiskLruCache, 硬盘缓存
private final Context mContext; // 上下文private static BitmapLruCache sInstance; // 单例private BitmapLruCache(@NonNull final Context context) {mContext = context.getApplicationContext();initMemoryCache(); // 初始化内存缓存initDiskCache(mContext); // 初始化磁盘缓存
}/*** 初始化内存缓存*/
private void initMemoryCache() {final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);final int cacheSize = maxMemory / 4;mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {@Override protected int sizeOf(String key, Bitmap value) {return value.getRowBytes() * value.getHeight() / 1024;}};
}/*** 初始化外存缓存** @param context 上下文*/
private void initDiskCache(@NonNull final Context context) {// 获取缓存文件File diskCacheDir = getDiskCacheDir(context);// 如果文件不存在, 则创建if (!diskCacheDir.exists()) {if (!diskCacheDir.mkdirs()) {Log.e("BitmapLruCache", "ERROR: 创建缓存失败");}}try {// 创建缓存地址mDiskCache = DiskLruCache.open(diskCacheDir, CACHE_VERSION, 1, CACHE_SIZE);} catch (IOException e) {e.printStackTrace();}
}

类中的addBitmapToCache方法, 将表情下载的url作为缓存映射Map的唯一Key. 下载后的Bitmap, 会优先写入外存缓存, 再同步写入内存缓存.

/*** 将Bitmap写入缓存** @param url Bitmap的网络Url(唯一标识)* @throws IOException*/
public void addBitmapToCache(final @NonNull String url) throws IOException {if (mDiskCache == null || TextUtils.isEmpty(url)) {return;}String key = hashKeyFormUrl(url); // Url的KeyDiskLruCache.Editor editor = mDiskCache.edit(key); // 得到Editor对象if (editor != null) {OutputStream outputStream = editor.newOutputStream(0);// 根据输出流的返回值决定是否提交至缓存if (downloadUrlToStream(url, outputStream)) {// 提交写入操作editor.commit();} else {// 撤销写入操作editor.abort();}mDiskCache.flush(); // 更新缓存}getBitmapFromCache(url); // 加载内存缓存
}

类中的getBitmapFromCache方法, 根据唯一标识下载url, 获取Bitmap. 优先从内存中获取, 当内存缓存不存在时, 从外存读取, 再同步写入内存; 当内存缓存存在时, 直接返回.

注意: Emoji表情一般都使用较小尺寸, 当图片加载入内存时, 防止图片过大, 优先进行压缩, 避免占用内存过多, 产生OOM. 尺寸大小支持外部配置.

/*** 从缓存中取出Bitmap** @param url 网络Url的地址, 图片的唯一标识* @return url匹配的Bitmap* @throws IOException*/
public Bitmap getBitmapFromCache(final @NonNull String url) throws IOException {//如果缓存中为空  直接返回为空if (mDiskCache == null || mMemoryCache == null || TextUtils.isEmpty(url)) {return null;}// 通过key值在缓存中找到对应的BitmapString key = hashKeyFormUrl(url);Bitmap bitmap = mMemoryCache.get(key);if (bitmap == null) {// 通过key得到Snapshot对象DiskLruCache.Snapshot snapShot = mDiskCache.get(key);if (snapShot != null) {// 得到文件输入流InputStream ins = snapShot.getInputStream(0);bitmap = BitmapFactory.decodeStream(ins);}if (bitmap != null) {// 设置图片大小, 防止内存缓存溢出, 节省内存int size = AppUtils.spToPx(mContext, mBitmapSize); // 默认18bitmap = Bitmap.createScaledBitmap(bitmap, size, size, true);mMemoryCache.put(key, bitmap);}}return bitmap;
}

管理Emoji数据

本例使用EmojiFileManager类作为Emoji表情集合的管理器, 同时作为接口, 向外部提供数据和方法. 原始的有序列表转换为无需映射HashMap, 便于快速查找表情; 转换为分页列表, 使用List<List<EmojiIcon>>匹配ViewPager的表情分页显示.

/*** 初始化Emoji的数据*/
public void initEmojiData() {DailyRequestData data = DailyRequestManager.getInstance().getLocalData();if (data != null) {// Emoji的有序列表ArrayList<Pair<String, String>> emojiPairList = data.getChunyuEmoji();if (!Utils.isListEmpty(emojiPairList)) {parseData(emojiPairList); // 结构化Emoji数据列表}}
}/*** 解析数据, 提前分页设置, 每页的表情数PAGE_SIZE.** @param pairs Emoji的Map*/
private void parseData(@NonNull final ArrayList<Pair<String, String>> pairs) {// 当解析数据为空时, 直接返回if (Utils.isListEmpty(pairs)) {return;}// 转换成为HashMap, 快速查找mEmojiMap = convertPairList2Map(pairs);// 转换为PageList, 用于ViewPagermEmojiPageLists = convertPairToPageList(pairs, PAGE_SIZE);
}

类中convertPairList2Map的方法, 将ArrayList-Pair数据结构转换为HashMap, 加快Emoji表情的查找速度.; 类中convertPairToPageList的方法, 将原始结构ArrayList-Pair, 组合成EmojiIcon的数组, 再根据每页显示个数, 重构成二维数组, 用于ViewPager的表情分页显示.


/*** 将有序的PairList转换为无序的Map** @param pairs 列表* @return 无序Map*/
private static Map<String, String> convertPairList2Map(final @NonNull ArrayList<Pair<String, String>> pairs) {Map<String, String> map = new HashMap<>(); // 快速查找for (int i = 0; i < pairs.size(); ++i) {map.put(pairs.get(i).first, pairs.get(i).second);}return map;
}/*** 将有序的PairList转换为按页的List数组** @param pairs     列表* @param page_size 每页数量* @return 按页的List数组*/
private List<List<EmojiIcon>> convertPairToPageList(final @NonNull ArrayList<Pair<String, String>> pairs,final int page_size) {List<List<EmojiIcon>> emojiPageLists = new ArrayList<>();// 保存于内存中的表情集合ArrayList<EmojiIcon> emojiIcons = new ArrayList<>(); EmojiIcon emojiEntry;// 遍历列表, 放入列表for (Pair<String, String> entry : pairs) {emojiEntry = new EmojiIcon();emojiEntry.setUnicode(entry.first);emojiEntry.setUrl(entry.second);emojiIcons.add(emojiEntry);}// 每一个页数int pageCount = (int) Math.ceil(emojiIcons.size() / page_size + 0.1);for (int i = 0; i < pageCount; i++) {emojiPageLists.add(getListData(emojiIcons, i)); // 获取每页数据}return emojiPageLists;
}

替换Emoji表情

在字符串中, 替换Emoji表情的方式主要有两种: 第一种是在已有字符串中查找已经存在的Emoji编码, 替换为相应的表情; 第二种是创建单个Emoji表情的字符串.

类中的getExpressionString方法, 设置查找模式, 调用dealExpression替换相应Emoji表情, 并返回支持文字和图片的组合的SpannableString类型.

注意: 在Pattern中设置Pattern.UNICODE_CASE参数, 使其仅检查Unicode字符串, 缩小范围, 可以显著提升匹配速度, 否则在字符串较长时, 匹配速度较慢.

/*** 获得SpannableString对象, 通过传入的字符串, 进行正则判断** @param context 上下文* @param str     输入字符串* @return 组合字符串*/
public SpannableString getExpressionString(@NonNull final Context context,@NonNull final CharSequence str) {SpannableString spannableString = new SpannableString(str);// 正则表达式比配字符串里是否含有表情, 通过传入的正则表达式来生成Pattern// 注意Pattern的模式, 大小写不敏感, Unicode, 加快检索速度Pattern emojiPattern = Pattern.compile(EMOJI_REGEX,Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE);try {dealExpression(context, spannableString, emojiPattern, 0);} catch (Exception e) {Log.e(LOG_TAG, e.getMessage());}return spannableString;
}

类中dealExpression方法查找匹配字符串, 调用addBitmap2Spannable替换图片, 并递归解析剩下的字符串, 直至全部替换完成. 具体步骤:
1. 将所需替换的字符串与Emoji的Unicode标准编码匹配, 组成Matcher.
2. 如果Matcher匹配成功, 则获取相应的字符串key.
3. 如果Emoji字典中存在这个key, 则获取Emoji的对应url.
4. 如果url存在, 则调用addBitmap2Spannable替换字符串为Emoji表情.
5. 继续递归调用, 解析剩下的字符串.

/*** 对SpannableString进行正则判断,如果符合要求,则以表情图片代替** @param context   上下文* @param spannable 组合字符串* @param patten    模式* @param start     递归起始位置*/
private void dealExpression(@NonNull final Context context, SpannableString spannable,Pattern patten, final int start) {if (start < 0) {return;}// 将字符串与模式创建匹配Matcher matcher = patten.matcher(spannable);// 匹配成功while (matcher.find()) {String key = matcher.group().toLowerCase(); // 默认小写// 返回第一个字符的索引的文本匹配整个正则表达式, 如果是true则继续递归if (matcher.start() < start) {continue;}// 根据Key获取URLString url = mEmojiMap.get(key);// 通过上面匹配得到的字符串来生成图片资源idif (!TextUtils.isEmpty(url)) {// 计算该图片名字的长度,也就是要替换的字符串的长度int end = matcher.start() + key.length();spannable = addBitmap2Spannable(context, url, spannable, matcher.start(), end);if (end < spannable.length()) {// 如果整个字符串还未验证完,则继续dealExpression(context, spannable, patten, end);}break;}}
}

类中的addBitmap2Spannable方法, 根据Emoji的url, 从图片缓存BitmapLruCache中获取相应的表情(Bitmap), 创建居中对齐的VerticalImageSpan, 与文字组合成SpannableString.

/*** 添加图片至Spannable** @param context   上下文* @param url       图片网络连接* @param spannable 文字* @param start     起始修改* @param end       终止修改* @return 添加图片后的文字*/
private SpannableString addBitmap2Spannable(Context context, String url,SpannableString spannable, int start, int end) {// 当bitmap为空时, 无法替换内容Bitmap bitmap = null;try {bitmap = BitmapLruCache.getInstance(context).getBitmapFromCache(url);} catch (IOException e) {e.printStackTrace();}VerticalImageSpan imageSpan = new VerticalImageSpan(context, bitmap);spannable.setSpan(imageSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);return spannable;
}

默认的ImageSpan参数不包含居中显示, 重写getSizedraw方法, 使ImageSpan居中对齐于文字, 注意位置数据的设置.

/*** 竖直居中的ImageSpan* * Created by wangchenlong on 17/2/7.*/
public class VerticalImageSpan extends ImageSpan {private WeakReference<Drawable> mDrawableRef;private static boolean DEBUG = false;private Context mContext;public VerticalImageSpan(Context context, Bitmap bitmap) {super(context, bitmap);mContext = context;}@Overridepublic int getSize(Paint paint, CharSequence text,int start, int end,Paint.FontMetricsInt fm) {Drawable d = getCachedDrawable();Rect rect = d.getBounds();if (fm != null) {Paint.FontMetricsInt pfm = paint.getFontMetricsInt();// keep it the same as paint's fmfm.ascent = pfm.ascent;fm.descent = pfm.descent;fm.top = pfm.top;fm.bottom = pfm.bottom;}return rect.right;}@Overridepublic void draw(@NonNull Canvas canvas, CharSequence text,int start, int end, float x,int top, int y, int bottom, @NonNull Paint paint) {Drawable b = getCachedDrawable();canvas.save();int drawableHeight = b.getIntrinsicHeight();int fontAscent = paint.getFontMetricsInt().ascent;int fontDescent = paint.getFontMetricsInt().descent;int offset = (bottom - top) - drawableHeight - (AppUtils.spToPx(mContext, 1) + 1);int transY = (bottom - offset) - b.getBounds().bottom +  // align bottom to bottom(drawableHeight - fontDescent + fontAscent) / 2;  // align center to centercanvas.translate(x, transY);b.draw(canvas);canvas.restore();}// Redefined locally because it is a private member from DynamicDrawableSpanprivate Drawable getCachedDrawable() {WeakReference<Drawable> wr = mDrawableRef;Drawable d = null;if (wr != null)d = wr.get();if (d == null) {d = getDrawable();mDrawableRef = new WeakReference<>(d);}return d;}
}

类中的addIcon方法, 创建单个Emoji表情的字符串. 通过addBitmap2Spannable方法, 将Emoji编码字符串替换为表情.

/*** 添加表情, 根据URL至BitmapDiskLruCache中匹配** @param context 上下文* @param url     图片的网络URL* @param string  字符串* @return*/
public SpannableString addIcon(Context context, String url, String string) {SpannableString spannable = new SpannableString(string);return addBitmap2Spannable(context, url, spannable, 0, string.length());
}

在需要替换Emoji表情的位置, 调用EmojiFileManagergetExpressionString方法, 将字符串中的Emoji编码替换为Emoji表情; 在需要添加Emoji表情的位置, 调用其addIcon方法获取单个Emoji表情, 与已存在的字符串, 拼接成最终字符串.

效果如下:

为文字输入型应用添加Emoji表情吧, 让输入获得更多乐趣.

That’s all! Enjoy it!

Emoji's World, 一起实现Emoji相关推荐

  1. php 微信emoji输出,页面如何输出Emoji表情字符

    Emoji表情符号广泛运用于各种的社交软件上,是非常受欢迎的一种字符,如果用语言文字无法表达你内心的冲动,发一个Emoji表情就好了. 对于web而言如何在页面保存和展现这些字符呢?这可不是件容易的事 ...

  2. oracle存储emoji表情,Android自带emoji表情的使用方法详解

    什么是emoji表情 emoji表情是一种表情符号,在代码中它现在其实是一组遵循Unicode的编码,即每一个表情符号都对应了一个Unicode编码.更进一步说,emoji表情实际上是一组Unicod ...

  3. emoji 乱码_这个自制emoji的网站,让你成为永远不输的斗图王者

    作为表情界的元老级人物,不管是苹果官网输入法.微信官方表情还是各个主流输入法里,我们都可以从里面找到大量 emoji 表情. 然鹅--就算这么多表情,小帮每次发 emoji 时还有有些选择困难. 因为 ...

  4. emoji mysql 转 unicode_unicode和emoji编码

    研究Hprose的windows编译时,发现它使用了emoji编码.emoji需要utf-32的字符才能存下.(此句不准确,应该是至少4个字节才能存放).所以有说法用mysql存emoji时,需要使用 ...

  5. java emoji编码转换_java转换emoji表情

    /** * @Description 将字符串中的emoji表情转换成可以在utf-8字符集数据库中保存的格式(表情占4个字节,需要utf8mb4字符集) * @param str * 待转换字符串 ...

  6. java emoji显示乱码_Java 解决Emoji表情过滤问题

    Emoji表情从三方数据中获取没有过滤,导致存入DB的时候报错. 原因: UTF-8编码有可能是两个.三个.四个字节.Emoji表情是4个字节,而Mysql的utf8编码最多3个字节,所以数据插不进去 ...

  7. excel emoji php,PHP导出带有emoji表情的文本到excel文件出问题了

    前段时间做了一个导出用户信息(包含微信昵称)到excel文件的功能,一直没问题,今天突然有人反馈说导出来的数据有一些丢失了.我试了一下,发现有些数据导出没问题,有些有问题,某些列出现了空白,数据打印出 ...

  8. emoji表情mysql处理_Mysql Emoji表情处理

    1.转码存储 将内容转码存储 如:转成 Unicode 存储,取出来时在解码 2.过滤 Emoji PHP function filterEmoji($str) { $str = preg_repla ...

  9. 匹配表情emoji 正则_php正则表达式过滤emoji表情符号

    php正则表达式过滤emoji表情符号2017-08-23 21:41 现在中国有手机的人用微信的应该占了90%吧!小编没事的时候也是天天玩微信. 不过不是发朋友圈这些,而是上面有很多感兴趣的文章. ...

  10. html icon 引入emoji,给自己网站增加Emoji表情图标(可以在百度显示类

    百度搜索引擎中能够显示ICO图标,一来说明建站时间比较长,二来可以看得出那站内权重相对不低,但是现在很多新站建立以后都不会出现ICO图标了,包括卢松松博客和我的个人博客都没有出现ICO图标,那么今天我 ...

最新文章

  1. jq修改iframe html代码,使用jQuery替换iframe的所有内容(包括doctype和html标签)
  2. python批量做线性规划(每次的约束条件参数有变换)
  3. Mysql 从库跳过
  4. 推荐一个高质量的git命名查询和学习的github仓库git-recipes
  5. wifi定位算法android,WIFI定位算法
  6. vue中解决three.js出现内存泄漏丢失上下文问题
  7. html5音乐播放器在线生成,一款极简的HTML5音乐播放器-skPlayer
  8. cortex a7 a53_镜头测试:蔡司红T28/2.8+索尼微单A7实拍北京景山公园
  9. 浪潮4U服务器 raid5 直通(JBOD)
  10. 改进平滑滚动,修改音量调节级数实现音量微调【编译自XDA 适用于大部分设备】
  11. Ubuntu简单使用操作
  12. 什么是抽象方法 java_java抽象类和抽象方法
  13. php 热敏打印,从PHP打印到POS打印机
  14. 关于emplace_back()的理解
  15. 王唯佳被南开计算机学院录取,662分!庞贝病少年王唯佳被南开大学录取
  16. ReentrantLock 原理(源码轰炸)
  17. 010 Editor算法逆向与编写注册机
  18. 支付宝:支付宝里面国际驾照认证件的申请领取步骤
  19. [BZOJ3110~3115]ZJOI2013
  20. 宝付受邀参加图书馆计划十周年庆典

热门文章

  1. vue 个人头像修改
  2. 《人月神话》-第19章-20年后的《人月神话》
  3. hmcl离线模式可以联机吗_hmcl启动器怎么联机-hmcl启动器联机方法介绍
  4. android反编译软件Mac,在Mac上进行安卓反编译
  5. 在线Javascript美化格式化工具
  6. vmstat 命令详解
  7. 小米商城官网部分代码
  8. linux opendir路径_Linux下目录文件的操作(opendir,readdir,closedir) 以及DIR,dirent,stat等结构体详解...
  9. CDA LEVELII考试内容记录学习--目录篇
  10. 机器学习笔记之概率图模型(五)马尔可夫随机场的结构表示