Refactor collapse animation to overlay selected row instead of shrinking rows

Selected row now moves up via offset + zIndex to cover other rows, which
stay in place. Collapse offset calculation excludes MonthHeader and
WeekdayHeader heights so only the grid portion is collapsed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-14 15:44:00 +08:00
parent edf8ea9851
commit a44ef44b42
2 changed files with 57 additions and 53 deletions

View File

@ -3,7 +3,7 @@ package plus.rua.project.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -11,9 +11,11 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import kotlinx.datetime.DatePeriod
import kotlinx.datetime.LocalDate
import kotlinx.datetime.minus
@ -22,12 +24,13 @@ import kotlinx.datetime.plus
/**
* 月度日历网格页6×7 布局支持折叠动画
*
* 折叠时选中行保持原高上方行向上收缩下方行向下收缩模拟"挤压"效果
* 折叠时选中行保持原高并向上移动覆盖其他行其他行保持原位不动
* 选中行通过 offset + zIndex 实现覆盖效果
*
* @param year 年份
* @param month 月份1-12
* @param selectedDate 当前选中日期
* @param today 今天的日期用于高亮标记
* @param today 今天的日期
* @param onDateClick 日期点击回调
* @param collapseProgress 折叠进度0f=展开61f=折叠仅选中行可见
* @param modifier 外部布局修饰符
@ -53,54 +56,46 @@ fun CalendarMonthPage(
}
var rowHeightPx by remember { mutableIntStateOf(0) }
val rowMeasured = rowHeightPx > 0
Column(modifier = modifier) {
// 选中行上移距离 = 上方行数 × 行高 × progress
val selectedOffsetPx = if (rowHeightPx > 0) {
-(selectedWeekIndex.toFloat() * rowHeightPx.toFloat() * collapseProgress)
} else {
0f
}
val selectedOffsetDp = with(density) { selectedOffsetPx.toDp() }
Column(modifier = modifier.clipToBounds()) {
weeks.forEachIndexed { weekIndex, week ->
val isAboveSelected = weekIndex < selectedWeekIndex
val isBelowSelected = weekIndex > selectedWeekIndex
val isSelected = weekIndex == selectedWeekIndex
val rowScale = when {
isAboveSelected || isBelowSelected -> 1f - collapseProgress
else -> 1f
}
val rowHeightDp = if (rowMeasured && rowScale > 0.01f) {
with(density) { (rowHeightPx * rowScale).toDp() }
} else if (!rowMeasured) {
// First frame: let aspectRatio determine height naturally
null
} else {
0.dp
}
val shouldShow = rowHeightDp == null || rowHeightDp > 0.dp
if (shouldShow) {
Row(
modifier = Modifier
.fillMaxWidth()
.then(
if (rowHeightDp != null) Modifier.height(rowHeightDp)
else Modifier
)
.onSizeChanged { size ->
if (size.height > 0 && !rowMeasured) {
rowHeightPx = size.height
}
Row(
modifier = Modifier
.fillMaxWidth()
.zIndex(if (isSelected) 1f else 0f)
.then(
if (isSelected && rowHeightPx > 0) {
Modifier.offset(y = selectedOffsetDp)
} else {
Modifier
}
)
.onSizeChanged { size ->
if (size.height > 0 && rowHeightPx == 0) {
rowHeightPx = size.height
}
.padding(vertical = 2.dp)
) {
week.forEach { dayData ->
DayCell(
date = dayData.date,
isCurrentMonth = dayData.isCurrentMonth,
isSelected = dayData.date == selectedDate,
isToday = dayData.date == today,
onClick = { onDateClick(dayData.date) },
modifier = Modifier.weight(1f)
)
}
.padding(vertical = 2.dp)
) {
week.forEach { dayData ->
DayCell(
date = dayData.date,
isCurrentMonth = dayData.isCurrentMonth,
isSelected = dayData.date == selectedDate,
isToday = dayData.date == today,
onClick = { onDateClick(dayData.date) },
modifier = Modifier.weight(1f)
)
}
}
}
@ -112,13 +107,12 @@ private data class DayData(
val isCurrentMonth: Boolean
)
@Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口
@Suppress("DEPRECATION")
private fun generateMonthDays(year: Int, month: Int): List<DayData> {
val firstOfMonth = LocalDate(year, month, 1)
val offset = firstOfMonth.dayOfWeek.ordinal
val startDate = firstOfMonth.minus(DatePeriod(days = offset))
// 6行×7列=42格覆盖跨月首尾周保证网格完整
return (0 until 42).map { i ->
val date = startDate.plus(DatePeriod(days = i))
DayData(

View File

@ -46,13 +46,16 @@ fun CalendarMonthView(
var calendarHeightPx by remember { mutableIntStateOf(0) }
var screenHeightPx by remember { mutableIntStateOf(0) }
var expandedCalendarHeightPx by remember { mutableIntStateOf(0) }
var monthHeaderHeightPx by remember { mutableIntStateOf(0) }
var weekdayHeaderHeightPx by remember { mutableIntStateOf(0) }
// collapseProgress: 0f=月视图(6行), 1f=周视图(1行)
// 折叠偏移量 = 进度 × 展开高度的5/6保留1行可见
// 日历网格高度 = 总高度 - MonthHeader - WeekdayHeader
val expandedGridHeightPx = expandedCalendarHeightPx - monthHeaderHeightPx - weekdayHeaderHeightPx
// 折叠偏移量 = 进度 × 网格5行高度保留1行可见
val collapseOffsetPx = if (viewModel.isCollapsed) {
0
} else {
-(viewModel.collapseProgress * expandedCalendarHeightPx * 5f / 6f).toInt()
-(viewModel.collapseProgress * expandedGridHeightPx * 5f / 6f).toInt()
}
val cardTopPx = if (viewModel.isCollapsed) {
calendarHeightPx
@ -79,9 +82,16 @@ fun CalendarMonthView(
MonthHeader(
year = currentYear,
month = currentMonth,
weekNumber = viewModel.getIsoWeekNumber(viewModel.selectedDate)
weekNumber = viewModel.getIsoWeekNumber(viewModel.selectedDate),
modifier = Modifier.onSizeChanged { size ->
monthHeaderHeightPx = size.height
}
)
WeekdayHeader(
modifier = Modifier.fillMaxWidth().onSizeChanged { size ->
weekdayHeaderHeightPx = size.height
}
)
WeekdayHeader(modifier = Modifier.fillMaxWidth())
if (viewModel.isCollapsed) {
WeekPager(
selectedDate = viewModel.selectedDate,