feat: Improve keyboard handling and fade-out animation in ChatDetailScreen
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user