From 0f55aef17930e492aeffcb75f82e1a264fdabb80 Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 5 Apr 2026 15:28:41 +0300 Subject: [PATCH] feat: animate pinned and composer state transitions feat: add smooth pinned-message visibility changes and voice composer mode transitions --- .../messenger/ui/chat/ChatScreen.kt | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt index 4073f13..e852403 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt @@ -82,6 +82,8 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.animation.core.LinearEasing @@ -1069,7 +1071,18 @@ private fun ChatScreen( ) } val pinnedMessage = state.pinnedMessage - if (pinnedMessage != null && dismissedPinnedMessageId != pinnedMessage.id) { + AnimatedVisibility( + visible = pinnedMessage != null && dismissedPinnedMessageId != pinnedMessage?.id, + enter = fadeIn(animationSpec = tween(180)) + slideInVertically( + initialOffsetY = { -it / 3 }, + animationSpec = tween(180), + ), + exit = fadeOut(animationSpec = tween(120)) + slideOutVertically( + targetOffsetY = { -it / 3 }, + animationSpec = tween(120), + ), + ) { + val resolvedPinnedMessage = pinnedMessage ?: return@AnimatedVisibility Row( modifier = Modifier .fillMaxWidth() @@ -1092,12 +1105,12 @@ private fun ChatScreen( fontWeight = FontWeight.SemiBold, ) Text( - text = pinnedMessage.text?.takeIf { it.isNotBlank() } ?: "[${pinnedMessage.type}]", + text = resolvedPinnedMessage.text?.takeIf { it.isNotBlank() } ?: "[${resolvedPinnedMessage.type}]", style = MaterialTheme.typography.labelMedium, maxLines = 2, ) } - IconButton(onClick = { dismissedPinnedMessageId = pinnedMessage.id }) { + IconButton(onClick = { dismissedPinnedMessageId = resolvedPinnedMessage.id }) { Icon(imageVector = Icons.Filled.Close, contentDescription = "Hide pinned") } } @@ -1825,7 +1838,17 @@ private fun ChatScreen( .padding(horizontal = 8.dp, vertical = 6.dp), verticalArrangement = Arrangement.spacedBy(6.dp), ) { - if (state.isRecordingVoice) { + AnimatedVisibility( + visible = state.isRecordingVoice, + enter = fadeIn(animationSpec = tween(150)) + slideInVertically( + initialOffsetY = { it / 3 }, + animationSpec = tween(150), + ), + exit = fadeOut(animationSpec = tween(100)) + slideOutVertically( + targetOffsetY = { it / 3 }, + animationSpec = tween(100), + ), + ) { VoiceRecordingStatusRow( durationMs = state.voiceRecordingDurationMs, hint = state.voiceRecordingHint, @@ -1833,7 +1856,18 @@ private fun ChatScreen( onCancel = onVoiceRecordCancel, onSend = onVoiceRecordSend, ) - } else { + } + AnimatedVisibility( + visible = !state.isRecordingVoice, + enter = fadeIn(animationSpec = tween(150)) + slideInVertically( + initialOffsetY = { it / 3 }, + animationSpec = tween(150), + ), + exit = fadeOut(animationSpec = tween(100)) + slideOutVertically( + targetOffsetY = { it / 3 }, + animationSpec = tween(100), + ), + ) { Row( modifier = Modifier .fillMaxWidth()