feat: Replace alpha animations with crossfade for smoother transitions in ChatsListScreen
This commit is contained in:
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user