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,21 +571,38 @@ 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
.graphicsLayer {
alpha = 1f - searchProgress
translationX = -100f * searchProgress
scaleX = 1f - (0.2f * searchProgress)
scaleY = 1f - (0.2f * searchProgress)
}
.clickable {
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime < 500) { if (currentTime - lastClickTime < 500) {
titleClickCount++ titleClickCount++
@@ -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,13 +667,16 @@ fun ChatsListScreen(
tint = textColor tint = textColor
) )
} }
// Search input with underline animation
Box(modifier = Modifier.weight(1f)) {
TextField( TextField(
value = searchQuery, value = searchQuery,
onValueChange = { searchQuery = it }, onValueChange = { searchQuery = it },
placeholder = { placeholder = {
Text( Text(
"Search chats...", "Search chats...",
color = secondaryTextColor color = secondaryTextColor.copy(alpha = 0.7f)
) )
}, },
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
@@ -670,17 +689,75 @@ fun ChatsListScreen(
unfocusedIndicatorColor = Color.Transparent unfocusedIndicatorColor = Color.Transparent
), ),
singleLine = true, singleLine = true,
modifier = Modifier.weight(1f) 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
)
}
}
} }
} }
} }
}, },
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(