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