From e29a8515100e1b9161901e90c4e06a82e4c2dc76 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Fri, 9 Jan 2026 02:05:05 +0500 Subject: [PATCH] feat: Enhance ChatsListScreen with animated title and search field transitions --- .../messenger/ui/chats/ChatsListScreen.kt | 207 ++++++++++++------ 1 file changed, 142 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index bc22d96..47c0343 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -19,8 +19,12 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.text.font.FontWeight @@ -567,33 +571,50 @@ fun ChatsListScreen( } }, title = { + val focusRequester = remember { FocusRequester() } + + // Auto-focus when search opens + LaunchedEffect(isSearchExpanded) { + if (isSearchExpanded) { + kotlinx.coroutines.delay(150) + focusRequester.requestFocus() + } + } + + // Animated transition between title and search + val searchProgress by animateFloatAsState( + targetValue = if (isSearchExpanded) 1f else 0f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow + ), + label = "searchProgress" + ) + Box(modifier = Modifier.fillMaxWidth()) { // Title - Triple click to open dev console - AnimatedVisibility( - visible = !isSearchExpanded, - enter = fadeIn(tween(300)) + slideInHorizontally( - initialOffsetX = { -it / 3 }, - animationSpec = tween(300, easing = FastOutSlowInEasing) - ), - exit = fadeOut(tween(200)) + slideOutHorizontally( - targetOffsetX = { -it / 3 }, - animationSpec = tween(200) - ) - ) { + if (searchProgress < 1f) { Column( - modifier = Modifier.clickable { - val currentTime = System.currentTimeMillis() - if (currentTime - lastClickTime < 500) { - titleClickCount++ - if (titleClickCount >= 3) { - showDevConsole = true - titleClickCount = 0 - } - } else { - titleClickCount = 1 + modifier = Modifier + .graphicsLayer { + alpha = 1f - searchProgress + translationX = -100f * searchProgress + scaleX = 1f - (0.2f * searchProgress) + scaleY = 1f - (0.2f * searchProgress) + } + .clickable { + val currentTime = System.currentTimeMillis() + if (currentTime - lastClickTime < 500) { + titleClickCount++ + if (titleClickCount >= 3) { + showDevConsole = true + titleClickCount = 0 + } + } else { + titleClickCount = 1 + } + lastClickTime = currentTime } - lastClickTime = currentTime - } ) { Text( "Rosetta", @@ -617,32 +638,27 @@ fun ChatsListScreen( } } - // Search TextField with beautiful animation - AnimatedVisibility( - visible = isSearchExpanded, - enter = fadeIn(tween(300)) + slideInHorizontally( - initialOffsetX = { it }, - animationSpec = tween(300, easing = FastOutSlowInEasing) - ) + expandHorizontally( - expandFrom = Alignment.End, - animationSpec = tween(300, easing = FastOutSlowInEasing) - ), - exit = fadeOut(tween(200)) + slideOutHorizontally( - targetOffsetX = { it }, - animationSpec = tween(200) - ) + shrinkHorizontally( - shrinkTowards = Alignment.End, - animationSpec = tween(200) - ) - ) { + // Search TextField with awesome animation + if (searchProgress > 0f) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .graphicsLayer { + alpha = searchProgress + translationX = 200f * (1f - searchProgress) + scaleX = 0.8f + (0.2f * searchProgress) + scaleY = 0.8f + (0.2f * searchProgress) + } ) { + // Animated back arrow IconButton( onClick = { isSearchExpanded = false searchQuery = "" + }, + modifier = Modifier.graphicsLayer { + rotationZ = -90f * (1f - searchProgress) } ) { Icon( @@ -651,36 +667,97 @@ fun ChatsListScreen( tint = textColor ) } - TextField( - value = searchQuery, - onValueChange = { searchQuery = it }, - placeholder = { - Text( - "Search chats...", - color = secondaryTextColor + + // Search input with underline animation + Box(modifier = Modifier.weight(1f)) { + TextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + placeholder = { + Text( + "Search chats...", + color = secondaryTextColor.copy(alpha = 0.7f) + ) + }, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + focusedTextColor = textColor, + unfocusedTextColor = textColor, + cursorColor = PrimaryBlue, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + ) + + // Animated underline + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth(searchProgress) + .height(2.dp) + .background( + PrimaryBlue.copy(alpha = 0.8f * searchProgress), + RoundedCornerShape(1.dp) + ) + ) + } + + // Clear button with bounce animation + if (searchQuery.isNotEmpty()) { + val clearScale by animateFloatAsState( + targetValue = 1f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ), + label = "clearScale" + ) + IconButton( + onClick = { searchQuery = "" }, + modifier = Modifier.scale(clearScale) + ) { + Icon( + Icons.Default.Clear, + contentDescription = "Clear", + tint = secondaryTextColor ) - }, - colors = TextFieldDefaults.colors( - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - focusedTextColor = textColor, - unfocusedTextColor = textColor, - cursorColor = PrimaryBlue, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent - ), - singleLine = true, - modifier = Modifier.weight(1f) - ) + } + } } } } }, actions = { - if (!isSearchExpanded) { + // Animated search button with scale and rotation + val searchButtonScale by animateFloatAsState( + targetValue = if (isSearchExpanded) 0f else 1f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ), + label = "searchButtonScale" + ) + val searchButtonRotation by animateFloatAsState( + targetValue = if (isSearchExpanded) 180f else 0f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioLowBouncy, + stiffness = Spring.StiffnessLow + ), + label = "searchButtonRotation" + ) + + if (searchButtonScale > 0.01f) { IconButton( - onClick = { - isSearchExpanded = true + onClick = { isSearchExpanded = true }, + modifier = Modifier.graphicsLayer { + scaleX = searchButtonScale + scaleY = searchButtonScale + rotationZ = searchButtonRotation } ) { Icon(