Add circular reveal animation to DayCell with animated state transitions

Replace hard-switched background/border modifiers with updateTransition-based
animated properties: revealProgress for circular indicator radius, contentColor
and selectedColor via animateColor, borderAlpha for today stroke. Use drawBehind
for custom circle drawing with tween(250, FastOutSlowInEasing) spec.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-15 17:49:12 +08:00
parent 2411416eb7
commit 8913e5ff0d

View File

@ -1,8 +1,10 @@
package plus.rua.project.ui
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
@ -10,13 +12,22 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.datetime.LocalDate
enum class DayCellState {
NORMAL, OTHER_MONTH, TODAY, SELECTED, SELECTED_TODAY
}
/**
* 单个日期单元格显示日期数字并支持选中/今天/非当月状态
*
@ -36,26 +47,84 @@ fun DayCell(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val contentColor = when {
isSelected && isToday -> MaterialTheme.colorScheme.onPrimaryContainer
isSelected -> MaterialTheme.colorScheme.onPrimary
isToday -> MaterialTheme.colorScheme.primary
!isCurrentMonth -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
else -> MaterialTheme.colorScheme.onSurface
val currentState = when {
isSelected && isToday -> DayCellState.SELECTED_TODAY
isSelected -> DayCellState.SELECTED
isToday -> DayCellState.TODAY
!isCurrentMonth -> DayCellState.OTHER_MONTH
else -> DayCellState.NORMAL
}
val transition = updateTransition(targetState = currentState, label = "dayCell")
val revealProgress by transition.animateFloat(
transitionSpec = { tween(250, easing = FastOutSlowInEasing) },
label = "revealProgress"
) { state ->
when (state) {
DayCellState.SELECTED, DayCellState.SELECTED_TODAY -> 1f
else -> 0f
}
}
val contentColor by transition.animateColor(
transitionSpec = { tween(250, easing = FastOutSlowInEasing) },
label = "contentColor"
) { state ->
when (state) {
DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer
DayCellState.SELECTED -> MaterialTheme.colorScheme.onPrimary
DayCellState.TODAY -> MaterialTheme.colorScheme.primary
DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
DayCellState.NORMAL -> MaterialTheme.colorScheme.onSurface
}
}
val selectedColor by transition.animateColor(
transitionSpec = { tween(250, easing = FastOutSlowInEasing) },
label = "selectedColor"
) { state ->
when (state) {
DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.primaryContainer
DayCellState.SELECTED -> MaterialTheme.colorScheme.primary
else -> Color.Transparent
}
}
val borderAlpha by transition.animateFloat(
transitionSpec = { tween(250, easing = FastOutSlowInEasing) },
label = "borderAlpha"
) { state ->
when (state) {
DayCellState.TODAY -> 1.5f
else -> 0f
}
}
val todayBorderColor = MaterialTheme.colorScheme.primary
Box(
modifier = modifier
.aspectRatio(1f)
.clip(CircleShape)
.then(
when {
isSelected && isToday -> Modifier.background(MaterialTheme.colorScheme.primaryContainer)
isSelected -> Modifier.background(MaterialTheme.colorScheme.primary)
isToday -> Modifier.border(BorderStroke(1.5.dp, MaterialTheme.colorScheme.primary), CircleShape)
else -> Modifier
.drawBehind {
if (revealProgress > 0f) {
val maxRadius = size.minDimension / 2f
drawCircle(
color = selectedColor,
radius = revealProgress * maxRadius,
center = Offset(size.width / 2f, size.height / 2f)
)
}
)
if (borderAlpha > 0f) {
drawCircle(
color = todayBorderColor.copy(alpha = borderAlpha.coerceAtMost(1f)),
radius = size.minDimension / 2f,
center = Offset(size.width / 2f, size.height / 2f),
style = Stroke(width = borderAlpha.coerceAtMost(1.5f) * 1.5.dp.toPx())
)
}
}
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {