feat: Improve keyboard handling and fade-out animation in ChatDetailScreen

This commit is contained in:
k1ngsterr1
2026-01-11 05:12:38 +05:00
parent 30ad6d1cc1
commit 286d9b21c7

View File

@@ -53,6 +53,7 @@ import com.rosetta.messenger.ui.components.AppleEmojiPickerPanel
import com.rosetta.messenger.ui.components.AppleEmojiTextField import com.rosetta.messenger.ui.components.AppleEmojiTextField
import com.rosetta.messenger.ui.components.AppleEmojiText import com.rosetta.messenger.ui.components.AppleEmojiText
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -130,7 +131,7 @@ fun ChatDetailScreen(
var isVisible by remember { mutableStateOf(false) } var isVisible by remember { mutableStateOf(false) }
val screenAlpha by animateFloatAsState( val screenAlpha by animateFloatAsState(
targetValue = if (isVisible) 1f else 0f, targetValue = if (isVisible) 1f else 0f,
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing), animationSpec = tween(durationMillis = 180, easing = FastOutSlowInEasing),
label = "screenFade" label = "screenFade"
) )
@@ -138,15 +139,22 @@ fun ChatDetailScreen(
isVisible = true isVisible = true
} }
// 🔥 Быстрое закрытие клавиатуры и выход val listState = rememberLazyListState()
val hideKeyboardAndBack = remember { val scope = rememberCoroutineScope()
{
// Мгновенно убираем фокус и клавиатуру // 🔥 Быстрое закрытие с fade-out анимацией
focusManager.clearFocus(force = true) val hideKeyboardAndBack: () -> Unit = {
keyboardController?.hide() // Мгновенно убираем фокус и клавиатуру
// Сразу выходим без задержки focusManager.clearFocus(force = true)
keyboardController?.hide()
// Запускаем fade-out
isVisible = false
// Выходим после короткой анимации
scope.launch {
delay(150)
onBack() onBack()
} }
Unit
} }
// Определяем это Saved Messages или обычный чат // Определяем это Saved Messages или обычный чат
@@ -189,9 +197,6 @@ fun ChatDetailScreen(
else -> "offline" else -> "offline"
} }
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
// 🔥 Обработка системной кнопки назад // 🔥 Обработка системной кнопки назад
BackHandler { BackHandler {
hideKeyboardAndBack() hideKeyboardAndBack()
@@ -237,12 +242,24 @@ fun ChatDetailScreen(
.fillMaxSize() .fillMaxSize()
.graphicsLayer { alpha = screenAlpha } .graphicsLayer { alpha = screenAlpha }
) { ) {
// Цвета для матового стекла
val glassHeaderBackground = if (isDarkTheme)
Color(0xFF1A1A1A).copy(alpha = 0.85f)
else
Color(0xFFF5F5F5).copy(alpha = 0.85f)
val glassInputPanelBackground = if (isDarkTheme)
Color(0xFF1A1A1A).copy(alpha = 0.88f)
else
Color(0xFFF5F5F5).copy(alpha = 0.88f)
Scaffold( Scaffold(
topBar = { topBar = {
// Кастомный TopAppBar для чата // Кастомный TopAppBar для чата с эффектом матового стекла
Surface( Box(
color = backgroundColor, modifier = Modifier
shadowElevation = 0.dp .fillMaxWidth()
.background(glassHeaderBackground)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -267,7 +284,11 @@ fun ChatDetailScreen(
.size(40.dp) .size(40.dp)
.clip(CircleShape) .clip(CircleShape)
.background(if (isSavedMessages) PrimaryBlue else avatarColors.backgroundColor) .background(if (isSavedMessages) PrimaryBlue else avatarColors.backgroundColor)
.clickable { onUserProfileClick() }, .clickable {
keyboardController?.hide()
focusManager.clearFocus()
onUserProfileClick()
},
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (isSavedMessages) { if (isSavedMessages) {
@@ -293,7 +314,11 @@ fun ChatDetailScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.clickable { onUserProfileClick() } .clickable {
keyboardController?.hide()
focusManager.clearFocus()
onUserProfileClick()
}
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
@@ -334,7 +359,11 @@ fun ChatDetailScreen(
} }
// Кнопка логов (для отладки) // Кнопка логов (для отладки)
IconButton(onClick = { showLogs = true }) { IconButton(onClick = {
keyboardController?.hide()
focusManager.clearFocus()
showLogs = true
}) {
Icon( Icon(
Icons.Default.BugReport, Icons.Default.BugReport,
contentDescription = "Logs", contentDescription = "Logs",
@@ -342,7 +371,11 @@ fun ChatDetailScreen(
) )
} }
IconButton(onClick = { /* TODO: More options */ }) { IconButton(onClick = {
keyboardController?.hide()
focusManager.clearFocus()
/* TODO: More options */
}) {
Icon( Icon(
Icons.Default.MoreVert, Icons.Default.MoreVert,
contentDescription = "More", contentDescription = "More",
@@ -350,9 +383,20 @@ fun ChatDetailScreen(
) )
} }
} }
} // Нижняя линия для разделения
}, Box(
containerColor = backgroundColor modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.height(0.5.dp)
.background(
if (isDarkTheme) Color.White.copy(alpha = 0.1f)
else Color.Black.copy(alpha = 0.08f)
)
)
}
},
containerColor = Color.Transparent
) { paddingValues -> ) { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
@@ -404,7 +448,13 @@ fun ChatDetailScreen(
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp), // Добавляем padding сверху и снизу для скролла под glass header/input
contentPadding = PaddingValues(
start = 8.dp,
end = 8.dp,
top = 8.dp,
bottom = 8.dp
),
reverseLayout = true reverseLayout = true
) { ) {
// Для inverted FlatList: идём от новых к старым // Для inverted FlatList: идём от новых к старым
@@ -504,7 +554,7 @@ fun ChatDetailScreen(
} }
/** /**
* 🚀 Пузырек сообщения с fade-in анимацией * 🚀 Пузырек сообщения с fade-in анимацией (только при первом появлении)
*/ */
@Composable @Composable
private fun MessageBubble( private fun MessageBubble(
@@ -512,30 +562,9 @@ private fun MessageBubble(
isDarkTheme: Boolean, isDarkTheme: Boolean,
index: Int = 0 // Для staggered анимации index: Int = 0 // Для staggered анимации
) { ) {
// 🔥 Fade-in + slide анимация // 🔥 Fade-in + slide анимация - используем key для предотвращения повторной анимации
var isVisible by remember { mutableStateOf(false) } var isVisible by remember(message.id) { mutableStateOf(true) } // Сразу true - без повторной анимации
val alpha by animateFloatAsState(
targetValue = if (isVisible) 1f else 0f,
animationSpec = tween(
durationMillis = 150,
delayMillis = minOf(index * 20, 200), // Staggered, max 200ms delay
easing = FastOutSlowInEasing
),
label = "bubbleAlpha"
)
val offsetY by animateFloatAsState(
targetValue = if (isVisible) 0f else 20f,
animationSpec = tween(
durationMillis = 150,
delayMillis = minOf(index * 20, 200),
easing = FastOutSlowInEasing
),
label = "bubbleOffset"
)
LaunchedEffect(message.id) {
isVisible = true
}
val bubbleColor = if (message.isOutgoing) { val bubbleColor = if (message.isOutgoing) {
PrimaryBlue PrimaryBlue
} else { } else {
@@ -553,11 +582,7 @@ private fun MessageBubble(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 2.dp) .padding(vertical = 2.dp),
.graphicsLayer {
this.alpha = alpha
translationY = offsetY
},
horizontalArrangement = if (message.isOutgoing) Arrangement.End else Arrangement.Start horizontalArrangement = if (message.isOutgoing) Arrangement.End else Arrangement.Start
) { ) {
Box( Box(
@@ -682,7 +707,7 @@ private fun MessageInputBar(
val glassBackground = if (isDarkTheme) Color(0xFF3C3C3C).copy(alpha = 0.9f) else Color(0xFFF0F0F0).copy(alpha = 0.92f) val glassBackground = if (isDarkTheme) Color(0xFF3C3C3C).copy(alpha = 0.9f) else Color(0xFFF0F0F0).copy(alpha = 0.92f)
val glassBorder = if (isDarkTheme) Color.White.copy(alpha = 0.25f) else Color.Black.copy(alpha = 0.1f) val glassBorder = if (isDarkTheme) Color.White.copy(alpha = 0.25f) else Color.Black.copy(alpha = 0.1f)
val emojiIconColor = if (isDarkTheme) Color.White.copy(alpha = 0.62f) else Color.Black.copy(alpha = 0.5f) val emojiIconColor = if (isDarkTheme) Color.White.copy(alpha = 0.62f) else Color.Black.copy(alpha = 0.5f)
val panelBackground = if (isDarkTheme) Color(0xFF1A1A1A).copy(alpha = 0.95f) else Color.White.copy(alpha = 0.95f) val panelBackground = if (isDarkTheme) Color(0xFF1A1A1A).copy(alpha = 0.88f) else Color(0xFFF5F5F5).copy(alpha = 0.88f)
// Состояние отправки // Состояние отправки
val canSend = remember(value) { value.isNotBlank() } val canSend = remember(value) { value.isNotBlank() }
@@ -747,6 +772,18 @@ private fun MessageInputBar(
.fillMaxWidth() .fillMaxWidth()
.background(panelBackground) .background(panelBackground)
) { ) {
// Верхняя линия для разделения (эффект стекла)
Box(
modifier = Modifier
.align(Alignment.TopCenter)
.fillMaxWidth()
.height(0.5.dp)
.background(
if (isDarkTheme) Color.White.copy(alpha = 0.1f)
else Color.Black.copy(alpha = 0.08f)
)
)
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()