feat: Enhance ChatsListScreen with animated title and search field transitions

This commit is contained in:
k1ngsterr1
2026-01-09 02:05:05 +05:00
parent 1a0679954f
commit e29a851510

View File

@@ -19,8 +19,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment 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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -567,33 +571,50 @@ fun ChatsListScreen(
} }
}, },
title = { 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()) { Box(modifier = Modifier.fillMaxWidth()) {
// Title - Triple click to open dev console // Title - Triple click to open dev console
AnimatedVisibility( if (searchProgress < 1f) {
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)
)
) {
Column( Column(
modifier = Modifier.clickable { modifier = Modifier
val currentTime = System.currentTimeMillis() .graphicsLayer {
if (currentTime - lastClickTime < 500) { alpha = 1f - searchProgress
titleClickCount++ translationX = -100f * searchProgress
if (titleClickCount >= 3) { scaleX = 1f - (0.2f * searchProgress)
showDevConsole = true scaleY = 1f - (0.2f * searchProgress)
titleClickCount = 0 }
} .clickable {
} else { val currentTime = System.currentTimeMillis()
titleClickCount = 1 if (currentTime - lastClickTime < 500) {
titleClickCount++
if (titleClickCount >= 3) {
showDevConsole = true
titleClickCount = 0
}
} else {
titleClickCount = 1
}
lastClickTime = currentTime
} }
lastClickTime = currentTime
}
) { ) {
Text( Text(
"Rosetta", "Rosetta",
@@ -617,32 +638,27 @@ fun ChatsListScreen(
} }
} }
// Search TextField with beautiful animation // Search TextField with awesome animation
AnimatedVisibility( if (searchProgress > 0f) {
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)
)
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, 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( IconButton(
onClick = { onClick = {
isSearchExpanded = false isSearchExpanded = false
searchQuery = "" searchQuery = ""
},
modifier = Modifier.graphicsLayer {
rotationZ = -90f * (1f - searchProgress)
} }
) { ) {
Icon( Icon(
@@ -651,36 +667,97 @@ fun ChatsListScreen(
tint = textColor tint = textColor
) )
} }
TextField(
value = searchQuery, // Search input with underline animation
onValueChange = { searchQuery = it }, Box(modifier = Modifier.weight(1f)) {
placeholder = { TextField(
Text( value = searchQuery,
"Search chats...", onValueChange = { searchQuery = it },
color = secondaryTextColor 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 = { 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( IconButton(
onClick = { onClick = { isSearchExpanded = true },
isSearchExpanded = true modifier = Modifier.graphicsLayer {
scaleX = searchButtonScale
scaleY = searchButtonScale
rotationZ = searchButtonRotation
} }
) { ) {
Icon( Icon(