Change collapse animation to staggered row slide-up with fade-out

Rows now keep full height and slide upward sequentially instead of
compressing. The anchor row (selected) moves to y=0 and stays fixed;
other rows exit top-to-bottom with staggered timing.
This commit is contained in:
meyou 2026-05-16 10:54:07 +08:00
parent f618d09458
commit 9584d46247
No known key found for this signature in database

View File

@ -2,7 +2,6 @@ package plus.rua.project.ui
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
@ -23,10 +22,10 @@ import kotlinx.datetime.number
import kotlinx.datetime.plus import kotlinx.datetime.plus
/** /**
* 月度日历网格页面支持折叠动画 * 月度日历网格页面支持逐行向上滑出的折叠动画
* *
* 折叠时非选中行高度按 (1-p) 缩放选中行保持原始高度 * 折叠时锚定行包含选中日期平滑移动到顶部固定其余行从上到下依次向上滑出并淡出
* 所有行通过手动 y-offset 定位形成向选中行收缩的视觉效果 * 下方行从锚定行背后经过z-index 遮挡所有行高度不变仅做 y 平移
* *
* @param year 年份 * @param year 年份
* @param month 月份1-12 * @param month 月份1-12
@ -58,14 +57,19 @@ fun CalendarMonthPage(
val density = LocalDensity.current val density = LocalDensity.current
val weeks = days.chunked(7) val weeks = days.chunked(7)
val selectedWeekIndex = remember(weeks, selectedDate) { val anchorIndex = remember(weeks, selectedDate) {
weeks.indexOfFirst { week -> week.any { it.date == selectedDate } } weeks.indexOfFirst { week -> week.any { it.date == selectedDate } }
} }
val hasAnchor = anchorIndex >= 0
val hasSelectedWeek = selectedWeekIndex >= 0
val h = rowHeightPx.toFloat() val h = rowHeightPx.toFloat()
// 使用与 CalendarMonthView 一致的 effectiveWeeks 计算高度,避免滑动中高度不匹配 // Stagger 参数:每行的动画延迟和持续时间
val totalNonAnchor = if (hasAnchor) weeks.size - 1 else weeks.size
val staggerGap = if (totalNonAnchor > 1) 0.5f / totalNonAnchor else 0f
val rowAnimDuration = if (totalNonAnchor > 1) {
(1f - (totalNonAnchor - 1) * staggerGap).coerceAtLeast(0.1f)
} else 1f
val totalHeightDp = if (rowHeightPx > 0) { val totalHeightDp = if (rowHeightPx > 0) {
val totalPx = h * (1 + (effectiveWeeks - 1) * (1f - collapseProgress)) val totalPx = h * (1 + (effectiveWeeks - 1) * (1f - collapseProgress))
with(density) { totalPx.toDp() } with(density) { totalPx.toDp() }
@ -80,49 +84,46 @@ fun CalendarMonthPage(
) )
) { ) {
weeks.forEachIndexed { weekIndex, week -> weeks.forEachIndexed { weekIndex, week ->
val isSelected = hasSelectedWeek && weekIndex == selectedWeekIndex val isAnchor = hasAnchor && weekIndex == anchorIndex
val isAbove = hasSelectedWeek && weekIndex < selectedWeekIndex
val isBelow = hasSelectedWeek && weekIndex > selectedWeekIndex
val rowScale = when { // 退出顺序:从上到下视觉顺序,锚定行跳过
isAbove || isBelow -> 1f - collapseProgress val exitOrder = when {
else -> 1f !hasAnchor -> weekIndex
weekIndex < anchorIndex -> weekIndex
weekIndex == anchorIndex -> -1
else -> weekIndex - 1
} }
val rowHeightDp = if (rowHeightPx > 0 && rowScale > 0.01f) { // 每行的局部进度staggered
with(density) { (h * rowScale).toDp() } val localProgress = when {
} else if (rowHeightPx <= 0) { collapseProgress <= 0f -> 0f
null isAnchor -> collapseProgress
} else { exitOrder < 0 -> 0f
0.dp totalNonAnchor <= 1 -> collapseProgress
else -> ((collapseProgress - exitOrder * staggerGap) / rowAnimDuration).coerceIn(0f, 1f)
} }
val yOffsetDp = if (rowHeightPx > 0 && hasSelectedWeek) { // Y 偏移
val yPx = when { val yOffsetDp = if (rowHeightPx > 0) {
isAbove -> weekIndex * h * (1f - collapseProgress) val yPx = if (isAnchor) {
isSelected -> selectedWeekIndex * h * (1f - collapseProgress) anchorIndex * h * (1f - localProgress)
isBelow -> selectedWeekIndex * h * (1f - collapseProgress) + h + (weekIndex - selectedWeekIndex - 1) * h * (1f - collapseProgress) } else {
else -> weekIndex * h val originalY = weekIndex * h
originalY - localProgress * (originalY + h)
} }
with(density) { yPx.toDp() } with(density) { yPx.toDp() }
} else if (rowHeightPx > 0) { } else 0.dp
val yPx = weekIndex * h
with(density) { yPx.toDp() }
} else {
0.dp
}
val shouldShow = rowHeightDp == null || rowHeightDp > 0.dp // 淡出
val rowAlpha = if (isAnchor) 1f else (1f - localProgress).coerceIn(0f, 1f)
val skipDayCells = (isAbove || isBelow) && rowScale < 0.1f && collapseProgress > 0.9f if (rowAlpha > 0.01f) {
if (shouldShow) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.zIndex(if (isSelected) 1f else 0f) .zIndex(if (isAnchor) 1f else 0f)
.then( .then(
if (rowHeightDp != null) Modifier.height(rowHeightDp) if (rowHeightPx > 0) Modifier.height(with(density) { h.toDp() })
else Modifier else Modifier
) )
.offset(y = yOffsetDp) .offset(y = yOffsetDp)
@ -137,25 +138,19 @@ fun CalendarMonthPage(
) )
.padding(vertical = ROW_PADDING_DP.dp) .padding(vertical = ROW_PADDING_DP.dp)
.then( .then(
if (isAbove || isBelow) Modifier.graphicsLayer { if (rowAlpha < 1f) Modifier.graphicsLayer { alpha = rowAlpha }
alpha = 1f - collapseProgress
}
else Modifier else Modifier
) )
) { ) {
if (skipDayCells) { week.forEach { dayData ->
Spacer(Modifier.weight(1f)) DayCell(
} else { date = dayData.date,
week.forEach { dayData -> isCurrentMonth = dayData.isCurrentMonth,
DayCell( isSelected = dayData.date == selectedDate,
date = dayData.date, isToday = dayData.date == today,
isCurrentMonth = dayData.isCurrentMonth, onClick = { onDateClick(dayData.date) },
isSelected = dayData.date == selectedDate, modifier = Modifier.weight(1f)
isToday = dayData.date == today, )
onClick = { onDateClick(dayData.date) },
modifier = Modifier.weight(1f)
)
}
} }
} }
} }