feat: Optimize AppleEmojiPicker with caching and enhance UI with Liquid Glass style
This commit is contained in:
@@ -56,8 +56,11 @@ import com.rosetta.messenger.ui.components.VerifiedBadge
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -284,14 +287,41 @@ fun ChatDetailScreen(
|
||||
|
||||
// 🔥 Обработка системной кнопки назад
|
||||
BackHandler { hideKeyboardAndBack() }
|
||||
|
||||
// 🔥 Cleanup при выходе из экрана
|
||||
DisposableEffect(Unit) {
|
||||
|
||||
// 🔥 Ручное отслеживание высоты клавиатуры
|
||||
val view = LocalView.current
|
||||
val density = LocalDensity.current
|
||||
var keyboardHeight by remember { mutableStateOf(0.dp) }
|
||||
|
||||
DisposableEffect(view) {
|
||||
val listener = ViewTreeObserver.OnGlobalLayoutListener {
|
||||
val rect = Rect()
|
||||
view.getWindowVisibleDisplayFrame(rect)
|
||||
val screenHeight = view.rootView.height
|
||||
val keyboardHeightPx = screenHeight - rect.bottom
|
||||
|
||||
// Учитываем navigation bar (если клавиатура меньше 150px - это не клавиатура)
|
||||
val actualKeyboardHeight = if (keyboardHeightPx > 150) keyboardHeightPx else 0
|
||||
|
||||
keyboardHeight = with(density) { actualKeyboardHeight.toDp() }
|
||||
}
|
||||
|
||||
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
|
||||
|
||||
onDispose {
|
||||
view.viewTreeObserver.removeOnGlobalLayoutListener(listener)
|
||||
focusManager.clearFocus()
|
||||
keyboardController?.hide()
|
||||
}
|
||||
}
|
||||
|
||||
// Автоматический скролл вниз при появлении клавиатуры
|
||||
LaunchedEffect(keyboardHeight) {
|
||||
if (keyboardHeight > 0.dp && messages.isNotEmpty()) {
|
||||
delay(50)
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализируем ViewModel с ключами и открываем диалог
|
||||
LaunchedEffect(user.publicKey) {
|
||||
@@ -549,15 +579,21 @@ fun ChatDetailScreen(
|
||||
},
|
||||
containerColor = Color.Transparent
|
||||
) { paddingValues ->
|
||||
// 🔥 Column с imePadding - весь контент поднимается с клавиатурой
|
||||
Column(
|
||||
// 🔥 Box с фиксированным Input внизу и сообщениями выше
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.imePadding() // Контент поднимается над клавиатурой
|
||||
) {
|
||||
// Список сообщений - занимает всё доступное пространство
|
||||
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||
// Список сообщений - занимает всё пространство, с padding снизу для input и клавиатуры
|
||||
val inputBarHeight = 60.dp
|
||||
val bottomPadding = inputBarHeight + keyboardHeight
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = bottomPadding)
|
||||
) {
|
||||
if (messages.isEmpty()) {
|
||||
// Пустое состояние
|
||||
Column(
|
||||
@@ -723,35 +759,42 @@ fun ChatDetailScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} // Закрытие внутреннего Box для сообщений
|
||||
|
||||
// 🔥 INPUT BAR - внизу Column, автоматически над клавиатурой
|
||||
MessageInputBar(
|
||||
value = inputText,
|
||||
onValueChange = {
|
||||
viewModel.updateInputText(it)
|
||||
// Отправляем индикатор печатания
|
||||
if (it.isNotEmpty() && !isSavedMessages) {
|
||||
viewModel.sendTypingIndicator()
|
||||
}
|
||||
},
|
||||
onSend = {
|
||||
// Скрываем кнопку scroll на время отправки
|
||||
isSendingMessage = true
|
||||
viewModel.sendMessage()
|
||||
// Скроллим к новому сообщению
|
||||
scope.launch {
|
||||
delay(100)
|
||||
listState.animateScrollToItem(0)
|
||||
delay(300) // Ждём завершения анимации
|
||||
isSendingMessage = false
|
||||
}
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
backgroundColor = inputBackgroundColor,
|
||||
textColor = textColor,
|
||||
placeholderColor = secondaryTextColor
|
||||
)
|
||||
// 🔥 INPUT BAR - фиксированный элемент внизу экрана
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = keyboardHeight) // Ручной padding над клавиатурой
|
||||
) {
|
||||
MessageInputBar(
|
||||
value = inputText,
|
||||
onValueChange = {
|
||||
viewModel.updateInputText(it)
|
||||
// Отправляем индикатор печатания
|
||||
if (it.isNotEmpty() && !isSavedMessages) {
|
||||
viewModel.sendTypingIndicator()
|
||||
}
|
||||
},
|
||||
onSend = {
|
||||
// Скрываем кнопку scroll на время отправки
|
||||
isSendingMessage = true
|
||||
viewModel.sendMessage()
|
||||
// Скроллим к новому сообщению
|
||||
scope.launch {
|
||||
delay(100)
|
||||
listState.animateScrollToItem(0)
|
||||
delay(300) // Ждём завершения анимации
|
||||
isSendingMessage = false
|
||||
}
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
backgroundColor = inputBackgroundColor,
|
||||
textColor = textColor,
|
||||
placeholderColor = secondaryTextColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} // Закрытие Box с fade-in
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.rosetta.messenger.ui.components
|
||||
import android.content.Context
|
||||
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
|
||||
@@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.*
|
||||
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.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
@@ -18,12 +20,15 @@ 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.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
|
||||
@@ -147,6 +152,7 @@ fun unifiedToEmoji(unified: String): String {
|
||||
|
||||
/**
|
||||
* Кнопка эмодзи с PNG изображением и анимацией нажатия
|
||||
* Оптимизирована с кэшированием
|
||||
*/
|
||||
@Composable
|
||||
fun EmojiButton(
|
||||
@@ -167,6 +173,19 @@ 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) // Задаём размер для оптимизации памяти
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.diskCachePolicy(CachePolicy.ENABLED)
|
||||
.memoryCacheKey("emoji_${unified}")
|
||||
.diskCacheKey("emoji_${unified}")
|
||||
.build()
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.aspectRatio(1f)
|
||||
@@ -181,10 +200,7 @@ fun EmojiButton(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data("file:///android_asset/emoji/${unified.lowercase()}.png")
|
||||
.crossfade(false)
|
||||
.build(),
|
||||
model = imageRequest,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
contentScale = ContentScale.Fit
|
||||
@@ -200,6 +216,7 @@ fun CategoryButton(
|
||||
category: EmojiCategory,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
isDarkTheme: Boolean = true,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -215,13 +232,26 @@ 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()
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(40.dp)
|
||||
.scale(scale)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (isSelected) PrimaryBlue.copy(alpha = 0.2f) else Color.Transparent
|
||||
if (isSelected) PrimaryBlue.copy(alpha = 0.25f) else Color.Transparent
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
@@ -231,10 +261,7 @@ fun CategoryButton(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data("file:///android_asset/emoji/${category.label.lowercase()}.png")
|
||||
.crossfade(false)
|
||||
.build(),
|
||||
model = imageRequest,
|
||||
contentDescription = category.key,
|
||||
modifier = Modifier.size(24.dp),
|
||||
contentScale = ContentScale.Fit
|
||||
@@ -243,7 +270,7 @@ fun CategoryButton(
|
||||
}
|
||||
|
||||
/**
|
||||
* Apple Emoji Picker Panel
|
||||
* Apple Emoji Picker Panel - Liquid Glass стиль
|
||||
*/
|
||||
@Composable
|
||||
fun AppleEmojiPickerPanel(
|
||||
@@ -253,25 +280,102 @@ fun AppleEmojiPickerPanel(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var selectedCategory by remember { mutableStateOf(EMOJI_CATEGORIES[0]) }
|
||||
val gridState = rememberLazyGridState()
|
||||
|
||||
val panelBackground = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF8F8FA)
|
||||
val categoryBackground = if (isDarkTheme) Color(0xFF2A2A2C) else Color(0xFFFFFFFF)
|
||||
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3C) else Color(0xFFE5E5EA)
|
||||
// Мемоизируем текущие эмодзи
|
||||
val currentEmojis = remember(selectedCategory.key) {
|
||||
EMOJIS_BY_CATEGORY[selectedCategory.key] ?: emptyList()
|
||||
}
|
||||
|
||||
val currentEmojis = EMOJIS_BY_CATEGORY[selectedCategory.key] ?: emptyList()
|
||||
// Сбрасываем скролл при смене категории
|
||||
LaunchedEffect(selectedCategory) {
|
||||
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)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.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(
|
||||
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()
|
||||
.background(categoryBackground)
|
||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
.padding(horizontal = 12.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(categoryBarBackground)
|
||||
.padding(horizontal = 4.dp, vertical = 6.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -279,27 +383,29 @@ fun AppleEmojiPickerPanel(
|
||||
CategoryButton(
|
||||
category = category,
|
||||
isSelected = selectedCategory == category,
|
||||
onClick = { selectedCategory = category }
|
||||
onClick = { selectedCategory = category },
|
||||
isDarkTheme = isDarkTheme
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider(
|
||||
color = dividerColor,
|
||||
thickness = 0.5.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Сетка эмодзи
|
||||
// Сетка эмодзи с оптимизированной загрузкой
|
||||
LazyVerticalGrid(
|
||||
state = gridState,
|
||||
columns = GridCells.Fixed(9),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
items(currentEmojis) { unified ->
|
||||
items(
|
||||
items = currentEmojis,
|
||||
key = { it } // Используем unified как ключ для оптимизации
|
||||
) { unified ->
|
||||
EmojiButton(
|
||||
unified = unified,
|
||||
onClick = { emoji ->
|
||||
@@ -308,5 +414,8 @@ fun AppleEmojiPickerPanel(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Отступ снизу для navigation bar
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user