为您的应用提供完备的 Emoji 支持

在移动互联网如此发达的今天,Emoji 已无处不在,并成为我们日常交流中不可或缺的一部分。据统计,Emoji 的使用率在过去 10 年内不断攀升,2021 年更是达到了历史新高,每天有超过五分之一的推文中包含了 Emoji,一些应用上的用户每天发送的 Emoji 数量更是达到了数十亿。然而用户在 Android 平台上使用 Emoji 时却存在着一些问题,本文将针对这些问题进行探讨,并向您介绍 Emoji 的工作原理以及 Android 平台近期关于 Emoji 的更新。

如果您更喜欢通过视频了解本文内容,请点击下方:

△ 为您的应用提供完备的 Emoji 支持

Emoji 在 Android 平台的现状

Emoji 在 Android 平台存在的问题

Unicode 每年都会对 Emoji 标准进行更新,用户越来越频繁地使用各种 Emoji,但却存在有约 96% 的 Android 用户无法正确查看新发布 Emoji 的问题,而 iOS 平台只有 16%,这一比例明显高出许多。

另外根据统计排名,前 100 位的 Emoji 占据了所有用户日常使用 Emoji 总量的 82%,但在约 20% 的情况下,当用户发送一个 Emoji 后,对方看到的却是豆腐块或一张损坏的图像,这种情况直接导致用户无法正确通过 Emoji 来传递自己的本意。如下图所示,用户发送了一张含泪的笑脸,但对方却只收到一个中间有 X 的方块 (我们称其为豆腐块)。

△ Emoji 在发送和接受方的不同显示效果

△ Emoji 在发送和接受方的不同显示效果

随着用户的增多,Unicode 也在不断增加新的 Emoji 来体现多元化和包容性,但是 Android 却并不能完全兼容这些新版的 Emoji,不同的 Android 版本对 Emoji 的支持程度也不同。比如以下的几个例子:

△ Emoji 无法在不同 Android 版本间正确表达多样性和包容性

△ Emoji 无法在不同 Android 版本间正确表达多样性和包容性

  1. 在 Android 7.0 Nougat 和更早版本上无法正确通过 Emoji 表示肤色。用户发送了一个表示深肤色手臂的 Emoji,但对方收到的却是一个手臂和深色方块的分解版本。
  2. 在 Android 8.0 Oreo 和更早版本上无法正确显示代表中性的 Emoji。
  3. 在 Android 9 Pie 和更早版本上不支持显示多人多肤色的 Emoji。

Android 平台针对 Emoji 的解决方案

以上问题显然会导致非常糟糕的用户体验,并且不利于用户之间通过 Emoji 进行交流。我们的目标就是确保所有 Android 用户无论是使用哪种应用,都能够正常地使用每个 Emoji。为此,从 Android 12 开始,我们引入了可更新系统字体 (Updatable System Fonts),首先引入的便是 Emoji 字体,这也意味着开发者再也不需要在搭载 Android 12 以及 12 以上版本的设备中考虑 Emoji 适配的问题了,系统将默认支持新的 Emoji。

但是考虑到时光机还没有被发明出来,我们也没办法穿越到过去把可更新系统字体塞到旧版本的设备中去。如果一直等待时光机被发明出来而什么也不做的话,Android 12 版本之前的设备就会一直显示豆腐块,或者以其他错误的方式进行渲染。因此我们还是做了些改进,让您可以通过更新 Jetpack 库 EmojiCompat 解决这一问题。

EmojiCompat 早在几年前就已经发布,2021 年我们对其做了很多改进,并将其整合至 AppCompat 1.4 版本 中。为此我们开发了一个新库 androidx.emoji2,并添加了自动配置选项,它可自行配置以加载正确的字体。我们将这个库集成到了 AppCompat 1.4 中,也就是说,您仅需升级至 AppCompat 1.4 版本,便可在 API 19 及更高的版本上正常显示新加入的 Emoji,开箱即用,无需任何额外配置。

AppCompat 1.4 针对 Emoji 的优化

△ Emoji2 和 Emoji 对比

△ Emoji2 和 Emoji 对比

Emoji2 库是 AppCompat 库的一个新的依赖项,虽然它会代替现有的 androidx.emoji 库,但是 API 几乎相同。在此次更新中,我们使用 androidx.startup 添加了新的初始化程序 (EmojiCompatInitializer),添加了新的默认配置,并且全部支持了 nullability 注解。另外,相较于 androidx.emoji,我们还删除了一些在使用 AppCompat 时不再需要的 TextView 子类,这使得在 RA 之后节省了约 14KB 的大小。

△ Emoji2 加载 Emoji 的步骤

△ 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

在 AppCompat 中,我们将 Emoji2 集成到了所有的视图中,这意味着所有视图都可以支持新版 Emoji,如果您的 Activity 继承了 AppCompatActivity,在 XML 中直接使用 TextView 或 EditView 即可。AppCompatActivity 安装了一个布局填充器 (LayoutInflater),它会用 AppCompatTextView 来替换 TextView 等视图,在代码中无论何时创建 TextView,都应该确保创建的是 AppCompatTextView,并且自定义视图应该继承相应的 AppCompat 子类。

所有集成了 Emoji2 的视图都有一个 EmojiCompatEnabled 属性,通过它可以控制是否开启 EmojiCompat,该属性还提供了 getter 和 setter 方法。EmojiCompatEnabled 属性有助于在知道文本绝不可能包含 Emoji 的情况下,来规避执行 Emoji 的处理逻辑,虽然即使不规避该逻辑所带来的成本也是极低的,但在某些情况下每一纳秒都至关重要,此属性便是为了支持这种情况。另外,该属性对于在后台线程上处理 Emoji 也很有帮助,AppCompat 对于 Emoji2 的集成会在 setText 之后的适当时间调用 EmojiCompat.process,您可以通过 EmojiCompatEnabled 属性禁用此方法调用,并对 Emoji 的处理移至后台线程。但通常这种优化没必要,除非是在 RecyclerView 中展示大量的文本导致卡顿,那么可以考虑采用这一优化方案。

测试新版 Emoji

由于集成了可下载字体,对于测试新版的 Emoji 并不是那么容易。要创建一个不会导致误报或漏报的通用自动化测试库很难,而大多数开发者在实际情况下会直接手动测试 EmojiCompat 的集成,因此最好的选择还是使用一个记录了用于测试的 Emoji 列表,同样此方式对于手动测试或对屏幕截图进行测试也都非常有用。

如果您希望了解更多信息,请查看文档: 支持新式表情符号。我们在文档中为您提供了一些关于配置测试模拟器和设备所需要的一些操作建议:

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” 的方式完全相同,都是查找字体文件后在屏幕上绘制出来。

△ 通过 NotoColorEmoji 对 Emoji 字符进行绘制

△ 通过 NotoColorEmoji 对 Emoji 字符进行绘制

在 Android 12 及以上版本中,平台可以确保 Emoji 会正常显示,因为可更新系统字体会将新版 Emoji 添加到字体文件中。但对于 Android 12 之前的版本,我们没有任何方法可以更新字体,这意味着 Android 不知道用什么字体来绘制 “融化脸”,此时它会改为绘制一个称为豆腐块的备用字形。这里就是 Emoji2 开始大展身手的地方了。

△ Emoji2 对 Emoji 字符的绘制过程

△ Emoji2 对 Emoji 字符的绘制过程

在将字符串发送到 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 只是在字符串中间绘制了一张图片,并没有别的特殊操作。

总结

本文通过分析 Emoji 在 Android 平台存在的问题,针对不同版本的 Android 系统介绍了两种解决方案:

  • Android 12 及以上的版本使用可更新系统字体,无需开发者手动适配;
  • Android 12 以下的版本集成 AppCompat 1.4 也可自动适配新版 Emoji,无需额外操作。

此外,我们还介绍了 Emoji 的渲染原理,让您更进一步了解 Emoji 是如何呈现在屏幕上的。请记得升级 AppCompat 到 1.4 版本,为用户提供支持新版 Emoji 的最佳体验。

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!