feat: Optimize AppleEmojiPicker with caching and enhance UI with Liquid Glass style

This commit is contained in:
k1ngsterr1
2026-01-12 02:09:56 +05:00
parent 5f348f329e
commit 7bac22850e
2 changed files with 214 additions and 62 deletions

View File

@@ -56,8 +56,11 @@ import com.rosetta.messenger.ui.components.VerifiedBadge
import com.rosetta.messenger.ui.onboarding.PrimaryBlue import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.content.Context import android.content.Context
import android.graphics.Rect
import android.view.ViewTreeObserver
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.LocalDensity
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -285,14 +288,41 @@ fun ChatDetailScreen(
// 🔥 Обработка системной кнопки назад // 🔥 Обработка системной кнопки назад
BackHandler { hideKeyboardAndBack() } 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 { onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(listener)
focusManager.clearFocus() focusManager.clearFocus()
keyboardController?.hide() keyboardController?.hide()
} }
} }
// Автоматический скролл вниз при появлении клавиатуры
LaunchedEffect(keyboardHeight) {
if (keyboardHeight > 0.dp && messages.isNotEmpty()) {
delay(50)
listState.animateScrollToItem(0)
}
}
// Инициализируем ViewModel с ключами и открываем диалог // Инициализируем ViewModel с ключами и открываем диалог
LaunchedEffect(user.publicKey) { LaunchedEffect(user.publicKey) {
viewModel.setUserKeys(currentUserPublicKey, currentUserPrivateKey) viewModel.setUserKeys(currentUserPublicKey, currentUserPrivateKey)
@@ -549,15 +579,21 @@ fun ChatDetailScreen(
}, },
containerColor = Color.Transparent containerColor = Color.Transparent
) { paddingValues -> ) { paddingValues ->
// 🔥 Column с imePadding - весь контент поднимается с клавиатурой // 🔥 Box с фиксированным Input внизу и сообщениями выше
Column( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
.imePadding() // Контент поднимается над клавиатурой
) { ) {
// Список сообщений - занимает всё доступное пространство // Список сообщений - занимает всё пространство, с padding снизу для input и клавиатуры
Box(modifier = Modifier.weight(1f).fillMaxWidth()) { val inputBarHeight = 60.dp
val bottomPadding = inputBarHeight + keyboardHeight
Box(
modifier = Modifier
.fillMaxSize()
.padding(bottom = bottomPadding)
) {
if (messages.isEmpty()) { if (messages.isEmpty()) {
// Пустое состояние // Пустое состояние
Column( Column(
@@ -723,9 +759,15 @@ fun ChatDetailScreen(
) )
} }
} }
} } // Закрытие внутреннего Box для сообщений
// 🔥 INPUT BAR - внизу Column, автоматически над клавиатурой // 🔥 INPUT BAR - фиксированный элемент внизу экрана
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(bottom = keyboardHeight) // Ручной padding над клавиатурой
) {
MessageInputBar( MessageInputBar(
value = inputText, value = inputText,
onValueChange = { onValueChange = {
@@ -754,6 +796,7 @@ fun ChatDetailScreen(
) )
} }
} }
}
} // Закрытие Box с fade-in } // Закрытие Box с fade-in
// Диалог логов // Диалог логов

View File

@@ -3,6 +3,7 @@ package com.rosetta.messenger.ui.components
import android.content.Context import android.content.Context
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
@@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.*
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.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.* import androidx.compose.material3.*
@@ -18,12 +20,15 @@ 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.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
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
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
@@ -147,6 +152,7 @@ fun unifiedToEmoji(unified: String): String {
/** /**
* Кнопка эмодзи с PNG изображением и анимацией нажатия * Кнопка эмодзи с PNG изображением и анимацией нажатия
* Оптимизирована с кэшированием
*/ */
@Composable @Composable
fun EmojiButton( fun EmojiButton(
@@ -167,6 +173,19 @@ fun EmojiButton(
label = "emojiScale" 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( Box(
modifier = modifier modifier = modifier
.aspectRatio(1f) .aspectRatio(1f)
@@ -181,10 +200,7 @@ fun EmojiButton(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
AsyncImage( AsyncImage(
model = ImageRequest.Builder(context) model = imageRequest,
.data("file:///android_asset/emoji/${unified.lowercase()}.png")
.crossfade(false)
.build(),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(32.dp), modifier = Modifier.size(32.dp),
contentScale = ContentScale.Fit contentScale = ContentScale.Fit
@@ -200,6 +216,7 @@ fun CategoryButton(
category: EmojiCategory, category: EmojiCategory,
isSelected: Boolean, isSelected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
isDarkTheme: Boolean = true,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -215,13 +232,26 @@ fun CategoryButton(
label = "categoryScale" 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( Box(
modifier = modifier modifier = modifier
.size(40.dp) .size(40.dp)
.scale(scale) .scale(scale)
.clip(CircleShape) .clip(CircleShape)
.background( .background(
if (isSelected) PrimaryBlue.copy(alpha = 0.2f) else Color.Transparent if (isSelected) PrimaryBlue.copy(alpha = 0.25f) else Color.Transparent
) )
.clickable( .clickable(
interactionSource = interactionSource, interactionSource = interactionSource,
@@ -231,10 +261,7 @@ fun CategoryButton(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
AsyncImage( AsyncImage(
model = ImageRequest.Builder(context) model = imageRequest,
.data("file:///android_asset/emoji/${category.label.lowercase()}.png")
.crossfade(false)
.build(),
contentDescription = category.key, contentDescription = category.key,
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
contentScale = ContentScale.Fit contentScale = ContentScale.Fit
@@ -243,7 +270,7 @@ fun CategoryButton(
} }
/** /**
* Apple Emoji Picker Panel * Apple Emoji Picker Panel - Liquid Glass стиль
*/ */
@Composable @Composable
fun AppleEmojiPickerPanel( fun AppleEmojiPickerPanel(
@@ -253,25 +280,102 @@ fun AppleEmojiPickerPanel(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var selectedCategory by remember { mutableStateOf(EMOJI_CATEGORIES[0]) } 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 currentEmojis = remember(selectedCategory.key) {
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3C) else Color(0xFFE5E5EA) 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( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.height(300.dp) .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( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(categoryBackground) .padding(horizontal = 12.dp)
.padding(horizontal = 8.dp, vertical = 6.dp), .clip(RoundedCornerShape(16.dp))
.background(categoryBarBackground)
.padding(horizontal = 4.dp, vertical = 6.dp),
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -279,27 +383,29 @@ fun AppleEmojiPickerPanel(
CategoryButton( CategoryButton(
category = category, category = category,
isSelected = selectedCategory == category, isSelected = selectedCategory == category,
onClick = { selectedCategory = category } onClick = { selectedCategory = category },
isDarkTheme = isDarkTheme
) )
} }
} }
Divider( Spacer(modifier = Modifier.height(8.dp))
color = dividerColor,
thickness = 0.5.dp
)
// Сетка эмодзи // Сетка эмодзи с оптимизированной загрузкой
LazyVerticalGrid( LazyVerticalGrid(
state = gridState,
columns = GridCells.Fixed(9), columns = GridCells.Fixed(9),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
.padding(horizontal = 8.dp, vertical = 8.dp), .padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp), horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalArrangement = Arrangement.spacedBy(2.dp) verticalArrangement = Arrangement.spacedBy(2.dp)
) { ) {
items(currentEmojis) { unified -> items(
items = currentEmojis,
key = { it } // Используем unified как ключ для оптимизации
) { unified ->
EmojiButton( EmojiButton(
unified = unified, unified = unified,
onClick = { emoji -> onClick = { emoji ->
@@ -308,5 +414,8 @@ fun AppleEmojiPickerPanel(
) )
} }
} }
// Отступ снизу для navigation bar
Spacer(modifier = Modifier.height(8.dp))
} }
} }