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:
parent
edf8ea9851
commit
a44ef44b42
@ -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=展开(6行),1f=折叠(仅选中行可见)
|
* @param collapseProgress 折叠进度,0f=展开(6行),1f=折叠(仅选中行可见)
|
||||||
* @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 无替代 API,kotlinx-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(
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user