feat: Add AppleEmojiTextField component for enhanced emoji input with PNG support

This commit is contained in:
k1ngsterr1
2026-01-10 21:17:13 +05:00
parent 308381fa94
commit 286706188b
2 changed files with 227 additions and 21 deletions

View File

@@ -42,6 +42,7 @@ import com.rosetta.messenger.network.SearchUser
import com.rosetta.messenger.ui.onboarding.PrimaryBlue import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import com.rosetta.messenger.ui.components.VerifiedBadge import com.rosetta.messenger.ui.components.VerifiedBadge
import com.rosetta.messenger.ui.components.AppleEmojiPickerPanel import com.rosetta.messenger.ui.components.AppleEmojiPickerPanel
import com.rosetta.messenger.ui.components.AppleEmojiTextField
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -518,35 +519,21 @@ private fun MessageInputBar(
.padding(horizontal = 14.dp, vertical = 4.dp), .padding(horizontal = 14.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Text field // Apple Emoji Text Field (с PNG эмодзи)
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(top = 8.dp, bottom = 8.dp, end = 70.dp), // место для emoji + send .padding(top = 8.dp, bottom = 8.dp, end = 70.dp), // место для emoji + send
contentAlignment = Alignment.CenterStart contentAlignment = Alignment.CenterStart
) { ) {
BasicTextField( AppleEmojiTextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
textStyle = androidx.compose.ui.text.TextStyle( textColor = textColor,
color = textColor, textSize = 16f,
fontSize = 16.sp hint = "Message",
), hintColor = placeholderColor.copy(alpha = 0.6f),
cursorBrush = SolidColor(PrimaryBlue), modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
maxLines = 5,
decorationBox = { innerTextField ->
Box(contentAlignment = Alignment.CenterStart) {
if (value.isEmpty()) {
Text(
text = "Message",
color = placeholderColor.copy(alpha = 0.6f),
fontSize = 16.sp
)
}
innerTextField()
}
}
) )
} }
} }

View File

@@ -0,0 +1,219 @@
package com.rosetta.messenger.ui.components
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.text.Editable
import android.text.SpannableStringBuilder
import android.text.TextWatcher
import android.text.style.ImageSpan
import android.util.AttributeSet
import android.util.LruCache
import android.view.Gravity
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.viewinterop.AndroidView
import java.util.regex.Pattern
/**
* Apple Emoji EditText - кастомный EditText с PNG эмодзи
* Заменяет системные эмодзи на Apple PNG изображения из assets
*/
class AppleEmojiEditTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.editTextStyle
) : EditText(context, attrs, defStyleAttr) {
var onTextChange: ((String) -> Unit)? = null
private var isUpdating = false
companion object {
// Regex для эмодзи
private val EMOJI_PATTERN = Pattern.compile(
"[\\x{1F600}-\\x{1F64F}]|" + // Emoticons
"[\\x{1F300}-\\x{1F5FF}]|" + // Misc Symbols and Pictographs
"[\\x{1F680}-\\x{1F6FF}]|" + // Transport and Map
"[\\x{1F1E0}-\\x{1F1FF}]|" + // Flags
"[\\x{2600}-\\x{26FF}]|" + // Misc symbols
"[\\x{2700}-\\x{27BF}]|" + // Dingbats
"[\\x{FE00}-\\x{FE0F}]|" + // Variation Selectors
"[\\x{1F900}-\\x{1F9FF}]|" + // Supplemental Symbols
"[\\x{1FA00}-\\x{1FA6F}]|" + // Chess Symbols
"[\\x{1FA70}-\\x{1FAFF}]|" + // Symbols Extended-A
"[\\x{231A}-\\x{231B}]|" + // Watch, Hourglass
"[\\x{23E9}-\\x{23F3}]|" + // Media controls
"[\\x{23F8}-\\x{23FA}]|" + // Media controls
"[\\x{25AA}-\\x{25AB}]|" + // Squares
"[\\x{25B6}]|[\\x{25C0}]|" + // Play buttons
"[\\x{25FB}-\\x{25FE}]|" + // Squares
"[\\x{2614}-\\x{2615}]|" + // Umbrella, Hot beverage
"[\\x{2648}-\\x{2653}]|" + // Zodiac
"[\\x{267F}]|[\\x{2693}]|" + // Wheelchair, Anchor
"[\\x{26A1}]|[\\x{26AA}-\\x{26AB}]|" +
"[\\x{26BD}-\\x{26BE}]|" + // Sports
"[\\x{26C4}-\\x{26C5}]|" + // Weather
"[\\x{26CE}]|[\\x{26D4}]|" +
"[\\x{26EA}]|[\\x{26F2}-\\x{26F3}]|" +
"[\\x{26F5}]|[\\x{26FA}]|[\\x{26FD}]|" +
"[\\x{2702}]|[\\x{2705}]|" +
"[\\x{2708}-\\x{270D}]|[\\x{270F}]|" +
"[\\x{2712}]|[\\x{2714}]|[\\x{2716}]|" +
"[\\x{271D}]|[\\x{2721}]|" +
"[\\x{2728}]|[\\x{2733}-\\x{2734}]|" +
"[\\x{2744}]|[\\x{2747}]|" +
"[\\x{274C}]|[\\x{274E}]|" +
"[\\x{2753}-\\x{2755}]|[\\x{2757}]|" +
"[\\x{2763}-\\x{2764}]|" +
"[\\x{2795}-\\x{2797}]|[\\x{27A1}]|" +
"[\\x{27B0}]|[\\x{27BF}]|" +
"[\\x{2934}-\\x{2935}]|" +
"[\\x{2B05}-\\x{2B07}]|" +
"[\\x{2B1B}-\\x{2B1C}]|" +
"[\\x{2B50}]|[\\x{2B55}]|" +
"[\\x{3030}]|[\\x{303D}]|" +
"[\\x{3297}]|[\\x{3299}]"
)
// Кэш для bitmap и drawable
private val bitmapCache = LruCache<String, Bitmap>(500)
private val drawableCache = LruCache<String, BitmapDrawable>(500)
}
init {
// Настраиваем EditText
background = null
setPadding(0, 0, 0, 0)
gravity = Gravity.CENTER_VERTICAL or Gravity.START
isSingleLine = false
maxLines = 5
imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI
addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
if (!isUpdating && s != null) {
replaceEmojisWithImages(s)
// Отправляем plain text без spans
onTextChange?.invoke(s.toString())
}
}
})
}
fun setTextWithEmojis(newText: String) {
if (newText == text.toString()) return
isUpdating = true
setText(newText)
isUpdating = false
replaceEmojisWithImages(editableText)
}
private fun replaceEmojisWithImages(editable: Editable) {
if (isUpdating) return
isUpdating = true
try {
val textStr = editable.toString()
val matcher = EMOJI_PATTERN.matcher(textStr)
val cursorPosition = selectionStart
while (matcher.find()) {
val emoji = matcher.group()
val start = matcher.start()
val end = matcher.end()
// Проверяем, есть ли уже ImageSpan
val existingSpans = editable.getSpans(start, end, ImageSpan::class.java)
if (existingSpans.isNotEmpty()) continue
val unified = emojiToUnified(emoji)
var drawable = drawableCache.get(unified)
if (drawable == null) {
var bitmap = bitmapCache.get(unified)
if (bitmap == null) {
bitmap = loadFromAssets(unified)
if (bitmap != null) {
bitmapCache.put(unified, bitmap)
}
}
if (bitmap != null) {
drawable = BitmapDrawable(getContext().resources, bitmap)
val size = (textSize * 1.15).toInt()
drawable.setBounds(0, 0, size, size)
drawableCache.put(unified, drawable)
}
}
if (drawable != null && start < editable.length && end <= editable.length) {
val imageSpan = ImageSpan(drawable, ImageSpan.ALIGN_CENTER)
editable.setSpan(imageSpan, start, end, SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
if (cursorPosition >= 0 && cursorPosition <= editable.length) {
setSelection(cursorPosition)
}
} finally {
isUpdating = false
}
}
private fun loadFromAssets(unified: String): Bitmap? {
return try {
val inputStream = getContext().assets.open("emoji/$unified.png")
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream.close()
bitmap
} catch (e: Exception) {
null
}
}
private fun emojiToUnified(emoji: String): String {
return emoji.codePoints()
.filter { it != 0xFE0F }
.mapToObj { String.format("%04x", it) }
.toList()
.joinToString("-")
}
}
/**
* Compose обёртка для AppleEmojiEditText
*/
@Composable
fun AppleEmojiTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
textColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.White,
textSize: Float = 16f,
hint: String = "Message",
hintColor: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.Gray
) {
AndroidView(
factory = { ctx ->
AppleEmojiEditTextView(ctx).apply {
setTextColor(textColor.toArgb())
setHintTextColor(hintColor.toArgb())
setHint(hint)
setTextSize(textSize)
onTextChange = onValueChange
}
},
update = { view ->
if (view.text.toString() != value) {
view.setTextWithEmojis(value)
}
},
modifier = modifier
)
}