fix(ui): 修复视图切换器平滑弹簧滑动动画与几何胶囊同心对齐
This commit is contained in:
parent
37cf5ffc4c
commit
80e4e5e86c
@ -17,6 +17,8 @@ import androidx.compose.animation.slideOutVertically
|
|||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
@ -453,42 +455,97 @@ private fun HeaderControlBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 右侧:视图切换按钮组 Segmented Button
|
// 右侧:视图切换按钮组 Segmented Control
|
||||||
|
SegmentedViewModeSwitcher(
|
||||||
|
currentViewMode = currentViewMode,
|
||||||
|
onViewModeChange = onViewModeChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 视图模式分段切换器,带平滑弹簧滑动指示器与精准同心胶囊形状切角。 */
|
||||||
|
@Composable
|
||||||
|
private fun SegmentedViewModeSwitcher(
|
||||||
|
currentViewMode: DateRecorderViewMode,
|
||||||
|
onViewModeChange: (DateRecorderViewMode) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val modes = DateRecorderViewMode.entries
|
||||||
|
val selectedIndex = modes.indexOf(currentViewMode).coerceAtLeast(0)
|
||||||
|
|
||||||
|
val itemWidth = 36.dp
|
||||||
|
val itemHeight = 32.dp
|
||||||
|
val outerPadding = 3.dp
|
||||||
|
|
||||||
|
val indicatorOffset by animateDpAsState(
|
||||||
|
targetValue = outerPadding + (itemWidth * selectedIndex),
|
||||||
|
animationSpec = spring(
|
||||||
|
stiffness = Spring.StiffnessMediumLow,
|
||||||
|
dampingRatio = Spring.DampingRatioMediumBouncy
|
||||||
|
),
|
||||||
|
label = "mode_switcher_indicator_offset"
|
||||||
|
)
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerLow,
|
color = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = CircleShape,
|
||||||
border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outlineVariant)
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
|
||||||
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Box(
|
||||||
modifier = Modifier.padding(2.dp),
|
modifier = Modifier.padding(outerPadding)
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
|
||||||
) {
|
) {
|
||||||
DateRecorderViewMode.entries.forEach { mode ->
|
// 滑动指示器 (Sliding Indicator Background)
|
||||||
val isSelected = mode == currentViewMode
|
Box(
|
||||||
val bgColor by animateColorAsState(
|
modifier = Modifier
|
||||||
targetValue = if (isSelected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent,
|
.graphicsLayer {
|
||||||
animationSpec = tween(200),
|
translationX = indicatorOffset.toPx() - outerPadding.toPx()
|
||||||
label = "view_mode_bg"
|
}
|
||||||
|
.size(width = itemWidth, height = itemHeight)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 图标按键(带柔和手感及颜色渐变)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
modes.forEachIndexed { index, mode ->
|
||||||
|
val isSelected = index == selectedIndex
|
||||||
val iconColor by animateColorAsState(
|
val iconColor by animateColorAsState(
|
||||||
targetValue = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant,
|
targetValue = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
animationSpec = tween(200),
|
animationSpec = tween(200),
|
||||||
label = "view_mode_icon"
|
label = "mode_switcher_icon_color"
|
||||||
)
|
)
|
||||||
IconButton(
|
val iconScale by animateFloatAsState(
|
||||||
onClick = { onViewModeChange(mode) },
|
targetValue = if (isSelected) 1.15f else 1f,
|
||||||
|
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
|
||||||
|
label = "mode_switcher_icon_scale"
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(30.dp)
|
.size(width = itemWidth, height = itemHeight)
|
||||||
.clip(RoundedCornerShape(10.dp))
|
.clip(CircleShape)
|
||||||
.background(bgColor),
|
.clickable(
|
||||||
colors = IconButtonDefaults.iconButtonColors(contentColor = iconColor)
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = { onViewModeChange(mode) }
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = mode.icon,
|
imageVector = mode.icon,
|
||||||
contentDescription = mode.label,
|
contentDescription = mode.label,
|
||||||
modifier = Modifier.size(16.dp)
|
tint = iconColor,
|
||||||
)
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = iconScale
|
||||||
|
scaleY = iconScale
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user