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) { val list = if (textPaint != null) {
arr.filter { textPaint.hasGlyph(it) } arr.filter { textPaint.hasGlyph(it) }
} else { } else {
arr.toList() arr.asList()
} }
val chunks = list.chunked(density.pageSize) val chunks = list.chunked(density.pageSize)
categories.add(cat to IntRange(pages.size, pages.size + chunks.size - 1)) 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 package org.fcitx.fcitx5.android.input.popup
import android.annotation.SuppressLint
import android.icu.lang.UCharacter import android.icu.lang.UCharacter
import android.icu.lang.UProperty import android.icu.lang.UProperty
import android.icu.text.UnicodeSet
import android.os.Build import android.os.Build
import android.text.TextPaint import android.text.TextPaint
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.data.prefs.AppPrefs import org.fcitx.fcitx5.android.data.prefs.AppPrefs
import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceEnum import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceEnum
import org.fcitx.fcitx5.android.utils.includes
object EmojiModifier { object EmojiModifier {
@ -29,16 +30,12 @@ object EmojiModifier {
/** /**
* **Special Case 1:** Drop `U+FE0F` (Variation Selector-16) when combining with skin tone * **Special Case 1:** Drop `U+FE0F` (Variation Selector-16) when combining with skin tone
*/ */
private val SpecialCase1 = intArrayOf( @RequiresApi(Build.VERSION_CODES.P)
0x261D, // ☝️ private fun shouldSkipVariationSelector16(ch: Int): Boolean {
0x26F9, // ⛹️ return UCharacter.hasBinaryProperty(ch, UProperty.EMOJI_MODIFIER_BASE)
0x270C, // ✌️ && !UCharacter.hasBinaryProperty(ch, UProperty.EMOJI_PRESENTATION)
0x1F3CB, // 🏋️ }
0x1F3CC, // 🏌️
0x1F574, // 🕴️
0x1F575, // 🕵️
0x1F590, // 🖐️
)
private const val VariationSelector16 = 0xFE0F private const val VariationSelector16 = 0xFE0F
/** /**
@ -56,7 +53,7 @@ object EmojiModifier {
} }
private fun isModifiable(modifiable: BooleanArray): Boolean { private fun isModifiable(modifiable: BooleanArray): Boolean {
val sum = modifiable.sumOf { if (it) 1 else 0 } val sum = modifiable.count { it }
// bail if too crowded // 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 // 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 return sum == 1 || sum == 2
@ -75,40 +72,59 @@ object EmojiModifier {
return codePoints to modifiable return codePoints to modifiable
} }
@RequiresApi(Build.VERSION_CODES.P)
private fun buildEmoji(codePoints: IntArray, modifiable: BooleanArray, tone: SkinTone): String { private fun buildEmoji(codePoints: IntArray, modifiable: BooleanArray, tone: SkinTone): String {
return buildString { return buildString {
for (i in 0..<codePoints.size) { var i = 0
// skip U+FE0F if the preceding character is special while (i < codePoints.size) {
if (i > 0 && codePoints[i] == VariationSelector16 &&
SpecialCase1.includes(codePoints[i - 1]) &&
tone != SkinTone.Default
) continue
appendCodePoint(codePoints[i]) appendCodePoint(codePoints[i])
if (modifiable[i]) { if (modifiable[i]) {
append(tone.value) 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 { fun getPreferredTone(emoji: String): String {
if (!isSupported()) return emoji if (!isSupported()) return emoji
val (codePoints, modifiable) = getCodePoints(emoji) val (codePoints, modifiable) = getCodePoints(emoji)
if (!isModifiable(modifiable)) return emoji val tone = defaultSkinTone
val candidate = buildEmoji(codePoints, modifiable, defaultSkinTone) if (tone == SkinTone.Default || !isModifiable(modifiable)) return emoji
return if (DefaultTextPaint.hasGlyph(candidate)) candidate else emoji val candidate = buildEmoji(codePoints, modifiable, tone)
return if (isValidEmoji(candidate)) candidate else emoji
} }
fun produceSkinTones(emoji: String): Array<String>? { fun produceSkinTones(emoji: String): Array<String>? {
if (!isSupported()) return null if (!isSupported()) return null
val (codePoints, modifiable) = getCodePoints(emoji) val (codePoints, modifiable) = getCodePoints(emoji)
val tone = defaultSkinTone
if (!isModifiable(modifiable)) return null if (!isModifiable(modifiable)) return null
val candidates = SkinTone.entries val candidates = SkinTone.entries
.filter { it != defaultSkinTone } .filter { it != tone }
.map { buildEmoji(codePoints, modifiable, it) } .map { buildEmoji(codePoints, modifiable, it) }
.filter { DefaultTextPaint.hasGlyph(it) } .filter { isValidEmoji(it) }
return if (candidates.isEmpty()) null else candidates.toTypedArray() return if (candidates.isEmpty()) null else candidates.toTypedArray()
} }
} }