在移动互联网如此发达的今天,Emoji 已无处不在,并成为我们日常交流中不可或缺的一部分。据统计,Emoji 的使用率在过去 10 年内不断攀升,2021 年更是达到了历史新高,每天有超过五分之一的推文中包含了 Emoji,一些应用上的用户每天发送的 Emoji 数量更是达到了数十亿。然而用户在 Android 平台上使用 Emoji 时却存在着一些问题,本文将针对这些问题进行探讨,并向您介绍 Emoji 的工作原理以及 Android 平台近期关于 Emoji 的更新。
如果您更喜欢通过视频了解本文内容,请点击下方:
Emoji 在 Android 平台的现状
Emoji 在 Android 平台存在的问题
Unicode 每年都会对 Emoji 标准进行更新,用户越来越频繁地使用各种 Emoji,但却存在有约 96% 的 Android 用户无法正确查看新发布 Emoji 的问题,而 iOS 平台只有 16%,这一比例明显高出许多。
另外根据统计排名,前 100 位的 Emoji 占据了所有用户日常使用 Emoji 总量的 82%,但在约 20% 的情况下,当用户发送一个 Emoji 后,对方看到的却是豆腐块或一张损坏的图像,这种情况直接导致用户无法正确通过 Emoji 来传递自己的本意。如下图所示,用户发送了一张含泪的笑脸,但对方却只收到一个中间有 X 的方块 (我们称其为豆腐块)。
△ Emoji 在发送和接受方的不同显示效果
随着用户的增多,Unicode 也在不断增加新的 Emoji 来体现多元化和包容性,但是 Android 却并不能完全兼容这些新版的 Emoji,不同的 Android 版本对 Emoji 的支持程度也不同。比如以下的几个例子:
△ Emoji 无法在不同 Android 版本间正确表达多样性和包容性
Android 平台针对 Emoji 的解决方案
https://developer.android.google.cn/guide/topics/ui/look-and-feel/emoji-compat
EmojiCompat 早在几年前就已经发布,2021 年我们对其做了很多改进,并将其整合至 AppCompat 1.4 版本中。为此我们开发了一个新库 androidx.emoji2,并添加了自动配置选项,它可自行配置以加载正确的字体。我们将这个库集成到了 AppCompat 1.4 中,也就是说,您仅需升级至 AppCompat 1.4 版本,便可在 API 19 及更高的版本上正常显示新加入的 Emoji,开箱即用,无需任何额外配置。
AppCompat 1.4 针对 Emoji 的优化
△ Emoji2 和 Emoji 对比
Emoji2 库是 AppCompat 库的一个新的依赖项,虽然它会代替现有的 androidx.emoji 库,但是 API 几乎相同。在此次更新中,我们使用 androidx.startup 添加了新的初始化程序 (EmojiCompatInitializer),添加了新的默认配置,并且全部支持了 nullability 注解。另外,相较于 androidx.emoji,我们还删除了一些在使用 AppCompat 时不再需要的 TextView 子类,这使得在 RA 之后节省了约 14KB 的大小。
△ Emoji2 加载 Emoji 的步骤
在这次更改中,一大新特性便是 EmojiCompatInitializer,它是一个使用了 androidx.startup 库的初始化程序,在应用启动时会自动配置 EmojiCompat。我们已对该初始化程序的性能进行了大量的调整,对于大多数应用来说使用默认配置已经完全足够,但如果您需要对应用启动做纳秒级别的优化,则可以考虑移除 startup 库和创建线程所带来的消耗。首先,确保先初始化 EmojiCompat,再执行 Activity.onCreate,这可以保证每个 TextView 都能显示新版 Emoji。然后,可以像 EmojiCompatInitializer 一样将 Emoji 字体加载延迟到首屏绘制之后,这样做是因为虽然加载过程是在后台线程中进行的,但它还是执行大量的网络和磁盘 I/O 操作,这些操作会同首屏加载一起抢夺资源。
这里再次强调,我们已对该初始化程序的性能进行了大量的调整和优化,除非必要,请使用 EmojiCompatInitializer 的默认实现。
// androidx. emoji
FontRequest fontRequest = new FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",
"Noto Color Emoji Compat",
R.array.gms_fonts_certs);
EmojiCompat.init(
new FontRequestEmojiCompatConfig(
context(), fontRequest));
△ 使用 androidx.emoji 时的模版配置
另一个重要的功能是默认配置,在 androidx.emoji 中您需要从文档的示例代码中复制一些模版配置 (类似于以上代码) 到应用中,而 Emoji2 中我们添加了可以直接用于 EmojiCompatInitializer 的 DefaultEmojiCompatConfig,如下代码所示,只需一行简单配置即可,当然也支持手动配置的需求。
// androidx.emoji2
DefaultEmojiCompatConfig
.create(context)
△ androidx.emoji2 的 DefaultEmojiCompatConfig
测试新版 Emoji
由于集成了可下载字体,对于测试新版的 Emoji 并不是那么容易。要创建一个不会导致误报或漏报的通用自动化测试库很难,而大多数开发者在实际情况下会直接手动测试 EmojiCompat 的集成,因此最好的选择还是使用一个记录了用于测试的 Emoji 列表,同样此方式对于手动测试或对屏幕截图进行测试也都非常有用。
如果您希望了解更多信息,请查看文档: 支持新式表情符号。我们在文档中为您提供了一些关于配置测试模拟器和设备所需要的一些操作建议:
https://developer.android.google.cn/guide/topics/ui/look-and-feel/emoji2
Emoji 渲染原理
△ 一组码点
Emoji 属于一种图形字符,是字符串的一部分。它就像字母 "I" 一样,只是绘制方式和从属的字体文件不同而已。但是对于计算机来说,它并不会特意关心什么是 Emoji 或字母 "I",一个字符串本质上就只是一组码点,其中的数字通过 Unicode 进行分配,代表着计算机上会出现的每一个字符。
现在 Unicode 并不仅仅只是一种格式了,它还代表了制定该标准的委员会,委员会会决定一些事情,比如数字 7 代表字母 "I" (实际上 7 并非真正代表字母 I 的码点,此处仅仅是举个例子)。那么当您试图在 Android 上渲染上述表示字符串的码点时会发生什么呢?
首先,Android 会根据码点和应用要求使用的字体样式为每个字符找出最佳字体。当前 Android 上非斜体且正常粗细 "V" 的默认字体是 roboto-regular.ttf,Android 会对字符串进行遍历,检查每个字符并查找最佳字体。它会检查码点和样式,您可以对字符串进行样式的定制操作,比如对一些字符进行加粗等等。对于上述简单的字符串来说,它就只是会选择 roboto-regular.ttf 字体。
△ 遍历码点查找正确的字符串
但是,当遇到 Emoji 字符时,您可能会觉得它会进行完全不同的渲染方式,毕竟它看起来不像任何其他的字母。但实际上,Emoji 就是个文本,由码点表示,同字母 "I" 和 "I" 一样没什么区别,绘制它的方式就存储到了字体中。Android 会首先尝试在字体中查找无斜体且正常粗细的 "融化脸",但这一次发现在 roboto-regular 中并没有想要的结果,便会去 NotoColorEmoji 中进行查找,这是 AOSP 上预装的 Emoji 字体,它包含了每个 Emoji 的图像,在 Android 平台上通过这种字体绘制 Emoji 和绘制字母 "I" 的方式完全相同,都是查找字体文件后在屏幕上绘制出来。
在 Android 12 及以上版本中,平台可以确保 Emoji 会正常显示,因为可更新系统字体会将新版 Emoji 添加到字体文件中。但对于 Android 12 之前的版本,我们没有任何方法可以更新字体,这意味着 Android 不知道用什么字体来绘制 "融化脸",此时它会改为绘制一个称为豆腐块的备用字形。这里就是 Emoji2 开始大展身手的地方了。
在将字符串发送到 Android 系统之前,在字符串上会调用 EmojiCompat.process 方法,此调用将遍历并查找那些系统不知道如何绘制的 Emoji,并为每个 Emoji 添加一个 EmojiSpan,这是一个替换 Span,这意味着它将只替换该段字符串中对应的内容。系统会直接使用 roboto-regular.ttf 正常绘制,但当找到 EmojiSpan 时它会将绘制权转交给 Span。
在该 Span 中 Android 使用了两个方法,首先,它会获取字符尺寸并告诉 Android 要在文本布局中为此 Span 保留多少空间,然后,当需要绘制字符串时,它将调用 EmojiSpan 上的 draw 而非自行绘制。在 EmojiSpan 中,它知道 Compat 版的 Emoji 字体位置,并能直接从中绘制出 "融化脸"。再返回到渲染阶段,平台将调用 EmojiSpan.draw,整个区域将由 EmojiSpan 进行绘制,而非平台。实际上,从平台的角度来看 EmojiSpan 只是在字符串中间绘制了一张图片,并没有别的特殊操作。
总结
点击屏末 | 阅读原文 | 即刻了解表情符号兼容性