refactor(ui): 优化首页左下角 FAB 浮动菜单视觉样式、动画与返回键拦截

This commit is contained in:
xfy 2026-07-24 11:34:15 +08:00
parent 94a4ba1e2a
commit dfe7b5a6c3

View File

@ -3,6 +3,18 @@
package plus.rua.project.ui package plus.rua.project.ui
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.Build
import androidx.compose.material.icons.outlined.CalendarMonth
import androidx.compose.material.icons.outlined.DateRange
import androidx.compose.material.icons.outlined.EventAvailable
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Tune
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExitTransition
@ -382,6 +394,26 @@ fun CalendarMonthView(
} }
} }
} }
BackHandler(enabled = isMenuExpanded) {
isMenuExpanded = false
}
// Scrim半透明遮罩在菜单展开时淡入拦截点击关闭菜单增强渐变视觉与聚焦感
AnimatedVisibility(
visible = isMenuExpanded,
enter = fadeIn(tween(200)),
exit = fadeOut(tween(200))
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.32f))
.pointerInput(Unit) {
detectTapGestures { isMenuExpanded = false }
}
)
}
// FAB 浮动按钮 // FAB 浮动按钮
FloatingActionButton( FloatingActionButton(
onClick = { isMenuExpanded = !isMenuExpanded }, onClick = { isMenuExpanded = !isMenuExpanded },
@ -396,57 +428,60 @@ fun CalendarMonthView(
AnimatedContent( AnimatedContent(
targetState = isMenuExpanded, targetState = isMenuExpanded,
transitionSpec = { transitionSpec = {
fadeIn(tween(200)) togetherWith fadeOut(tween(200)) (fadeIn(tween(200)) + scaleIn(initialScale = 0.8f)) togetherWith
(fadeOut(tween(200)) + scaleOut(targetScale = 0.8f))
}, },
label = "fab_icon" label = "fab_icon"
) { expanded -> ) { expanded ->
val rotation by animateFloatAsState(
targetValue = if (expanded) 180f else 0f,
animationSpec = spring(
stiffness = Spring.StiffnessMediumLow,
dampingRatio = Spring.DampingRatioLowBouncy
),
label = "fab_rotation"
)
Icon( Icon(
imageVector = if (expanded) Icons.Filled.Close else Icons.Filled.Menu, imageVector = if (expanded) Icons.Filled.Close else Icons.Filled.Menu,
contentDescription = if (expanded) "关闭菜单" else "打开菜单" contentDescription = if (expanded) "关闭菜单" else "打开菜单",
modifier = Modifier.graphicsLayer { rotationZ = rotation }
) )
} }
} }
// Scrim全透明仅拦截点击关闭菜单无动画 // 物理弹簧缩放动画菜单
AnimatedVisibility(
visible = isMenuExpanded,
enter = EnterTransition.None,
exit = ExitTransition.None
) {
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures { isMenuExpanded = false }
}
)
}
// 缩放动画菜单
AnimatedVisibility( AnimatedVisibility(
visible = isMenuExpanded, visible = isMenuExpanded,
enter = scaleIn( enter = scaleIn(
initialScale = 0.2f, initialScale = 0.2f,
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), animationSpec = spring(
stiffness = Spring.StiffnessMediumLow,
dampingRatio = Spring.DampingRatioLowBouncy
),
transformOrigin = TransformOrigin(0f, 1f) transformOrigin = TransformOrigin(0f, 1f)
) + fadeIn(tween(150)), ) + fadeIn(tween(180)),
exit = scaleOut( exit = scaleOut(
targetScale = 0.2f, targetScale = 0.2f,
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing), animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing),
transformOrigin = TransformOrigin(0f, 1f) transformOrigin = TransformOrigin(0f, 1f)
) + fadeOut(tween(100)), ) + fadeOut(tween(120)),
modifier = Modifier modifier = Modifier
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
.padding(start = 24.dp, bottom = 32.dp + 56.dp + 8.dp) .padding(start = 24.dp, bottom = 32.dp + 56.dp + 8.dp)
) { ) {
Card( Card(
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh) colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh)
) { ) {
Column(modifier = Modifier.width(140.dp)) { Column(
modifier = Modifier
.width(170.dp)
.padding(vertical = 4.dp)
) {
MenuItem( MenuItem(
text = "月视图", text = "月视图",
icon = Icons.Outlined.CalendarMonth,
selected = !isYearView, selected = !isYearView,
onClick = { onClick = {
isMenuExpanded = false isMenuExpanded = false
@ -459,6 +494,7 @@ fun CalendarMonthView(
) )
MenuItem( MenuItem(
text = "年视图", text = "年视图",
icon = Icons.Outlined.DateRange,
selected = isYearView, selected = isYearView,
onClick = { onClick = {
isMenuExpanded = false isMenuExpanded = false
@ -471,11 +507,12 @@ fun CalendarMonthView(
) )
HorizontalDivider( HorizontalDivider(
thickness = 1.dp, thickness = 1.dp,
color = MaterialTheme.colorScheme.outlineVariant, color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
modifier = Modifier.padding(horizontal = 8.dp) modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp)
) )
MenuItem( MenuItem(
text = "显示调休", text = "显示调休",
icon = Icons.Outlined.EventAvailable,
selected = showLegalHoliday, selected = showLegalHoliday,
onClick = { onClick = {
isMenuExpanded = false isMenuExpanded = false
@ -484,6 +521,7 @@ fun CalendarMonthView(
) )
MenuItem( MenuItem(
text = "班次设置", text = "班次设置",
icon = Icons.Outlined.Tune,
selected = false, selected = false,
onClick = { onClick = {
isMenuExpanded = false isMenuExpanded = false
@ -492,11 +530,12 @@ fun CalendarMonthView(
) )
HorizontalDivider( HorizontalDivider(
thickness = 1.dp, thickness = 1.dp,
color = MaterialTheme.colorScheme.outlineVariant, color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
modifier = Modifier.padding(horizontal = 8.dp) modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp)
) )
MenuItem( MenuItem(
text = "工具", text = "工具",
icon = Icons.Outlined.Build,
selected = false, selected = false,
onClick = { onClick = {
isMenuExpanded = false isMenuExpanded = false
@ -505,6 +544,7 @@ fun CalendarMonthView(
) )
MenuItem( MenuItem(
text = "关于", text = "关于",
icon = Icons.Outlined.Info,
selected = false, selected = false,
onClick = { onClick = {
isMenuExpanded = false isMenuExpanded = false
@ -692,6 +732,7 @@ private fun BottomCardArea(
@Composable @Composable
private fun MenuItem( private fun MenuItem(
text: String, text: String,
icon: ImageVector,
selected: Boolean, selected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@ -700,19 +741,43 @@ private fun MenuItem(
onClick = onClick, onClick = onClick,
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = if (selected) MaterialTheme.colorScheme.primaryContainer containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
else MaterialTheme.colorScheme.surfaceContainerHigh
), ),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
modifier = modifier.fillMaxWidth() modifier = modifier
.fillMaxWidth()
.padding(horizontal = 4.dp)
) { ) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = if (selected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text( Text(
text = text, text = text,
color = if (selected) MaterialTheme.colorScheme.onPrimaryContainer color = if (selected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurface, else MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp) modifier = Modifier.weight(1f)
) )
if (selected) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = "已选择",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(18.dp)
)
}
}
} }
} }