feat: 日期检查器新行添加入场动画(淡入+上滑)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xfy 2026-06-01 15:54:40 +08:00
parent bc9c10d82e
commit f0975f119d

View File

@ -43,15 +43,18 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.semantics.testTagsAsResourceId
@ -71,7 +74,7 @@ import kotlinx.datetime.plus
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.todayIn import kotlinx.datetime.todayIn
private data class ExpiryRow(val id: Int, val days: Int? = null) private data class ExpiryRow(val id: Int, val days: Int? = null, val isNew: Boolean = false)
private sealed class DatePickerTarget { private sealed class DatePickerTarget {
data object Production : DatePickerTarget() data object Production : DatePickerTarget()
@ -158,7 +161,7 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
val newId = nextId val newId = nextId
rows = rows + ExpiryRow(newId, null) rows = rows + ExpiryRow(newId, null, isNew = true)
nextId++ nextId++
highlightedRowId = newId highlightedRowId = newId
scope.launch { scope.launch {
@ -247,7 +250,12 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
SwipeToDismissBox( SwipeToDismissBox(
state = dismissState, state = dismissState,
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(
placementSpec = androidx.compose.animation.core.tween(
durationMillis = 400,
easing = androidx.compose.animation.core.FastOutSlowInEasing
)
),
backgroundContent = { backgroundContent = {
Box( Box(
modifier = Modifier modifier = Modifier
@ -271,6 +279,7 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
daysRemaining = daysRemaining, daysRemaining = daysRemaining,
status = status, status = status,
isHighlighted = row.id == highlightedRowId, isHighlighted = row.id == highlightedRowId,
isNew = row.isNew,
onDaysChange = { newDays -> onDaysChange = { newDays ->
rows = rows.map { rows = rows.map {
if (it.id == row.id) it.copy(days = newDays) else it if (it.id == row.id) it.copy(days = newDays) else it
@ -285,6 +294,11 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
onShowDatePicker = { onShowDatePicker = {
datePickerTarget = DatePickerTarget.Row(row.id) datePickerTarget = DatePickerTarget.Row(row.id)
showDatePicker = true showDatePicker = true
},
onNewRowAnimated = {
rows = rows.map {
if (it.id == row.id) it.copy(isNew = false) else it
}
} }
) )
} }
@ -422,14 +436,37 @@ private fun ExpiryCard(
daysRemaining: Int?, daysRemaining: Int?,
status: ExpiryStatus, status: ExpiryStatus,
isHighlighted: Boolean, isHighlighted: Boolean,
isNew: Boolean,
onDaysChange: (Int?) -> Unit, onDaysChange: (Int?) -> Unit,
onExpiryDateChange: (LocalDate) -> Unit, onExpiryDateChange: (LocalDate) -> Unit,
onShowDatePicker: () -> Unit, onShowDatePicker: () -> Unit,
onNewRowAnimated: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var daysText by remember(days) { mutableStateOf(days?.toString() ?: "") } var daysText by remember(days) { mutableStateOf(days?.toString() ?: "") }
var dateText by remember(expiryDate) { mutableStateOf(expiryDate?.toString() ?: "") } var dateText by remember(expiryDate) { mutableStateOf(expiryDate?.toString() ?: "") }
val density = androidx.compose.ui.platform.LocalDensity.current
val enterOffsetPx = remember(density) { with(density) { 20.dp.toPx() } }
val animatedAlpha by animateFloatAsState(
targetValue = if (isNew) 0f else 1f,
animationSpec = androidx.compose.animation.core.tween<Float>(350, delayMillis = 50),
label = "enterAlpha"
)
val animatedOffset by animateFloatAsState(
targetValue = if (isNew) enterOffsetPx else 0f,
animationSpec = androidx.compose.animation.core.tween<Float>(350, delayMillis = 50),
label = "enterOffset"
)
LaunchedEffect(Unit) {
if (isNew) {
kotlinx.coroutines.delay(50)
onNewRowAnimated()
}
}
val backgroundColor by androidx.compose.animation.animateColorAsState( val backgroundColor by androidx.compose.animation.animateColorAsState(
targetValue = if (isHighlighted) { targetValue = if (isHighlighted) {
MaterialTheme.colorScheme.primaryContainer MaterialTheme.colorScheme.primaryContainer
@ -445,6 +482,10 @@ private fun ExpiryCard(
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.background(backgroundColor) .background(backgroundColor)
.graphicsLayer(
alpha = animatedAlpha,
translationY = animatedOffset
)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier