feat: 日期检查器添加行级入场/出场动画与 animateContentSize

This commit is contained in:
xfy 2026-06-01 16:56:19 +08:00
parent c9bdff9063
commit aeb147c6a7

View File

@ -2,6 +2,12 @@
package plus.rua.project.ui package plus.rua.project.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -16,12 +22,11 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.rememberScrollState
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
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -42,6 +47,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue 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.key
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -129,7 +135,7 @@ 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) }
val listState = rememberLazyListState() val scrollState = rememberScrollState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Scaffold( Scaffold(
@ -161,8 +167,8 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
rows = rows + ExpiryRow(newId, null) rows = rows + ExpiryRow(newId, null)
nextId++ nextId++
scope.launch { scope.launch {
delay(50) delay(100)
listState.animateScrollToItem(rows.size - 1) scrollState.animateScrollTo(Int.MAX_VALUE)
} }
}, },
modifier = Modifier.testTag("date_checker_fab"), modifier = Modifier.testTag("date_checker_fab"),
@ -214,14 +220,19 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
LazyColumn( Column(
state = listState,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f), .weight(1f)
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 4.dp) .verticalScroll(scrollState)
.animateContentSize(
animationSpec = androidx.compose.animation.core.spring(
stiffness = androidx.compose.animation.core.Spring.StiffnessMediumLow
)
)
.padding(horizontal = 16.dp, vertical = 4.dp)
) { ) {
items(rows, key = { it.id }) { row -> rows.forEachIndexed { index, row ->
val expiryDate = row.days?.let { productionDate.plus(DatePeriod(days = it)) } val expiryDate = row.days?.let { productionDate.plus(DatePeriod(days = it)) }
val daysRemaining = expiryDate?.let { today.daysUntil(it) } val daysRemaining = expiryDate?.let { today.daysUntil(it) }
val status = when { val status = when {
@ -244,50 +255,68 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
} }
) )
SwipeToDismissBox( key(row.id) {
state = dismissState, var visible by remember { mutableStateOf(false) }
modifier = Modifier.animateItem(),
backgroundContent = { androidx.compose.runtime.LaunchedEffect(Unit) {
Box( visible = true
modifier = Modifier }
.fillMaxSize()
.clip(RoundedCornerShape(16.dp)) AnimatedVisibility(
.background(MaterialTheme.colorScheme.errorContainer) visible = visible,
.padding(horizontal = 20.dp), enter = expandVertically(
contentAlignment = Alignment.CenterEnd expandFrom = Alignment.Bottom,
animationSpec = androidx.compose.animation.core.spring(
stiffness = androidx.compose.animation.core.Spring.StiffnessMediumLow
)
) + fadeIn(animationSpec = androidx.compose.animation.core.tween(300)),
exit = shrinkVertically() + fadeOut()
) {
SwipeToDismissBox(
state = dismissState,
backgroundContent = {
Box(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.errorContainer)
.padding(horizontal = 20.dp),
contentAlignment = Alignment.CenterEnd
) {
Text(
text = "删除",
color = MaterialTheme.colorScheme.onErrorContainer,
style = MaterialTheme.typography.labelLarge
)
}
}
) { ) {
Text( ExpiryCard(
text = "删除", days = row.days,
color = MaterialTheme.colorScheme.onErrorContainer, expiryDate = expiryDate,
style = MaterialTheme.typography.labelLarge daysRemaining = daysRemaining,
status = status,
onDaysChange = { newDays ->
rows = rows.map {
if (it.id == row.id) it.copy(days = newDays) else it
}
},
onExpiryDateChange = { newDate ->
val newDays = productionDate.daysUntil(newDate)
rows = rows.map {
if (it.id == row.id) it.copy(days = newDays) else it
}
},
onShowDatePicker = {
datePickerTarget = DatePickerTarget.Row(row.id)
showDatePicker = true
}
) )
} }
} }
) {
ExpiryCard(
days = row.days,
expiryDate = expiryDate,
daysRemaining = daysRemaining,
status = status,
onDaysChange = { newDays ->
rows = rows.map {
if (it.id == row.id) it.copy(days = newDays) else it
}
},
onExpiryDateChange = { newDate ->
val newDays = productionDate.daysUntil(newDate)
rows = rows.map {
if (it.id == row.id) it.copy(days = newDays) else it
}
},
onShowDatePicker = {
datePickerTarget = DatePickerTarget.Row(row.id)
showDatePicker = true
}
)
} }
if (row.id != rows.lastOrNull()?.id) { if (index < rows.lastIndex) {
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
} }
} }