为什么复杂的表情在 Android canvas 上绘制时不合并而是拆分?

Why are complex emojis not merged but split up when drawn on Android canvas?

我想为我的键盘应用程序键盘设计器实现表情符号选择器。为此,我想基于十六进制格式的 unicode 绘制表情符号。当我在文本字段中写入表情符号“\u1F636\u200D\u1F32B\uFE0F”时它正确显示(云后面的两只眼睛),但是当我在 canvas 上绘制它时,它看起来像两个单独的表情符号(左上角)键盘一角):

提示:心与问题无关,只标记喜欢的表情符号

我的源代码嵌套在对象和方法中,但为了向您展示它是如何工作的,我将其更改为线性命令:

ArrayList<char[]> utf16Chars = new ArrayList<>();
String hexCode = "\u1F636\u200D\u1F32B\uFE0F";
int distance;
if (hexCode.startsWith("\")) {
    for (int i = 0; i < hexCode.length(); i += distance) {
        distance = hexCode.indexOf("\", i + 1) - i;
        if (distance < 0)
            distance = hexCode.length() - i;
        String utf16Code = hexCode.substring(i, i + distance);
        int decimalCode = Integer.parseInt(utf16Code.length() >= 6 ? utf16Code.substring(2) : utf16Code, 16);
        char[] utf8Chars = Character.toChars(decimalCode);
        utf16Chars.add(utf8Chars);
    }
}
StringBuilder stringBuilder = new StringBuilder();
for(char[] utf8Chars : utf16Chars)
    for(char character : utf8Chars)
        stringBuilder.append(character);

String emoji = stringBuilder.toString();
canvas.drawText(emoji, 0, emoji.length(), x, y, paint);

有人知道我做错了什么吗?

更新

另见 this bug report

Face in Clouds Emoji was added in Emoji Version 13.1 并且可能会在 Android 的更高版本中普遍可用。我在下面的回答适用于 API 31 但不适用于 API 30 或更早。该解决方案存在向后兼容性问题。

有一个class EmojiCompat可以提供兼容性,可以在API 31之前的Android版本上显示“云中的脸”表情符号。

我整理了一份EmojiCompat project based on the EmojiCompat app in Google's user-interface-samples。关于这个演示的几个注意事项:

  1. EmojiCompatApplication class 有一些重要的设置。

  2. androidx.emoji1.1.0版本的依赖:emoji-bundled更新为1.2版本。 0-alpha03。如果没有此更新,“云中的脸”表情符号将显示为两个表情符号,而不是一个。随着新表情符号的发布(我认为每年一次),这个库将需要更新。我相信另一种选择是使用可下载的表情符号字体,但我不会在这里讨论可下载的字体。

  3. MainActivity 中,我保留了 Google 项目中的所有内容,只是我添加了对“MyView”的处理,它创建了一个 StaticLayout 并使用我之前的解决方案中指定的 Layout.draw(canvas) 显示内容,这是 OP 所要求的。 Canvas.drawText()还是心灰意冷

这是模拟器上演示应用程序的输出 运行 API 24:

这比我最初想象的要复杂得多,我在网上找不到好的教程。也许有人知道并可以推荐它。



我使用以下简单代码创建了组合表情符号。

//    val cloudy = "\u1F636\u200D\u1F32B\uFE0F"
val cloudyFace = intArrayOf(0x1F636, 0x200D, 0x1F32B, 0xFE0F)
val sb = StringBuilder()
for (i in 0 until cloudyFace.size) {
    sb.append(getUtf16FromInt(cloudyFace[i]))
}
binding.textView.text = sb.toString()

fun getUtf16FromInt(unicode: Int) = String(Character.toChars(unicode))

不要使用 Canvas.drawText(),而是使用 layout.draw(canvas),其中 layoutStaticLayout。来自文档:

This is used by widgets to control text layout. You should not need to use this class directly unless you are implementing your own widget or custom display object, or would be tempted to call Canvas.drawText() directly.

底线:不要使用 Canvas.drawText().

如果 BoringLayout class 更适合您的需要,您也可以使用它。