Reduce hardcoded special case in EmojiModifier

This commit is contained in:
Rocka 2026-06-07 18:23:47 +08:00
parent 9409cb1112
commit d0364b8387
No known key found for this signature in database
GPG Key ID: 28031158FFDD6853
2 changed files with 41 additions and 25 deletions

View File

@ -47,7 +47,7 @@ class PickerPagesAdapter(
val list = if (textPaint != null) {
arr.filter { textPaint.hasGlyph(it) }
} else {
arr.toList()
arr.asList()
}
val chunks = list.chunked(density.pageSize)
categories.add(cat to IntRange(pages.size, pages.size + chunks.size - 1))

View File

@ -5,15 +5,16 @@
package org.fcitx.fcitx5.android.input.popup
import android.annotation.SuppressLint
import android.icu.lang.UCharacter
import android.icu.lang.UProperty
import android.icu.text.UnicodeSet
import android.os.Build
import android.text.TextPaint
import androidx.annotation.RequiresApi
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceEnum
import org.fcitx.fcitx5.android.utils.includes
object EmojiModifier {
@ -29,16 +30,12 @@ object EmojiModifier {
/**
* **Special Case 1:** Drop `U+FE0F` (Variation Selector-16) when combining with skin tone
*/
private val SpecialCase1 = intArrayOf(
0x261D, // ☝️
0x26F9, // ⛹️
0x270C, // ✌️
0x1F3CB, // 🏋️
0x1F3CC, // 🏌️
0x1F574, // 🕴️
0x1F575, // 🕵️
0x1F590, // 🖐️
)
@RequiresApi(Build.VERSION_CODES.P)
private fun shouldSkipVariationSelector16(ch: Int): Boolean {
return UCharacter.hasBinaryProperty(ch, UProperty.EMOJI_MODIFIER_BASE)
&& !UCharacter.hasBinaryProperty(ch, UProperty.EMOJI_PRESENTATION)
}
private const val VariationSelector16 = 0xFE0F
/**
@ -56,7 +53,7 @@ object EmojiModifier {
}
private fun isModifiable(modifiable: BooleanArray): Boolean {
val sum = modifiable.sumOf { if (it) 1 else 0 }
val sum = modifiable.count { it }
// bail if too crowded
// eg. https://emojipedia.org/family-man-medium-light-skin-tone-woman-medium-light-skin-tone-girl-medium-light-skin-tone-boy-medium-light-skin-tone
return sum == 1 || sum == 2
@ -75,40 +72,59 @@ object EmojiModifier {
return codePoints to modifiable
}
@RequiresApi(Build.VERSION_CODES.P)
private fun buildEmoji(codePoints: IntArray, modifiable: BooleanArray, tone: SkinTone): String {
return buildString {
for (i in 0..<codePoints.size) {
// skip U+FE0F if the preceding character is special
if (i > 0 && codePoints[i] == VariationSelector16 &&
SpecialCase1.includes(codePoints[i - 1]) &&
tone != SkinTone.Default
) continue
var i = 0
while (i < codePoints.size) {
appendCodePoint(codePoints[i])
if (modifiable[i]) {
append(tone.value)
if (tone != SkinTone.Default &&
codePoints.getOrNull(i + 1) == VariationSelector16 &&
shouldSkipVariationSelector16(codePoints[i])
) i++
}
i++
}
}
}
private val DefaultTextPaint = TextPaint()
private val DefaultTextPaint by lazy {
TextPaint()
}
private val RGIEmojiSet by lazy {
@SuppressLint("NewApi")
UnicodeSet("[:RGI_Emoji:]").freeze()
}
private fun isValidEmoji(emoji: String): Boolean {
// UProperty.RGI_EMOJI is available on 34+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
if (!RGIEmojiSet.contains(emoji)) return false
}
return DefaultTextPaint.hasGlyph(emoji)
}
fun getPreferredTone(emoji: String): String {
if (!isSupported()) return emoji
val (codePoints, modifiable) = getCodePoints(emoji)
if (!isModifiable(modifiable)) return emoji
val candidate = buildEmoji(codePoints, modifiable, defaultSkinTone)
return if (DefaultTextPaint.hasGlyph(candidate)) candidate else emoji
val tone = defaultSkinTone
if (tone == SkinTone.Default || !isModifiable(modifiable)) return emoji
val candidate = buildEmoji(codePoints, modifiable, tone)
return if (isValidEmoji(candidate)) candidate else emoji
}
fun produceSkinTones(emoji: String): Array<String>? {
if (!isSupported()) return null
val (codePoints, modifiable) = getCodePoints(emoji)
val tone = defaultSkinTone
if (!isModifiable(modifiable)) return null
val candidates = SkinTone.entries
.filter { it != defaultSkinTone }
.filter { it != tone }
.map { buildEmoji(codePoints, modifiable, it) }
.filter { DefaultTextPaint.hasGlyph(it) }
.filter { isValidEmoji(it) }
return if (candidates.isEmpty()) null else candidates.toTypedArray()
}
}