refactor: 移除日期检查器新行入场动画与高亮动画

- 移除 ExpiryRow.isNew 字段及相关入场淡入+上滑动效
- 移除 highlightedRowId 高亮逻辑及相关动画
- simplify animateItem() 为默认参数
- 用 Spacer 替代 verticalArrangement 实现行间距
- 用 animateScrollToItem 替代 scrollToItem + delay 模式
This commit is contained in:
xfy 2026-06-01 16:41:33 +08:00
parent f0975f119d
commit 24cb8fd3fe

View File

@ -43,18 +43,15 @@ 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
@ -63,6 +60,7 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlin.time.Clock import kotlin.time.Clock
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.datetime.DatePeriod import kotlinx.datetime.DatePeriod
import kotlin.time.Instant import kotlin.time.Instant
@ -74,7 +72,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, val isNew: Boolean = false) private data class ExpiryRow(val id: Int, val days: Int? = null)
private sealed class DatePickerTarget { private sealed class DatePickerTarget {
data object Production : DatePickerTarget() data object Production : DatePickerTarget()
@ -130,7 +128,6 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
var showDatePicker by remember { mutableStateOf(false) } var showDatePicker by remember { mutableStateOf(false) }
var datePickerTarget by remember { mutableStateOf<DatePickerTarget?>(null) } var datePickerTarget by remember { mutableStateOf<DatePickerTarget?>(null) }
var highlightedRowId by remember { mutableIntStateOf(-1) }
val listState = rememberLazyListState() val listState = rememberLazyListState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -161,13 +158,11 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
val newId = nextId val newId = nextId
rows = rows + ExpiryRow(newId, null, isNew = true) rows = rows + ExpiryRow(newId, null)
nextId++ nextId++
highlightedRowId = newId
scope.launch { scope.launch {
listState.scrollToItem(rows.size) delay(50)
kotlinx.coroutines.delay(800) listState.animateScrollToItem(rows.size - 1)
highlightedRowId = -1
} }
}, },
modifier = Modifier.testTag("date_checker_fab"), modifier = Modifier.testTag("date_checker_fab"),
@ -221,8 +216,9 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxWidth(), modifier = Modifier
verticalArrangement = Arrangement.spacedBy(10.dp), .fillMaxWidth()
.weight(1f),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 4.dp) contentPadding = PaddingValues(horizontal = 16.dp, vertical = 4.dp)
) { ) {
items(rows, key = { it.id }) { row -> items(rows, key = { it.id }) { row ->
@ -250,12 +246,7 @@ 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
@ -278,8 +269,6 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
expiryDate = expiryDate, expiryDate = expiryDate,
daysRemaining = daysRemaining, daysRemaining = daysRemaining,
status = status, status = status,
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
@ -294,14 +283,13 @@ 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
}
} }
) )
} }
if (row.id != rows.lastOrNull()?.id) {
Spacer(modifier = Modifier.height(10.dp))
}
} }
} }
} }
@ -435,57 +423,19 @@ private fun ExpiryCard(
expiryDate: LocalDate?, expiryDate: LocalDate?,
daysRemaining: Int?, daysRemaining: Int?,
status: ExpiryStatus, status: ExpiryStatus,
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(
targetValue = if (isHighlighted) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceContainerHigh
},
animationSpec = androidx.compose.animation.core.tween(400),
label = "highlight"
)
Box( Box(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.background(backgroundColor) .background(MaterialTheme.colorScheme.surfaceContainerHigh)
.graphicsLayer(
alpha = animatedAlpha,
translationY = animatedOffset
)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier