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 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
|
||||||
|
|
||||||
// Диалог логов
|
// Диалог логов
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user