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.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(