Replace internal rowHeightPx measurement with external rowHeightPx parameter passed from CalendarMonthView. This eliminates the mismatch between outer gridHeightPx (based on shrinking container) and inner totalHeightDp (based on independently measured row height), which was causing visual jitter during collapse animation. Remove onRowHeightMeasured callback, TAG constant, and debug printlns from CalendarMonthPage as part of this refactor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
155 lines
5.6 KiB
Kotlin
155 lines
5.6 KiB
Kotlin
package plus.rua.project.ui
|
||
|
||
import androidx.compose.foundation.layout.Box
|
||
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.remember
|
||
import androidx.compose.ui.Modifier
|
||
import androidx.compose.ui.draw.clipToBounds
|
||
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
|
||
import kotlinx.datetime.plus
|
||
|
||
/**
|
||
* 月度日历网格页面,支持折叠动画。
|
||
*
|
||
* 折叠时非选中行高度按 (1-p) 缩放,选中行保持原始高度,
|
||
* 所有行通过手动 y-offset 定位,形成向选中行收缩的视觉效果。
|
||
*
|
||
* @param rowHeightPx 从外层传入的锁定行高(像素),折叠过程中不变
|
||
*/
|
||
@Composable
|
||
fun CalendarMonthPage(
|
||
year: Int,
|
||
month: Int,
|
||
selectedDate: LocalDate,
|
||
today: LocalDate,
|
||
onDateClick: (LocalDate) -> Unit,
|
||
collapseProgress: Float,
|
||
rowHeightPx: Int,
|
||
modifier: Modifier = Modifier
|
||
) {
|
||
val days = remember(year, month) {
|
||
generateMonthDays(year, month)
|
||
}
|
||
val density = LocalDensity.current
|
||
|
||
val weeks = days.chunked(7)
|
||
val selectedWeekIndex = remember(weeks, selectedDate) {
|
||
weeks.indexOfFirst { week -> week.any { it.date == selectedDate } }
|
||
}
|
||
|
||
val hasSelectedWeek = selectedWeekIndex >= 0
|
||
val H = rowHeightPx.toFloat()
|
||
|
||
// 总高度 = 选中行高度 + 上方行压缩高度 + 下方行压缩高度
|
||
val totalHeightDp = if (rowHeightPx > 0) {
|
||
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 {
|
||
null
|
||
}
|
||
|
||
Box(modifier = modifier.clipToBounds().then(
|
||
if (totalHeightDp != null) Modifier.height(totalHeightDp)
|
||
else Modifier
|
||
)) {
|
||
weeks.forEachIndexed { weekIndex, week ->
|
||
val isSelected = hasSelectedWeek && weekIndex == selectedWeekIndex
|
||
val isAbove = hasSelectedWeek && weekIndex < selectedWeekIndex
|
||
val isBelow = hasSelectedWeek && weekIndex > selectedWeekIndex
|
||
|
||
val rowScale = when {
|
||
isAbove || isBelow -> 1f - collapseProgress
|
||
else -> 1f
|
||
}
|
||
|
||
val rowHeightDp = if (rowHeightPx > 0 && rowScale > 0.01f) {
|
||
with(density) { (H * rowScale).toDp() }
|
||
} else if (rowHeightPx <= 0) {
|
||
null
|
||
} else {
|
||
0.dp
|
||
}
|
||
|
||
val yOffsetDp = if (rowHeightPx > 0 && 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 (rowHeightPx > 0) {
|
||
with(density) { (weekIndex * H).toDp() }
|
||
} else {
|
||
0.dp
|
||
}
|
||
|
||
val shouldShow = rowHeightDp == null || rowHeightDp > 0.dp
|
||
|
||
if (shouldShow) {
|
||
Row(
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.zIndex(if (isSelected) 1f else 0f)
|
||
.then(
|
||
if (rowHeightDp != null) Modifier.height(rowHeightDp)
|
||
else Modifier
|
||
)
|
||
.offset(y = yOffsetDp)
|
||
.padding(vertical = 4.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)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private data class DayData(
|
||
val date: LocalDate,
|
||
val isCurrentMonth: Boolean
|
||
)
|
||
|
||
@Suppress("DEPRECATION") // monthNumber 无替代 API,kotlinx-datetime 尚未提供新接口
|
||
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))
|
||
val nextMonth = if (month == 12) LocalDate(year + 1, 1, 1) else LocalDate(year, month + 1, 1)
|
||
val daysInMonth = nextMonth.minus(DatePeriod(days = 1)).dayOfMonth
|
||
val rows = ((offset + daysInMonth - 1) / 7) + 1
|
||
val totalDays = rows * 7
|
||
|
||
return (0 until totalDays).map { i ->
|
||
val date = startDate.plus(DatePeriod(days = i))
|
||
DayData(
|
||
date = date,
|
||
isCurrentMonth = date.monthNumber == month && date.year == year
|
||
)
|
||
}
|
||
} |