Android Emoji 最佳实践

3,868

Introduction

由于 android 系统的碎片化,导致了最新的 emoji 在老的手机上不支持。
微信也做得不好,很多 emoji 表情直接变成了 X ,不知道什么意思。
解铃还须系铃人, Android 官方为此做了不少努力。

Android Official Solution

EmojiCompat 解决方案

EmojiCompat 的方案包含如下两种方式

  • Downloadable fonts configuration (网络下载)
  • Bundled fonts configuration (本地打包)把一个 7.4M 的 ttf 文件塞进 apk 中

这两种方式都有缺陷:

  • 网络下载方式依赖于翻墙,门槛比较高
  • 本地打包方式对 apk 的体积增大不可接受

有没有一种方式既不用翻墙又不增大 apk 的体积呢?

答案是有的

Best Practice

无论是网络下载还是本地打包,都涉及到读取 font 文件进行渲染,那么这里有两步

  • 接管读取的流程
  • 接管渲染的流程

Hook Loading Process

首先,我们在 gradle 中导入如下依赖

compile "com.android.support:support-emoji:$version"
compile "com.android.support:support-emoji-appcompat:$version"
compile "com.android.support:support-emoji-bundled:$version"

这三个依赖所包含的东西不同

  • support-emoji 包含 EmojiCompat 方案所需要的基本实现以及 Downloadable fonts configuration 的代码
  • support-emoji-appcompat 主要是针对 AppCompat 的 UI 组件的支持
  • support-emoji-bundled 是 Bundled fonts configuration 的代码

然后根据官网解决方案,编译打包成 apk 后,解压 apk ,取出 NotoColorEmojiCompat.ttf 文件(建议使用最新版本的 support 包) 放着备用,一会儿下锅(宽油警告?),哦不,上传到七牛或者其他云存储

接下来就不需要 support-emoji-bundled 了,也不需要 support-emoji ,因为 support-emoji-appcompat 包含了

新建一个类

class DownloadEmojiCompatConfig : EmojiCompat.Config(DownloadMetadataLoader()) {
    private class DownloadMetadataLoader internal constructor() : EmojiCompat.MetadataRepoLoader {
        @RequiresApi(19)
        override fun load(loaderCallback: EmojiCompat.MetadataRepoLoaderCallback) {
            // 这里只是关键部分,并不表示只有这一行
            loaderCallback.onLoaded(MetadataRepo.create(Typeface.createFromFile(file.absolutePath), FileInputStream(file)))
        }
    }
}

其中 file 就是我们下载好的 ttf 文件

注意事项:

  • 整个 EmojiCompat 只适用于 4.4 以上,再往下的就不支持了(也够用了)
  • 既然是想通过下载来解决本地安装包增大的问题,那么就好考虑好 wifi 4g 等情况了, 7M 流量呢

然后就是 EmojiCompat 自己的初始化流程了

EmojiCompat.init(
        DownloadEmojiCompatConfig()
                .setReplaceAll(false)
                .registerInitCallback(object : EmojiCompat.InitCallback() {
                    override fun onInitialized() {
                        emojiReady = true
                    }

                    override fun onFailed(throwable: Throwable?) {
                        emojiReady = false
                    }
                })

注意事项:

  • 放到其他线程来做,这个初始化时间至少 150ms
  • 利用 InitCallback 回调来处理 ready 状态, ready 了后面渲染才可以用
  • 小于 4.4 就不要初始化了
  • setReplaceAll 方法这里重点说明下
    • true 表示所有的 emoji 都替换为 ttf 里的,这样可以保证 emoji 风格统一
    • false 表示优先使用系统支持的 emoji ,不支持才用 ttf 里的

此处使用 setReplaceAll(false) 是因为 ttf 里的 emoji 在渲染国旗的时候会出现问题(也包括中国国旗)

到这里,完成了 EmojiCompat 的 loading 流程

Hook Rendering Process

Activity 的 LayoutInflaterCompat.setFactory2 替换 xml 里的 View

新写 EmojiTextView 继承 AppCompatTextView

@Override
public void setText(CharSequence text, BufferType type) {
    if (emojiReady) {
        super.setText(EmojiCompat.get().process(text), type);
        return;
    }
    super.setText(text, type);
}

EditText 就直接替换为 support-emoji-appcompat 里的 EmojiAppCompatEditText 即可

到这里,完成了 EmojiCompat 的 rendering 流程


事情到这里就结束了么?并没有,因为新的 emoji 一直会出来,而 android 的 support 包并不会及时更新

那怎么办,有一个开源的组织叫 emojione 这里 是它的官网

这个组织会发布自己的一套 emoji ttf 来兼容各种设备和统一不同风格的 emoji

也别高兴得太早,上一次更新是 v3 版本,落后于 Android 提供的 support 包的版本(就是支持的 emoji 比官方少)

不过,最近,它出了 v4 了,可以期待下,但目前 ttf 版本还是 coming soon 状态,可以订阅下以便及时收到邮件提醒

Job Ad

欢迎正在刷 即刻app 的你加入我们,一起参与千万级用户产品的研发和运营!

招聘岗位列表

当然也包括 Android 职位啦

工作地点:上海市杨浦区创智天地
简历请发送至 hr@okjike.com 并在邮件标题中注明职位 + 姓名

一起打造明天的即刻 (^U^)ノ~YO

补一下 DownloadEmojiCompatConfig 的源码

class DownloadEmojiCompatConfig : EmojiCompat.Config(MetadataLoader()) {

    private class MetadataLoader internal constructor() : EmojiCompat.MetadataRepoLoader {

        private val nTtfFile: File = File(StoreUtil.internalFileDir, EMOJI_FILE_NAME)

        override fun load(loaderCallback: EmojiCompat.MetadataRepoLoaderCallback) {
            Observable.create<Any> {
                try {
                    if (nTtfFile.exists() && nTtfFile.isDirectory) {
                        clearEmoji()
                    }

                    if (!nTtfFile.exists()) {
                        val downloadSuccess = FileUtil.downloadFile(CdnRes.ILLUSTRATION_EMOJI, nTtfFile)
                        if (!downloadSuccess) {
                            throw Exception("download ttf failed")
                        }
                    }

                    loaderCallback.onLoaded(MetadataRepo.create(Typeface.createFromFile(nTtfFile), FileInputStream(nTtfFile)))
                    it.onNext(Unit)
                    it.onComplete()
                } catch (t: Throwable) {
                    loaderCallback.onFailed(t)
                    it.onError(t)
                }
            }
                    .compose(RxUtil.io())
                    .subscribe()
        }
    }

    companion object {

        private const val EMOJI_FILE_NAME = "emoji.ttf"

        private const val TAG = "emoji"

        /**
         * 删除旧的 emoji 文件
         */
        @JvmStatic
        fun clearEmoji() {
            File(StoreUtil.internalFileDir, EMOJI_FILE_NAME).deleteRecursively()
        }
    }

}