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:
parent
f618d09458
commit
9584d46247
@ -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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,4 +180,4 @@ private fun generateMonthDays(year: Int, month: Int): List<DayData> {
|
|||||||
isCurrentMonth = date.month.number == month && date.year == year
|
isCurrentMonth = date.month.number == month && date.year == year
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user