Replace Column layout with Box+manual y-offset for collapse animation

Switch CalendarMonthPage from Column to Box with calculated y positions per row,
enabling precise height computation that aligns with CalendarMonthView's
BottomCard positioning. Simplify cardTopPx calculation by deriving grid height
from the same formula instead of tracking expandedCalendarHeightPx separately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-14 16:49:45 +08:00
parent 2fb36168a3
commit 151b15d09e
2 changed files with 74 additions and 41 deletions

View File

@ -1,6 +1,6 @@
package plus.rua.project.ui package plus.rua.project.ui
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Box
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.height
@ -42,37 +42,68 @@ fun CalendarMonthPage(
weeks.indexOfFirst { week -> week.any { it.date == selectedDate } } weeks.indexOfFirst { week -> week.any { it.date == selectedDate } }
} }
val hasSelectedWeek = selectedWeekIndex >= 0
var rowHeightPx by remember { mutableIntStateOf(0) } var rowHeightPx by remember { mutableIntStateOf(0) }
val rowMeasured = rowHeightPx > 0 val rowMeasured = rowHeightPx > 0
val H = rowHeightPx.toFloat()
// 选中行上移距离 = 上方行数 × 行高 × progress // 总高度 = 6行 × 行高(展开时),或选中行高度(折叠时)
val selectedOffsetPx = if (rowMeasured) { val totalHeightDp = if (rowMeasured) {
-(selectedWeekIndex.toFloat() * rowHeightPx.toFloat() * collapseProgress) if (hasSelectedWeek) {
// 选中行高度 + 上方行压缩高度 + 下方行压缩高度
val aboveH = selectedWeekIndex * H * (1f - collapseProgress)
val belowH = (weeks.size - 1 - selectedWeekIndex) * H * (1f - collapseProgress)
val selH = H
with(density) { (aboveH + selH + belowH).toDp() }
} else {
with(density) { (weeks.size * H).toDp() }
}
} else { } else {
0f null
} }
val selectedOffsetDp = with(density) { selectedOffsetPx.toDp() }
Column(modifier = modifier.clipToBounds()) { Box(modifier = modifier.clipToBounds().then(
if (totalHeightDp != null) Modifier.height(totalHeightDp)
else Modifier
).onSizeChanged { size ->
if (collapseProgress > 0f) {
println("[Page] totalH=${size.height}px p=$collapseProgress selWeek=$selectedWeekIndex rowH=$rowHeightPx")
}
}) {
weeks.forEachIndexed { weekIndex, week -> weeks.forEachIndexed { weekIndex, week ->
val isSelected = weekIndex == selectedWeekIndex val isSelected = hasSelectedWeek && weekIndex == selectedWeekIndex
val isAboveSelected = weekIndex < selectedWeekIndex val isAbove = hasSelectedWeek && weekIndex < selectedWeekIndex
val isBelowSelected = weekIndex > selectedWeekIndex val isBelow = hasSelectedWeek && weekIndex > selectedWeekIndex
// 非选中行高度跟手压缩
val rowScale = when { val rowScale = when {
isAboveSelected || isBelowSelected -> 1f - collapseProgress isAbove || isBelow -> 1f - collapseProgress
else -> 1f else -> 1f
} }
val rowHeightDp = if (rowMeasured && rowScale > 0.01f) { val rowHeightDp = if (rowMeasured && rowScale > 0.01f) {
with(density) { (rowHeightPx.toFloat() * rowScale).toDp() } with(density) { (H * rowScale).toDp() }
} else if (!rowMeasured) { } else if (!rowMeasured) {
null null
} else { } else {
0.dp 0.dp
} }
// 手动计算每行的视觉 y 位置
val yOffsetDp = if (rowMeasured && hasSelectedWeek) {
val yPx = when {
isAbove -> weekIndex * H * (1f - collapseProgress)
isSelected -> selectedWeekIndex * H * (1f - collapseProgress)
isBelow -> selectedWeekIndex * H * (1f - collapseProgress) + H + (weekIndex - selectedWeekIndex - 1) * H * (1f - collapseProgress)
else -> weekIndex * H
}
with(density) { yPx.toDp() }
} else if (rowMeasured) {
with(density) { (weekIndex * H).toDp() }
} else {
0.dp
}
val shouldShow = rowHeightDp == null || rowHeightDp > 0.dp val shouldShow = rowHeightDp == null || rowHeightDp > 0.dp
if (shouldShow) { if (shouldShow) {
@ -84,10 +115,7 @@ fun CalendarMonthPage(
if (rowHeightDp != null) Modifier.height(rowHeightDp) if (rowHeightDp != null) Modifier.height(rowHeightDp)
else Modifier else Modifier
) )
.then( .offset(y = yOffsetDp)
if (isSelected && rowMeasured) Modifier.offset(y = selectedOffsetDp)
else Modifier
)
.onSizeChanged { size -> .onSizeChanged { size ->
if (size.height > 0 && !rowMeasured) { if (size.height > 0 && !rowMeasured) {
rowHeightPx = size.height rowHeightPx = size.height

View File

@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
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.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -19,7 +18,6 @@ import androidx.compose.ui.Modifier
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 kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn import kotlinx.datetime.todayIn
import kotlin.time.Clock import kotlin.time.Clock
@ -44,26 +42,34 @@ fun CalendarMonthView(
val density = LocalDensity.current val density = LocalDensity.current
var calendarHeightPx by remember { mutableIntStateOf(0) } 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 monthHeaderHeightPx by remember { mutableIntStateOf(0) }
var weekdayHeaderHeightPx by remember { mutableIntStateOf(0) } var weekdayHeaderHeightPx by remember { mutableIntStateOf(0) }
var screenHeightPx by remember { mutableIntStateOf(0) }
// 日历网格高度 = 总高度 - MonthHeader - WeekdayHeader val p = viewModel.collapseProgress
val expandedGridHeightPx = expandedCalendarHeightPx - monthHeaderHeightPx - weekdayHeaderHeightPx val headerHeightPx = monthHeaderHeightPx + weekdayHeaderHeightPx
// 折叠偏移量 = 进度 × 网格5行高度保留1行可见
val collapseOffsetPx = if (viewModel.isCollapsed) { // 展开时网格高度 = 首次测量的日历总高度 - headers
0 val expandedGridHeightPx = calendarHeightPx - headerHeightPx
} else { val weeksCount = 6
-(viewModel.collapseProgress * expandedGridHeightPx * 5f / 6f).toInt()
} // 折叠时网格高度公式(与 CalendarMonthPage 一致):
val cardTopPx = if (viewModel.isCollapsed) { // gridH = rowH × (1 + (weeks-1) × (1-p))
calendarHeightPx // 其中 rowH = expandedGridHeightPx / weeksCount
} else { val gridHeightPx = if (expandedGridHeightPx > 0 && p > 0f) {
expandedCalendarHeightPx + collapseOffsetPx val rowH = expandedGridHeightPx.toFloat() / weeksCount
} (rowH * (1 + (weeksCount - 1) * (1f - p))).toInt()
} else if (expandedGridHeightPx > 0) {
expandedGridHeightPx
} else 0
val cardTopPx = headerHeightPx + gridHeightPx
val cardHeightPx = screenHeightPx - cardTopPx val cardHeightPx = screenHeightPx - cardTopPx
if (p > 0f) {
println("[View] p=$p monthH=$monthHeaderHeightPx weekdayH=$weekdayHeaderHeightPx expandedGridH=$expandedGridHeightPx gridH=$gridHeightPx cardTop=$cardTopPx cardH=$cardHeightPx screenH=$screenHeightPx calH=$calendarHeightPx isCollapsed=${viewModel.isCollapsed}")
}
Box( Box(
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
@ -73,12 +79,11 @@ fun CalendarMonthView(
} }
) { ) {
Column(modifier = Modifier.padding(horizontal = 16.dp).onSizeChanged { size -> Column(modifier = Modifier.padding(horizontal = 16.dp).onSizeChanged { size ->
calendarHeightPx = size.height // 仅在展开时记录日历总高度(折叠时 HorizontalPager 不缩小)
// 仅在首次展开时记录完整日历高度,折叠后不再覆盖 if (p < 0.01f) {
if (!viewModel.isCollapsed && viewModel.collapseProgress < 0.01f) { calendarHeightPx = size.height
expandedCalendarHeightPx = size.height }
} }) {
}) {
MonthHeader( MonthHeader(
year = currentYear, year = currentYear,
month = currentMonth, month = currentMonth,