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.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(
|
||||||
|
|||||||
Reference in New Issue
Block a user