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) { key(isDarkTheme) {
TopAppBar( // Custom header с фиксированной структурой для плавной анимации без скачков
navigationIcon = { Surface(
// Burger menu - скрывается при поиске modifier = Modifier
if (!isSearchExpanded) { .fillMaxWidth()
IconButton( .statusBarsPadding(),
onClick = { color = backgroundColor
scope.launch { drawerState.open() } ) {
} Box(
) { modifier = Modifier
Icon( .fillMaxWidth()
Icons.Default.Menu, .height(64.dp)
contentDescription = "Menu", ) {
tint = textColor // 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() }
} }
} },
}, enabled = !isSearchExpanded,
title = { 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 { val focusRequester = remember {
FocusRequester() FocusRequester()
} // Auto-focus when search opens } // Auto-focus when search opens
@@ -444,82 +476,21 @@ fun ChatsListScreen(
} }
} }
// Animate alpha for smooth fade without position changes // Crossfade for smooth transition without position jumps
val titleAlpha by animateFloatAsState( Crossfade(
targetValue = if (isSearchExpanded) 0f else 1f, targetState = isSearchExpanded,
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing), animationSpec = tween(
label = "titleAlpha" durationMillis = 300,
) easing = FastOutSlowInEasing
val searchAlpha by animateFloatAsState( ),
targetValue = if (isSearchExpanded) 1f else 0f, modifier = Modifier.fillMaxWidth(),
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing), label = "searchCrossfade"
label = "searchAlpha" ) { expanded ->
) if (expanded) {
// Search Mode
Box(modifier = Modifier.fillMaxWidth()) {
// Title - Triple click to open dev console (stays in place, just fades)
if (titleAlpha > 0.01f) {
Row( Row(
modifier = Modifier verticalAlignment = Alignment.CenterVertically,
.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,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.graphicsLayer { alpha = searchAlpha }
) { ) {
// Back arrow // Back arrow
IconButton( 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) { // Search button - справа, всегда на месте
IconButton( val searchButtonScale by animateFloatAsState(
onClick = { targetValue = if (isSearchExpanded) 0f else 1f,
if (protocolState == animationSpec = tween(
ProtocolState durationMillis = 250,
.AUTHENTICATED easing = FastOutSlowInEasing
) { ),
searchViewModel.expandSearch() label = "searchButtonScale"
} )
}, val searchButtonAlpha by animateFloatAsState(
enabled = targetValue = if (isSearchExpanded) 0f else 1f,
protocolState == animationSpec = tween(
ProtocolState.AUTHENTICATED, durationMillis = 250,
modifier = easing = FastOutSlowInEasing
Modifier.graphicsLayer { ),
scaleX = searchButtonScale label = "searchButtonAlpha"
scaleY = searchButtonScale )
alpha =
searchButtonAlpha * IconButton(
if (protocolState == onClick = {
ProtocolState if (protocolState == ProtocolState.AUTHENTICATED) {
.AUTHENTICATED searchViewModel.expandSearch()
)
1f
else 0.5f
}
) {
Icon(
Icons.Default.Search,
contentDescription = "Search",
tint = textColor
)
} }
} },
}, enabled = protocolState == ProtocolState.AUTHENTICATED && !isSearchExpanded,
colors = modifier = Modifier
TopAppBarDefaults.topAppBarColors( .align(Alignment.CenterEnd)
containerColor = backgroundColor, .padding(end = 4.dp)
scrolledContainerColor = backgroundColor, .graphicsLayer {
navigationIconContentColor = textColor, scaleX = searchButtonScale
titleContentColor = textColor, scaleY = searchButtonScale
actionIconContentColor = textColor alpha = searchButtonAlpha * if (protocolState == ProtocolState.AUTHENTICATED) 1f else 0.5f
) }
) ) {
Icon(
Icons.Default.Search,
contentDescription = "Search",
tint = textColor
)
}
}
}
} }
} }
}, },