feat: 日期检查器添加新行自动滚动与高亮动画

- 点击 FAB 添加新行后自动滚动到列表底部
- 新增行短暂高亮显示(primaryContainer 背景色)
- LazyColumn 添加 animateItem() 提升列表动画体验

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

View File

@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
@ -42,6 +43,7 @@ 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.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
@ -58,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.launch
import kotlinx.datetime.DatePeriod import kotlinx.datetime.DatePeriod
import kotlin.time.Instant import kotlin.time.Instant
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
@ -124,6 +127,10 @@ 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 scope = rememberCoroutineScope()
Scaffold( Scaffold(
modifier = modifier.semantics { testTagsAsResourceId = true }, modifier = modifier.semantics { testTagsAsResourceId = true },
@ -150,8 +157,15 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
floatingActionButton = { floatingActionButton = {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
rows = rows + ExpiryRow(nextId, null) val newId = nextId
rows = rows + ExpiryRow(newId, null)
nextId++ nextId++
highlightedRowId = newId
scope.launch {
listState.scrollToItem(rows.size)
kotlinx.coroutines.delay(800)
highlightedRowId = -1
}
}, },
modifier = Modifier.testTag("date_checker_fab"), modifier = Modifier.testTag("date_checker_fab"),
shape = CircleShape, shape = CircleShape,
@ -203,6 +217,7 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
LazyColumn( LazyColumn(
state = listState,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 4.dp) contentPadding = PaddingValues(horizontal = 16.dp, vertical = 4.dp)
@ -232,6 +247,7 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
SwipeToDismissBox( SwipeToDismissBox(
state = dismissState, state = dismissState,
modifier = Modifier.animateItem(),
backgroundContent = { backgroundContent = {
Box( Box(
modifier = Modifier modifier = Modifier
@ -254,6 +270,7 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
expiryDate = expiryDate, expiryDate = expiryDate,
daysRemaining = daysRemaining, daysRemaining = daysRemaining,
status = status, status = status,
isHighlighted = row.id == highlightedRowId,
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
@ -404,6 +421,7 @@ private fun ExpiryCard(
expiryDate: LocalDate?, expiryDate: LocalDate?,
daysRemaining: Int?, daysRemaining: Int?,
status: ExpiryStatus, status: ExpiryStatus,
isHighlighted: Boolean,
onDaysChange: (Int?) -> Unit, onDaysChange: (Int?) -> Unit,
onExpiryDateChange: (LocalDate) -> Unit, onExpiryDateChange: (LocalDate) -> Unit,
onShowDatePicker: () -> Unit, onShowDatePicker: () -> Unit,
@ -412,11 +430,21 @@ private fun ExpiryCard(
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 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(MaterialTheme.colorScheme.surfaceContainerHigh) .background(backgroundColor)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier