feat: Enhance ChatsListScreen with animated title and search field transitions
This commit is contained in:
@@ -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,21 +571,38 @@ 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 {
|
||||
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++
|
||||
@@ -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,13 +667,16 @@ fun ChatsListScreen(
|
||||
tint = textColor
|
||||
)
|
||||
}
|
||||
|
||||
// Search input with underline animation
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
TextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
"Search chats...",
|
||||
color = secondaryTextColor
|
||||
color = secondaryTextColor.copy(alpha = 0.7f)
|
||||
)
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
@@ -670,17 +689,75 @@ fun ChatsListScreen(
|
||||
unfocusedIndicatorColor = Color.Transparent
|
||||
),
|
||||
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 = {
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user