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

View File

@ -46,13 +46,16 @@ fun CalendarMonthView(
var calendarHeightPx by remember { mutableIntStateOf(0) } var calendarHeightPx by remember { mutableIntStateOf(0) }
var screenHeightPx by remember { mutableIntStateOf(0) } var screenHeightPx by remember { mutableIntStateOf(0) }
var expandedCalendarHeightPx 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行) // 日历网格高度 = 总高度 - MonthHeader - WeekdayHeader
// 折叠偏移量 = 进度 × 展开高度的5/6保留1行可见 val expandedGridHeightPx = expandedCalendarHeightPx - monthHeaderHeightPx - weekdayHeaderHeightPx
// 折叠偏移量 = 进度 × 网格5行高度保留1行可见
val collapseOffsetPx = if (viewModel.isCollapsed) { val collapseOffsetPx = if (viewModel.isCollapsed) {
0 0
} else { } else {
-(viewModel.collapseProgress * expandedCalendarHeightPx * 5f / 6f).toInt() -(viewModel.collapseProgress * expandedGridHeightPx * 5f / 6f).toInt()
} }
val cardTopPx = if (viewModel.isCollapsed) { val cardTopPx = if (viewModel.isCollapsed) {
calendarHeightPx calendarHeightPx
@ -79,9 +82,16 @@ fun CalendarMonthView(
MonthHeader( MonthHeader(
year = currentYear, year = currentYear,
month = currentMonth, 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) { if (viewModel.isCollapsed) {
WeekPager( WeekPager(
selectedDate = viewModel.selectedDate, selectedDate = viewModel.selectedDate,