feat: Refactor MessageInputBar for improved emoji picker and input field design
This commit is contained in:
@@ -1,28 +1,31 @@
|
|||||||
package com.rosetta.messenger.ui.components
|
package com.rosetta.messenger.ui.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -31,136 +34,167 @@ import coil.compose.AsyncImage
|
|||||||
import coil.request.CachePolicy
|
import coil.request.CachePolicy
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apple Emoji Picker с PNG изображениями
|
* 🍎 Apple Emoji Picker с PNG изображениями
|
||||||
* Загружает ВСЕ эмодзи из assets/emoji директории
|
* - Категория "Все" первая
|
||||||
|
* - Apple Style эмодзи из assets/emoji
|
||||||
|
* - Фоновая загрузка для оптимизации
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Категории эмодзи
|
// Категории эмодзи с Material иконками
|
||||||
data class EmojiCategory(
|
data class EmojiCategory(
|
||||||
val key: String,
|
val key: String,
|
||||||
val label: String, // unified код для иконки категории
|
val label: String,
|
||||||
val ranges: List<Pair<Int, Int>> // Unicode диапазоны для этой категории
|
val icon: ImageVector,
|
||||||
|
val ranges: List<Pair<Int, Int>>
|
||||||
)
|
)
|
||||||
|
|
||||||
// Стандартный порядок категорий как в iOS/Android/WhatsApp
|
// Порядок категорий: "All" первая, затем стандартные
|
||||||
val EMOJI_CATEGORIES = listOf(
|
val EMOJI_CATEGORIES = listOf(
|
||||||
// 😀 Smileys & Emotion: лица, эмоции, руки
|
// 🔥 ALL - все эмодзи (первая категория)
|
||||||
EmojiCategory("Smileys", "1f600", listOf(
|
EmojiCategory("All", "Все", Icons.Default.Apps, emptyList()),
|
||||||
0x1F600 to 0x1F64F, // Emoticons
|
// 😀 Smileys & Emotion
|
||||||
0x1F910 to 0x1F92F, // Face with...
|
EmojiCategory("Smileys", "Смайлы", Icons.Default.SentimentSatisfied, listOf(
|
||||||
0x1F970 to 0x1F9FF, // More faces
|
0x1F600 to 0x1F64F,
|
||||||
0x263A to 0x263A, // ☺
|
0x1F910 to 0x1F92F,
|
||||||
0x2639 to 0x2639 // ☹
|
0x1F970 to 0x1F9FF,
|
||||||
|
0x263A to 0x263A,
|
||||||
|
0x2639 to 0x2639
|
||||||
)),
|
)),
|
||||||
// 👋 People & Body: люди, жесты, части тела
|
// 👋 People & Body
|
||||||
EmojiCategory("People", "1f44b", listOf(
|
EmojiCategory("People", "Люди", Icons.Default.Person, listOf(
|
||||||
0x1F466 to 0x1F4FF, // People
|
0x1F466 to 0x1F4FF,
|
||||||
0x1F9D0 to 0x1F9DF, // More people
|
0x1F9D0 to 0x1F9DF,
|
||||||
0x270A to 0x270D, // Hands
|
0x270A to 0x270D,
|
||||||
0x261D to 0x261D, // ☝
|
0x261D to 0x261D,
|
||||||
0x1F440 to 0x1F465 // Body parts
|
0x1F440 to 0x1F465
|
||||||
)),
|
)),
|
||||||
// 🐱 Animals & Nature: животные, растения, природа
|
// 🐱 Animals & Nature
|
||||||
EmojiCategory("Animals", "1f431", listOf(
|
EmojiCategory("Animals", "Животные", Icons.Default.Pets, listOf(
|
||||||
0x1F400 to 0x1F43F, // Animals
|
0x1F400 to 0x1F43F,
|
||||||
0x1F980 to 0x1F9AE, // More animals
|
0x1F980 to 0x1F9AE,
|
||||||
0x1F330 to 0x1F335, // Plants
|
0x1F330 to 0x1F335,
|
||||||
0x1F337 to 0x1F34F, // Flowers, fruits
|
0x1F337 to 0x1F34F,
|
||||||
0x2618 to 0x2618 // ☘
|
0x2618 to 0x2618
|
||||||
)),
|
)),
|
||||||
// 🍎 Food & Drink: еда, напитки
|
// 🍎 Food & Drink
|
||||||
EmojiCategory("Food", "1f34e", listOf(
|
EmojiCategory("Food", "Еда", Icons.Default.Restaurant, listOf(
|
||||||
0x1F345 to 0x1F37F, // Food
|
0x1F345 to 0x1F37F,
|
||||||
0x1F950 to 0x1F96F, // More food
|
0x1F950 to 0x1F96F,
|
||||||
0x1F9C0 to 0x1F9CB, // Cheese, drinks
|
0x1F9C0 to 0x1F9CB,
|
||||||
0x1FAD0 to 0x1FAD9, // New food
|
0x1FAD0 to 0x1FAD9,
|
||||||
0x2615 to 0x2615 // ☕
|
0x2615 to 0x2615
|
||||||
)),
|
)),
|
||||||
// ✈️ Travel & Places: транспорт, места, здания
|
// ✈️ Travel & Places
|
||||||
EmojiCategory("Travel", "2708-fe0f", listOf(
|
EmojiCategory("Travel", "Места", Icons.Default.Flight, listOf(
|
||||||
0x1F680 to 0x1F6FF, // Transport
|
0x1F680 to 0x1F6FF,
|
||||||
0x1F3D4 to 0x1F3DF, // Places
|
0x1F3D4 to 0x1F3DF,
|
||||||
0x1F3E0 to 0x1F3F0, // Buildings
|
0x1F3E0 to 0x1F3F0,
|
||||||
0x2708 to 0x2708, // ✈
|
0x2708 to 0x2708,
|
||||||
0x26F0 to 0x26FF // Mountains, etc
|
0x26F0 to 0x26FF
|
||||||
)),
|
)),
|
||||||
// ⚽ Activities: спорт, игры, хобби
|
// ⚽ Activities
|
||||||
EmojiCategory("Activities", "26bd", listOf(
|
EmojiCategory("Activities", "Спорт", Icons.Default.SportsSoccer, listOf(
|
||||||
0x1F3A0 to 0x1F3CA, // Activities
|
0x1F3A0 to 0x1F3CA,
|
||||||
0x1F3CB to 0x1F3D3, // Sports
|
0x1F3CB to 0x1F3D3,
|
||||||
0x1F93C to 0x1F94F, // More sports
|
0x1F93C to 0x1F94F,
|
||||||
0x26BD to 0x26BE, // ⚽⚾
|
0x26BD to 0x26BE,
|
||||||
0x265F to 0x2660, // Chess
|
0x265F to 0x2660,
|
||||||
0x1F9E0 to 0x1F9FF // Games
|
0x1F9E0 to 0x1F9FF
|
||||||
)),
|
)),
|
||||||
// 💡 Objects: предметы, инструменты
|
// 💡 Objects
|
||||||
EmojiCategory("Objects", "1f4a1", listOf(
|
EmojiCategory("Objects", "Объекты", Icons.Default.Lightbulb, listOf(
|
||||||
0x1F4A1 to 0x1F4FF, // Objects (lightbulb to...)
|
0x1F4A1 to 0x1F4FF,
|
||||||
0x1F500 to 0x1F5FF, // More objects
|
0x1F500 to 0x1F5FF,
|
||||||
0x1F6E0 to 0x1F6EF, // Tools
|
0x1F6E0 to 0x1F6EF,
|
||||||
0x1FA70 to 0x1FAFF, // New objects
|
0x1FA70 to 0x1FAFF,
|
||||||
0x2328 to 0x2328 // ⌨
|
0x2328 to 0x2328
|
||||||
)),
|
)),
|
||||||
// ❤️ Symbols: сердца, знаки, символы
|
// ❤️ Symbols
|
||||||
EmojiCategory("Symbols", "2764-fe0f", listOf(
|
EmojiCategory("Symbols", "Символы", Icons.Default.Favorite, listOf(
|
||||||
0x2764 to 0x2764, // ❤
|
0x2764 to 0x2764,
|
||||||
0x1F490 to 0x1F49F, // Hearts
|
0x1F490 to 0x1F49F,
|
||||||
0x2600 to 0x26FF, // Misc symbols
|
0x2600 to 0x26FF,
|
||||||
0x2700 to 0x27BF, // Dingbats
|
0x2700 to 0x27BF,
|
||||||
0x1F170 to 0x1F1FF, // Squared letters (before flags)
|
0x1F170 to 0x1F1FF,
|
||||||
0x00A9 to 0x00AE, // ©®
|
0x00A9 to 0x00AE,
|
||||||
0x203C to 0x3299 // Misc
|
0x203C to 0x3299
|
||||||
)),
|
)),
|
||||||
// 🏳️ Flags: флаги стран
|
// 🏳️ Flags
|
||||||
EmojiCategory("Flags", "1f3f3-fe0f", listOf(
|
EmojiCategory("Flags", "Флаги", Icons.Default.Flag, listOf(
|
||||||
0x1F1E0 to 0x1F1FF, // Regional indicators (flags)
|
0x1F1E0 to 0x1F1FF,
|
||||||
0x1F3F3 to 0x1F3F4, // 🏳🏴
|
0x1F3F3 to 0x1F3F4,
|
||||||
0x1F3C1 to 0x1F3C1, // 🏁
|
0x1F3C1 to 0x1F3C1,
|
||||||
0x1F6A9 to 0x1F6A9 // 🚩
|
0x1F6A9 to 0x1F6A9
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверяет, попадает ли emoji в диапазон категории
|
* Проверяет, попадает ли emoji в диапазон категории
|
||||||
*/
|
*/
|
||||||
fun emojiMatchesCategory(emoji: String, category: EmojiCategory): Boolean {
|
private fun emojiMatchesCategory(emoji: String, category: EmojiCategory): Boolean {
|
||||||
val unified = emoji.lowercase().split("-").firstOrNull() ?: return false
|
val unified = emoji.lowercase().split("-").firstOrNull() ?: return false
|
||||||
val codePoint = try { unified.toInt(16) } catch (e: Exception) { return false }
|
val codePoint = try { unified.toInt(16) } catch (e: Exception) { return false }
|
||||||
return category.ranges.any { (start, end) -> codePoint in start..end }
|
return category.ranges.any { (start, end) -> codePoint in start..end }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Загружает все эмодзи из assets/emoji
|
* Синглтон для кэширования эмодзи
|
||||||
*/
|
*/
|
||||||
fun loadAllEmojisFromAssets(context: Context): List<String> {
|
object EmojiCache {
|
||||||
return try {
|
private var allEmojis: List<String>? = null
|
||||||
context.assets.list("emoji")
|
private var emojisByCategory: Map<String, List<String>>? = null
|
||||||
|
var isLoaded by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
suspend fun loadEmojis(context: Context) {
|
||||||
|
if (allEmojis != null) {
|
||||||
|
isLoaded = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val emojis = context.assets.list("emoji")
|
||||||
?.filter { it.endsWith(".png") }
|
?.filter { it.endsWith(".png") }
|
||||||
?.map { it.removeSuffix(".png") }
|
?.map { it.removeSuffix(".png") }
|
||||||
?.sorted()
|
?.sorted()
|
||||||
?: emptyList()
|
?: emptyList()
|
||||||
|
|
||||||
|
allEmojis = emojis
|
||||||
|
emojisByCategory = groupEmojis(emojis)
|
||||||
|
Log.d("EmojiCache", "Loaded ${emojis.size} emojis")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
emptyList()
|
Log.e("EmojiCache", "Error loading emojis", e)
|
||||||
|
allEmojis = emptyList()
|
||||||
|
emojisByCategory = emptyMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isLoaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEmojisForCategory(categoryKey: String): List<String> {
|
||||||
|
return if (categoryKey == "All") {
|
||||||
|
allEmojis ?: emptyList()
|
||||||
|
} else {
|
||||||
|
emojisByCategory?.get(categoryKey) ?: emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun groupEmojis(allEmojis: List<String>): Map<String, List<String>> {
|
||||||
* Группирует эмодзи по категориям на основе Unicode диапазонов
|
|
||||||
*/
|
|
||||||
fun groupEmojisByCategory(allEmojis: List<String>): Map<String, List<String>> {
|
|
||||||
val result = mutableMapOf<String, MutableList<String>>()
|
val result = mutableMapOf<String, MutableList<String>>()
|
||||||
val usedEmojis = mutableSetOf<String>()
|
val usedEmojis = mutableSetOf<String>()
|
||||||
|
|
||||||
// Инициализируем категории
|
EMOJI_CATEGORIES.filter { it.key != "All" }.forEach { category ->
|
||||||
EMOJI_CATEGORIES.forEach { category ->
|
|
||||||
result[category.key] = mutableListOf()
|
result[category.key] = mutableListOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Группируем по Unicode диапазонам
|
|
||||||
for (emoji in allEmojis) {
|
for (emoji in allEmojis) {
|
||||||
for (category in EMOJI_CATEGORIES) {
|
for (category in EMOJI_CATEGORIES) {
|
||||||
|
if (category.key == "All") continue
|
||||||
if (emojiMatchesCategory(emoji, category) && emoji !in usedEmojis) {
|
if (emojiMatchesCategory(emoji, category) && emoji !in usedEmojis) {
|
||||||
result[category.key]?.add(emoji)
|
result[category.key]?.add(emoji)
|
||||||
usedEmojis.add(emoji)
|
usedEmojis.add(emoji)
|
||||||
@@ -169,7 +203,6 @@ fun groupEmojisByCategory(allEmojis: List<String>): Map<String, List<String>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем неклассифицированные в Symbols
|
|
||||||
for (emoji in allEmojis) {
|
for (emoji in allEmojis) {
|
||||||
if (emoji !in usedEmojis) {
|
if (emoji !in usedEmojis) {
|
||||||
result["Symbols"]?.add(emoji)
|
result["Symbols"]?.add(emoji)
|
||||||
@@ -178,6 +211,7 @@ fun groupEmojisByCategory(allEmojis: List<String>): Map<String, List<String>> {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Конвертирует unified код в emoji символ
|
* Конвертирует unified код в emoji символ
|
||||||
@@ -196,8 +230,7 @@ fun unifiedToEmoji(unified: String): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Кнопка эмодзи с PNG изображением и анимацией нажатия
|
* Кнопка эмодзи с PNG изображением
|
||||||
* Оптимизирована с кэшированием
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun EmojiButton(
|
fun EmojiButton(
|
||||||
@@ -218,16 +251,15 @@ fun EmojiButton(
|
|||||||
label = "emojiScale"
|
label = "emojiScale"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Мемоизируем ImageRequest для оптимизации - используем assetFile fetcher
|
|
||||||
val imageRequest = remember(unified, context) {
|
val imageRequest = remember(unified, context) {
|
||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
.data("file:///android_asset/emoji/${unified.lowercase()}.png")
|
.data("file:///android_asset/emoji/${unified.lowercase()}.png")
|
||||||
.crossfade(false)
|
.crossfade(false)
|
||||||
.size(64) // Задаём размер для оптимизации памяти
|
.size(64)
|
||||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||||
.diskCachePolicy(CachePolicy.ENABLED)
|
.diskCachePolicy(CachePolicy.ENABLED)
|
||||||
.memoryCacheKey("emoji_${unified}")
|
.memoryCacheKey("emoji_$unified")
|
||||||
.diskCacheKey("emoji_${unified}")
|
.diskCacheKey("emoji_$unified")
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,14 +279,14 @@ fun EmojiButton(
|
|||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = imageRequest,
|
model = imageRequest,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(32.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
contentScale = ContentScale.Fit
|
contentScale = ContentScale.Fit
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Кнопка категории с PNG изображением
|
* Кнопка категории с Material иконкой
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryButton(
|
fun CategoryButton(
|
||||||
@@ -264,7 +296,6 @@ fun CategoryButton(
|
|||||||
isDarkTheme: Boolean = true,
|
isDarkTheme: Boolean = true,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
@@ -277,27 +308,17 @@ fun CategoryButton(
|
|||||||
label = "categoryScale"
|
label = "categoryScale"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Мемоизируем ImageRequest с размером для оптимизации
|
val backgroundColor = if (isSelected) PrimaryBlue.copy(alpha = 0.2f) else Color.Transparent
|
||||||
val imageRequest = remember(category.label, context) {
|
val iconTint = if (isSelected) PrimaryBlue
|
||||||
ImageRequest.Builder(context)
|
else if (isDarkTheme) Color.White.copy(alpha = 0.6f)
|
||||||
.data("file:///android_asset/emoji/${category.label.lowercase()}.png")
|
else Color.Black.copy(alpha = 0.5f)
|
||||||
.crossfade(false)
|
|
||||||
.size(48)
|
|
||||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
|
||||||
.diskCachePolicy(CachePolicy.ENABLED)
|
|
||||||
.memoryCacheKey("category_${category.label}")
|
|
||||||
.diskCacheKey("category_${category.label}")
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.size(40.dp)
|
.size(40.dp)
|
||||||
.scale(scale)
|
.scale(scale)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(backgroundColor)
|
||||||
if (isSelected) PrimaryBlue.copy(alpha = 0.25f) else Color.Transparent
|
|
||||||
)
|
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
indication = null,
|
indication = null,
|
||||||
@@ -305,18 +326,17 @@ fun CategoryButton(
|
|||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
Icon(
|
||||||
model = imageRequest,
|
imageVector = category.icon,
|
||||||
contentDescription = category.key,
|
contentDescription = category.label,
|
||||||
modifier = Modifier.size(24.dp),
|
tint = iconTint,
|
||||||
contentScale = ContentScale.Fit
|
modifier = Modifier.size(22.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apple Emoji Picker Panel - Liquid Glass стиль
|
* Apple Emoji Picker Panel
|
||||||
* Загружает ВСЕ эмодзи из assets
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AppleEmojiPickerPanel(
|
fun AppleEmojiPickerPanel(
|
||||||
@@ -326,22 +346,21 @@ fun AppleEmojiPickerPanel(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var selectedCategory by remember { mutableStateOf(EMOJI_CATEGORIES[0]) }
|
var selectedCategory by remember { mutableStateOf(EMOJI_CATEGORIES[0]) } // "All" по умолчанию
|
||||||
val gridState = rememberLazyGridState()
|
val gridState = rememberLazyGridState()
|
||||||
|
|
||||||
// Загружаем ВСЕ эмодзи из assets один раз
|
// Загружаем эмодзи в фоне
|
||||||
val allEmojis = remember {
|
LaunchedEffect(Unit) {
|
||||||
loadAllEmojisFromAssets(context)
|
EmojiCache.loadEmojis(context)
|
||||||
}
|
|
||||||
|
|
||||||
// Группируем по категориям
|
|
||||||
val emojisByCategory = remember(allEmojis) {
|
|
||||||
groupEmojisByCategory(allEmojis)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Текущие эмодзи для выбранной категории
|
// Текущие эмодзи для выбранной категории
|
||||||
val currentEmojis = remember(selectedCategory.key, emojisByCategory) {
|
val currentEmojis = remember(selectedCategory.key, EmojiCache.isLoaded) {
|
||||||
emojisByCategory[selectedCategory.key] ?: emptyList()
|
if (EmojiCache.isLoaded) {
|
||||||
|
EmojiCache.getEmojisForCategory(selectedCategory.key)
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сбрасываем скролл при смене категории
|
// Сбрасываем скролл при смене категории
|
||||||
@@ -349,94 +368,25 @@ fun AppleEmojiPickerPanel(
|
|||||||
gridState.scrollToItem(0)
|
gridState.scrollToItem(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Liquid Glass цвета
|
val panelBackground = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF2F2F7)
|
||||||
val glassBackground = if (isDarkTheme) {
|
val categoryBarBackground = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White
|
||||||
Brush.verticalGradient(
|
val dividerColor = if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
|
||||||
colors = listOf(
|
|
||||||
Color(0xFF2D2D2F).copy(alpha = 0.95f),
|
|
||||||
Color(0xFF1C1C1E).copy(alpha = 0.98f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color(0xFFF2F2F7).copy(alpha = 0.96f),
|
|
||||||
Color(0xFFE5E5EA).copy(alpha = 0.98f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val glassBorder = if (isDarkTheme) {
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.White.copy(alpha = 0.15f),
|
|
||||||
Color.White.copy(alpha = 0.05f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.White.copy(alpha = 0.9f),
|
|
||||||
Color.Black.copy(alpha = 0.05f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val categoryBarBackground = if (isDarkTheme) Color(0xFF1A1A1C).copy(alpha = 0.9f)
|
|
||||||
else Color.White.copy(alpha = 0.85f)
|
|
||||||
val dividerColor = if (isDarkTheme) Color.White.copy(alpha = 0.08f)
|
|
||||||
else Color.Black.copy(alpha = 0.06f)
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(300.dp)
|
.background(panelBackground)
|
||||||
.shadow(
|
|
||||||
elevation = 8.dp,
|
|
||||||
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
|
|
||||||
clip = false,
|
|
||||||
ambientColor = Color.Black.copy(alpha = 0.3f),
|
|
||||||
spotColor = Color.Black.copy(alpha = 0.3f)
|
|
||||||
)
|
|
||||||
.clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp))
|
|
||||||
.background(glassBackground)
|
|
||||||
.border(
|
|
||||||
width = 1.dp,
|
|
||||||
brush = glassBorder,
|
|
||||||
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
// Ручка для свайпа (как в iOS)
|
// Категории - горизонтальный скролл
|
||||||
Box(
|
LazyRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 8.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(36.dp)
|
|
||||||
.height(4.dp)
|
|
||||||
.clip(RoundedCornerShape(2.dp))
|
|
||||||
.background(
|
|
||||||
if (isDarkTheme) Color.White.copy(alpha = 0.3f)
|
|
||||||
else Color.Black.copy(alpha = 0.2f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Категории сверху - тоже в glass стиле
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 12.dp)
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
|
||||||
.background(categoryBarBackground)
|
.background(categoryBarBackground)
|
||||||
.padding(horizontal = 4.dp, vertical = 6.dp),
|
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
EMOJI_CATEGORIES.forEach { category ->
|
items(EMOJI_CATEGORIES) { category ->
|
||||||
CategoryButton(
|
CategoryButton(
|
||||||
category = category,
|
category = category,
|
||||||
isSelected = selectedCategory == category,
|
isSelected = selectedCategory == category,
|
||||||
@@ -446,33 +396,63 @@ fun AppleEmojiPickerPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
// Разделитель
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(0.5.dp)
|
||||||
|
.background(dividerColor)
|
||||||
|
)
|
||||||
|
|
||||||
// Сетка эмодзи с оптимизированной загрузкой
|
// Сетка эмодзи
|
||||||
|
if (!EmojiCache.isLoaded) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
color = PrimaryBlue,
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (currentEmojis.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Нет эмодзи",
|
||||||
|
color = if (isDarkTheme) Color.White.copy(alpha = 0.5f) else Color.Black.copy(alpha = 0.4f),
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
state = gridState,
|
state = gridState,
|
||||||
columns = GridCells.Fixed(9),
|
columns = GridCells.Fixed(8),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 4.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
contentPadding = PaddingValues(vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = currentEmojis,
|
items = currentEmojis,
|
||||||
key = { it } // Используем unified как ключ для оптимизации
|
key = { it }
|
||||||
) { unified ->
|
) { unified ->
|
||||||
EmojiButton(
|
EmojiButton(
|
||||||
unified = unified,
|
unified = unified,
|
||||||
onClick = { emoji ->
|
onClick = { emoji -> onEmojiSelected(emoji) }
|
||||||
onEmojiSelected(emoji)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Отступ снизу для navigation bar
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user