feat: Enhance AppleEmojiTextView to support :emoji_XXXX: format and Unicode emojis
This commit is contained in:
@@ -272,6 +272,9 @@ fun AppleEmojiText(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Apple Emoji TextView - для отображения текста с PNG эмодзи
|
* Apple Emoji TextView - для отображения текста с PNG эмодзи
|
||||||
|
* 🔥 Поддерживает:
|
||||||
|
* - Реальные Unicode эмодзи (😀)
|
||||||
|
* - Текстовый формат :emoji_1f600: (как в React Native версии)
|
||||||
*/
|
*/
|
||||||
class AppleEmojiTextView @JvmOverloads constructor(
|
class AppleEmojiTextView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -281,6 +284,8 @@ class AppleEmojiTextView @JvmOverloads constructor(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val EMOJI_PATTERN = AppleEmojiEditTextView.EMOJI_PATTERN
|
private val EMOJI_PATTERN = AppleEmojiEditTextView.EMOJI_PATTERN
|
||||||
|
// 🔥 Паттерн для :emoji_XXXX: формата (из React Native)
|
||||||
|
private val EMOJI_CODE_PATTERN = Pattern.compile(":emoji_([a-fA-F0-9_-]+):")
|
||||||
private val bitmapCache = LruCache<String, Bitmap>(100)
|
private val bitmapCache = LruCache<String, Bitmap>(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,24 +295,67 @@ class AppleEmojiTextView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setTextWithEmojis(text: String) {
|
fun setTextWithEmojis(text: String) {
|
||||||
|
// 🔥 Сначала заменяем :emoji_XXXX: на PNG изображения
|
||||||
val spannable = SpannableStringBuilder(text)
|
val spannable = SpannableStringBuilder(text)
|
||||||
val matcher = EMOJI_PATTERN.matcher(text)
|
|
||||||
|
|
||||||
while (matcher.find()) {
|
// Собираем все замены (чтобы не сбить индексы)
|
||||||
val emoji = matcher.group()
|
data class EmojiMatch(val start: Int, val end: Int, val unified: String)
|
||||||
|
val emojiMatches = mutableListOf<EmojiMatch>()
|
||||||
|
|
||||||
|
// 1. Ищем :emoji_XXXX: формат
|
||||||
|
val codeMatcher = EMOJI_CODE_PATTERN.matcher(text)
|
||||||
|
while (codeMatcher.find()) {
|
||||||
|
val unified = codeMatcher.group(1) ?: continue
|
||||||
|
emojiMatches.add(EmojiMatch(codeMatcher.start(), codeMatcher.end(), unified))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Ищем реальные Unicode эмодзи
|
||||||
|
val unicodeMatcher = EMOJI_PATTERN.matcher(text)
|
||||||
|
while (unicodeMatcher.find()) {
|
||||||
|
val emoji = unicodeMatcher.group()
|
||||||
val unified = emojiToUnified(emoji)
|
val unified = emojiToUnified(emoji)
|
||||||
val bitmap = loadEmojiBitmap(unified)
|
// Проверяем что этот диапазон не перекрывается с :emoji_XXXX:
|
||||||
|
val overlaps = emojiMatches.any {
|
||||||
|
(unicodeMatcher.start() >= it.start && unicodeMatcher.start() < it.end) ||
|
||||||
|
(unicodeMatcher.end() > it.start && unicodeMatcher.end() <= it.end)
|
||||||
|
}
|
||||||
|
if (!overlaps) {
|
||||||
|
emojiMatches.add(EmojiMatch(unicodeMatcher.start(), unicodeMatcher.end(), unified))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Сортируем по позиции в обратном порядке (чтобы не сбить индексы при замене)
|
||||||
|
emojiMatches.sortByDescending { it.start }
|
||||||
|
|
||||||
|
// 4. Применяем все замены
|
||||||
|
for (match in emojiMatches) {
|
||||||
|
val bitmap = loadEmojiBitmap(match.unified)
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
val size = (textSize * 1.3).toInt() // Увеличиваем размер emoji
|
val size = (textSize * 1.3).toInt()
|
||||||
val scaledBitmap = Bitmap.createScaledBitmap(bitmap, size, size, true)
|
val scaledBitmap = Bitmap.createScaledBitmap(bitmap, size, size, true)
|
||||||
val drawable = BitmapDrawable(resources, scaledBitmap)
|
val drawable = BitmapDrawable(resources, scaledBitmap)
|
||||||
drawable.setBounds(0, 0, size, size)
|
drawable.setBounds(0, 0, size, size)
|
||||||
|
|
||||||
// ALIGN_BOTTOM лучше работает с emoji - не обрезает сверху
|
|
||||||
val span = ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM)
|
val span = ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM)
|
||||||
spannable.setSpan(span, matcher.start(), matcher.end(),
|
|
||||||
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
// Для :emoji_XXXX: заменяем весь текст на пробел + span
|
||||||
|
// Для Unicode эмодзи оставляем символ как есть
|
||||||
|
if (match.end - match.start > 10) {
|
||||||
|
// Это :emoji_XXXX: формат - заменяем на один символ
|
||||||
|
spannable.replace(match.start, match.end, "\u200B") // Zero-width space
|
||||||
|
spannable.setSpan(span, match.start, match.start + 1,
|
||||||
|
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
} else {
|
||||||
|
// Это Unicode эмодзи
|
||||||
|
spannable.setSpan(span, match.start, match.end,
|
||||||
|
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
} else if (match.end - match.start > 10) {
|
||||||
|
// 🔥 Fallback: если PNG не найден, конвертируем :emoji_XXXX: в Unicode эмодзи
|
||||||
|
val unicodeEmoji = unifiedToEmoji(match.unified)
|
||||||
|
if (unicodeEmoji != null) {
|
||||||
|
spannable.replace(match.start, match.end, unicodeEmoji)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,4 +384,16 @@ class AppleEmojiTextView @JvmOverloads constructor(
|
|||||||
.toList()
|
.toList()
|
||||||
.joinToString("-")
|
.joinToString("-")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔥 Конвертирует unified код (1f600) в Unicode эмодзи (😀)
|
||||||
|
*/
|
||||||
|
private fun unifiedToEmoji(unified: String): String? {
|
||||||
|
return try {
|
||||||
|
val codePoints = unified.split("-").map { it.toInt(16) }
|
||||||
|
String(codePoints.toIntArray(), 0, codePoints.size)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user