feat: Replace alpha animations with crossfade for smoother transitions in ChatsListScreen

This commit is contained in:
k1ngsterr1
2026-01-12 15:42:06 +05:00
parent 99121ce996
commit f6c2fd5e1e

View File

@@ -416,24 +416,56 @@ fun ChatsListScreen(
)
) {
key(isDarkTheme) {
TopAppBar(
navigationIcon = {
// Burger menu - скрывается при поиске
if (!isSearchExpanded) {
IconButton(
onClick = {
scope.launch { drawerState.open() }
}
) {
Icon(
Icons.Default.Menu,
contentDescription = "Menu",
tint = textColor
)
// Custom header с фиксированной структурой для плавной анимации без скачков
Surface(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding(),
color = backgroundColor
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
) {
// Burger menu - всегда на месте, только меняет alpha
val burgerAlpha by animateFloatAsState(
targetValue = if (isSearchExpanded) 0f else 1f,
animationSpec = tween(
durationMillis = 250,
easing = FastOutSlowInEasing
),
label = "burgerAlpha"
)
IconButton(
onClick = {
if (!isSearchExpanded) {
scope.launch { drawerState.open() }
}
}
},
title = {
},
enabled = !isSearchExpanded,
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 4.dp)
.graphicsLayer {
alpha = burgerAlpha
}
) {
Icon(
Icons.Default.Menu,
contentDescription = "Menu",
tint = textColor
)
}
// Center content - Title и Search в одном месте
Box(
modifier = Modifier
.align(Alignment.Center)
.fillMaxWidth()
.padding(horizontal = 60.dp)
) {
val focusRequester = remember {
FocusRequester()
} // Auto-focus when search opens
@@ -444,82 +476,21 @@ fun ChatsListScreen(
}
}
// Animate alpha for smooth fade without position changes
val titleAlpha by animateFloatAsState(
targetValue = if (isSearchExpanded) 0f else 1f,
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing),
label = "titleAlpha"
)
val searchAlpha by animateFloatAsState(
targetValue = if (isSearchExpanded) 1f else 0f,
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
label = "searchAlpha"
)
Box(modifier = Modifier.fillMaxWidth()) {
// Title - Triple click to open dev console (stays in place, just fades)
if (titleAlpha > 0.01f) {
// Crossfade for smooth transition without position jumps
Crossfade(
targetState = isSearchExpanded,
animationSpec = tween(
durationMillis = 300,
easing = FastOutSlowInEasing
),
modifier = Modifier.fillMaxWidth(),
label = "searchCrossfade"
) { expanded ->
if (expanded) {
// Search Mode
Row(
modifier = Modifier
.clickable {
val currentTime =
System.currentTimeMillis()
if (currentTime -
lastClickTime <
500
) {
titleClickCount++
if (titleClickCount >=
3
) {
showDevConsole =
true
titleClickCount =
0
}
} else {
titleClickCount =
1
}
lastClickTime =
currentTime
}
.graphicsLayer { alpha = titleAlpha },
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Rosetta",
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
color = textColor
)
Spacer(modifier = Modifier.width(8.dp))
// Status indicator dot
Box(
modifier = Modifier
.size(10.dp)
.clip(CircleShape)
.background(
when (protocolState) {
ProtocolState.AUTHENTICATED -> Color(0xFF4CAF50) // Green
ProtocolState.CONNECTING, ProtocolState.CONNECTED, ProtocolState.HANDSHAKING -> Color(0xFFFFC107) // Yellow
ProtocolState.DISCONNECTED -> Color(0xFFF44336) // Red
}
)
.clickable {
showStatusDialog = true
}
)
}
}
// Search TextField (appears on top with fade)
if (searchAlpha > 0.01f) {
Row(
verticalAlignment =
Alignment.CenterVertically,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
.graphicsLayer { alpha = searchAlpha }
) {
// Back arrow
IconButton(
@@ -638,80 +609,106 @@ fun ChatsListScreen(
}
}
}
} else {
// Title Mode - Triple click to open dev console
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
val currentTime =
System.currentTimeMillis()
if (currentTime -
lastClickTime <
500
) {
titleClickCount++
if (titleClickCount >=
3
) {
showDevConsole =
true
titleClickCount =
0
}
} else {
titleClickCount =
1
}
lastClickTime =
currentTime
},
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Rosetta",
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
color = textColor
)
Spacer(modifier = Modifier.width(8.dp))
// Status indicator dot
Box(
modifier = Modifier
.size(10.dp)
.clip(CircleShape)
.background(
when (protocolState) {
ProtocolState.AUTHENTICATED -> Color(0xFF4CAF50) // Green
ProtocolState.CONNECTING, ProtocolState.CONNECTED, ProtocolState.HANDSHAKING -> Color(0xFFFFC107) // Yellow
ProtocolState.DISCONNECTED -> Color(0xFFF44336) // Red
}
)
.clickable {
showStatusDialog = true
}
)
}
}
}
},
actions = {
// Animated search button with fade
val searchButtonScale by
animateFloatAsState(
targetValue =
if (isSearchExpanded) 0f
else 1f,
animationSpec =
tween(
durationMillis = 200,
easing = FastOutSlowInEasing
),
label = "searchButtonScale"
)
val searchButtonAlpha by
animateFloatAsState(
targetValue =
if (isSearchExpanded) 0f
else 1f,
animationSpec =
tween(
durationMillis = 200,
easing = FastOutSlowInEasing
),
label = "searchButtonAlpha"
)
if (searchButtonScale > 0.01f) {
IconButton(
onClick = {
if (protocolState ==
ProtocolState
.AUTHENTICATED
) {
searchViewModel.expandSearch()
}
},
enabled =
protocolState ==
ProtocolState.AUTHENTICATED,
modifier =
Modifier.graphicsLayer {
scaleX = searchButtonScale
scaleY = searchButtonScale
alpha =
searchButtonAlpha *
if (protocolState ==
ProtocolState
.AUTHENTICATED
)
1f
else 0.5f
}
) {
Icon(
Icons.Default.Search,
contentDescription = "Search",
tint = textColor
)
}
// Search button - справа, всегда на месте
val searchButtonScale by animateFloatAsState(
targetValue = if (isSearchExpanded) 0f else 1f,
animationSpec = tween(
durationMillis = 250,
easing = FastOutSlowInEasing
),
label = "searchButtonScale"
)
val searchButtonAlpha by animateFloatAsState(
targetValue = if (isSearchExpanded) 0f else 1f,
animationSpec = tween(
durationMillis = 250,
easing = FastOutSlowInEasing
),
label = "searchButtonAlpha"
)
IconButton(
onClick = {
if (protocolState == ProtocolState.AUTHENTICATED) {
searchViewModel.expandSearch()
}
}
},
colors =
TopAppBarDefaults.topAppBarColors(
containerColor = backgroundColor,
scrolledContainerColor = backgroundColor,
navigationIconContentColor = textColor,
titleContentColor = textColor,
actionIconContentColor = textColor
)
)
},
enabled = protocolState == ProtocolState.AUTHENTICATED && !isSearchExpanded,
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 4.dp)
.graphicsLayer {
scaleX = searchButtonScale
scaleY = searchButtonScale
alpha = searchButtonAlpha * if (protocolState == ProtocolState.AUTHENTICATED) 1f else 0.5f
}
) {
Icon(
Icons.Default.Search,
contentDescription = "Search",
tint = textColor
)
}
}
}
}
}
},