Use dynamic row count (4/5/6) for calendar grid instead of fixed 6 rows
Calculate actual weeks needed per month and interpolate row count during page swipe so BottomCard follows the grid height smoothly. Remove debug println statements. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
98e91c273e
commit
b94b264d5c
@ -135,9 +135,12 @@ class CalendarViewModel(private val coroutineScope: CoroutineScope) {
|
|||||||
val firstOfMonth = LocalDate(year, month, 1)
|
val firstOfMonth = LocalDate(year, month, 1)
|
||||||
val dayOfWeekOffset = firstOfMonth.dayOfWeek.ordinal
|
val dayOfWeekOffset = firstOfMonth.dayOfWeek.ordinal
|
||||||
val startDate = firstOfMonth.minus(DatePeriod(days = dayOfWeekOffset))
|
val startDate = firstOfMonth.minus(DatePeriod(days = dayOfWeekOffset))
|
||||||
|
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 = ((dayOfWeekOffset + daysInMonth - 1) / 7) + 1
|
||||||
|
val totalDays = rows * 7
|
||||||
|
|
||||||
// 6行×7列=42格,覆盖跨月首尾周,保证网格完整
|
return (0 until totalDays).map { i ->
|
||||||
return (0 until 42).map { i ->
|
|
||||||
val date = startDate.plus(DatePeriod(days = i))
|
val date = startDate.plus(DatePeriod(days = i))
|
||||||
CalendarDay(
|
CalendarDay(
|
||||||
date = date,
|
date = date,
|
||||||
|
|||||||
@ -30,6 +30,7 @@ fun CalendarMonthPage(
|
|||||||
today: LocalDate,
|
today: LocalDate,
|
||||||
onDateClick: (LocalDate) -> Unit,
|
onDateClick: (LocalDate) -> Unit,
|
||||||
collapseProgress: Float,
|
collapseProgress: Float,
|
||||||
|
onRowHeightMeasured: ((Int) -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val days = remember(year, month) {
|
val days = remember(year, month) {
|
||||||
@ -66,11 +67,7 @@ fun CalendarMonthPage(
|
|||||||
Box(modifier = modifier.clipToBounds().then(
|
Box(modifier = modifier.clipToBounds().then(
|
||||||
if (totalHeightDp != null) Modifier.height(totalHeightDp)
|
if (totalHeightDp != null) Modifier.height(totalHeightDp)
|
||||||
else Modifier
|
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 = hasSelectedWeek && weekIndex == selectedWeekIndex
|
val isSelected = hasSelectedWeek && weekIndex == selectedWeekIndex
|
||||||
val isAbove = hasSelectedWeek && weekIndex < selectedWeekIndex
|
val isAbove = hasSelectedWeek && weekIndex < selectedWeekIndex
|
||||||
@ -149,8 +146,12 @@ 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))
|
||||||
|
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 42).map { i ->
|
return (0 until totalDays).map { i ->
|
||||||
val date = startDate.plus(DatePeriod(days = i))
|
val date = startDate.plus(DatePeriod(days = i))
|
||||||
DayData(
|
DayData(
|
||||||
date = date,
|
date = date,
|
||||||
|
|||||||
@ -7,7 +7,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
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.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -24,13 +26,17 @@ import kotlinx.datetime.LocalDate
|
|||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.plus
|
import kotlinx.datetime.plus
|
||||||
import kotlinx.datetime.todayIn
|
import kotlinx.datetime.todayIn
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import plus.rua.project.CalendarViewModel
|
import plus.rua.project.CalendarViewModel
|
||||||
|
|
||||||
|
private const val START_PAGE = Int.MAX_VALUE / 2
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日历主界面,包含月/周视图切换和折叠动画。
|
* 日历主界面,包含月/周视图切换和折叠动画。
|
||||||
*
|
*
|
||||||
* 折叠时日历从月视图(6行)收缩为周视图(1行),BottomCard 同步上移填充空间。
|
* 折叠时日历从月视图收缩为周视图(1行),BottomCard 同步上移填充空间。
|
||||||
|
* 支持动态行数(4/5/6行),滑动切换月份时 BottomCard 跟手移动。
|
||||||
*
|
*
|
||||||
* @param modifier 外部布局修饰符
|
* @param modifier 外部布局修饰符
|
||||||
*/
|
*/
|
||||||
@ -49,22 +55,39 @@ fun CalendarMonthView(
|
|||||||
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) }
|
var screenHeightPx by remember { mutableIntStateOf(0) }
|
||||||
|
var currentWeeksCount by remember { mutableIntStateOf(6) }
|
||||||
|
var expandedWeeksCount by remember { mutableIntStateOf(6) }
|
||||||
|
|
||||||
|
val pagerState = rememberPagerState(initialPage = START_PAGE, pageCount = { Int.MAX_VALUE })
|
||||||
|
|
||||||
val p = viewModel.collapseProgress
|
val p = viewModel.collapseProgress
|
||||||
val headerHeightPx = monthHeaderHeightPx + weekdayHeaderHeightPx
|
val headerHeightPx = monthHeaderHeightPx + weekdayHeaderHeightPx
|
||||||
|
|
||||||
// 展开时网格高度 = 首次测量的日历总高度 - headers
|
// 单行高度:从首次展开时测量并锁定(基于 expandedWeeksCount)
|
||||||
val expandedGridHeightPx = calendarHeightPx - headerHeightPx
|
val rowHeightPx = if (calendarHeightPx > 0 && expandedWeeksCount > 0) {
|
||||||
val weeksCount = 6
|
(calendarHeightPx - headerHeightPx) / expandedWeeksCount
|
||||||
|
} else 0
|
||||||
|
|
||||||
|
// 滑动偏移插值行数
|
||||||
|
val offsetFraction by remember { derivedStateOf { pagerState.currentPageOffsetFraction } }
|
||||||
|
val interpolatedWeeks = if (abs(offsetFraction) > 0.01f) {
|
||||||
|
val targetPage = if (offsetFraction > 0) pagerState.currentPage + 1 else pagerState.currentPage - 1
|
||||||
|
val targetWeeks = calculateWeeksCountForPage(targetPage, today)
|
||||||
|
lerp(currentWeeksCount.toFloat(), targetWeeks.toFloat(), abs(offsetFraction))
|
||||||
|
} else {
|
||||||
|
currentWeeksCount.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
// 折叠时网格高度公式(与 CalendarMonthPage 一致):
|
// 折叠时网格高度公式(与 CalendarMonthPage 一致):
|
||||||
// gridH = rowH × (1 + (weeks-1) × (1-p))
|
// gridH = rowH × (1 + (weeks-1) × (1-p))
|
||||||
// 其中 rowH = expandedGridHeightPx / weeksCount
|
val gridHeightPx = if (rowHeightPx > 0) {
|
||||||
val gridHeightPx = if (expandedGridHeightPx > 0 && p > 0f) {
|
val rowH = rowHeightPx.toFloat()
|
||||||
val rowH = expandedGridHeightPx.toFloat() / weeksCount
|
val weeks = interpolatedWeeks
|
||||||
(rowH * (1 + (weeksCount - 1) * (1f - p))).toInt()
|
if (p > 0f) {
|
||||||
} else if (expandedGridHeightPx > 0) {
|
(rowH * (1 + (weeks - 1) * (1f - p))).toInt()
|
||||||
expandedGridHeightPx
|
} else {
|
||||||
|
(rowH * weeks).toInt()
|
||||||
|
}
|
||||||
} else 0
|
} else 0
|
||||||
|
|
||||||
val rowPaddingPx = with(density) { 4.dp.toPx() }.toInt()
|
val rowPaddingPx = with(density) { 4.dp.toPx() }.toInt()
|
||||||
@ -72,7 +95,7 @@ fun CalendarMonthView(
|
|||||||
val cardTopPx = headerHeightPx + gridHeightPx + rowPaddingPx
|
val cardTopPx = headerHeightPx + gridHeightPx + rowPaddingPx
|
||||||
val cardHeightPx = screenHeightPx - cardTopPx
|
val cardHeightPx = screenHeightPx - cardTopPx
|
||||||
|
|
||||||
val pagerModifier = if (p > 0.01f && expandedGridHeightPx > 0) {
|
val pagerModifier = if (p > 0.01f && rowHeightPx > 0) {
|
||||||
Modifier
|
Modifier
|
||||||
.height(with(density) { gridHeightPx.toDp() })
|
.height(with(density) { gridHeightPx.toDp() })
|
||||||
.clipToBounds()
|
.clipToBounds()
|
||||||
@ -80,10 +103,6 @@ fun CalendarMonthView(
|
|||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
@ -93,10 +112,8 @@ fun CalendarMonthView(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(horizontal = 16.dp).onSizeChanged { size ->
|
Column(modifier = Modifier.padding(horizontal = 16.dp).onSizeChanged { size ->
|
||||||
// 仅在展开时记录日历总高度(折叠时 HorizontalPager 不缩小)
|
calendarHeightPx = size.height
|
||||||
if (p < 0.01f) {
|
if (p < 0.01f) expandedWeeksCount = currentWeeksCount
|
||||||
calendarHeightPx = size.height
|
|
||||||
}
|
|
||||||
}) {
|
}) {
|
||||||
MonthHeader(
|
MonthHeader(
|
||||||
year = currentYear,
|
year = currentYear,
|
||||||
@ -139,6 +156,11 @@ fun CalendarMonthView(
|
|||||||
currentMonth = month
|
currentMonth = month
|
||||||
},
|
},
|
||||||
collapseProgress = viewModel.collapseProgress,
|
collapseProgress = viewModel.collapseProgress,
|
||||||
|
onWeeksChanged = { weeks ->
|
||||||
|
currentWeeksCount = weeks
|
||||||
|
if (p < 0.01f) expandedWeeksCount = weeks
|
||||||
|
},
|
||||||
|
pagerState = pagerState,
|
||||||
modifier = pagerModifier
|
modifier = pagerModifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -153,4 +175,17 @@ fun CalendarMonthView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun lerp(start: Float, end: Float, fraction: Float): Float = start + (end - start) * fraction
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION") // monthNumber 无替代 API,kotlinx-datetime 尚未提供新接口
|
||||||
|
private fun calculateWeeksCountForPage(page: Int, today: LocalDate): Int {
|
||||||
|
val initialYear = today.year
|
||||||
|
val initialMonth = today.monthNumber
|
||||||
|
val offset = page - START_PAGE
|
||||||
|
val totalMonths = initialYear * 12 + (initialMonth - 1) + offset
|
||||||
|
val year = totalMonths / 12
|
||||||
|
val month = totalMonths % 12 + 1
|
||||||
|
return calculateWeeksCount(year, month)
|
||||||
}
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package plus.rua.project.ui
|
package plus.rua.project.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.PagerDefaults
|
import androidx.compose.foundation.pager.PagerDefaults
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
@ -11,7 +12,9 @@ import androidx.compose.runtime.snapshotFlow
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.DatePeriod
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.minus
|
||||||
|
|
||||||
/** 无限分页中心页,用于 HorizontalPager 的起始位置 */
|
/** 无限分页中心页,用于 HorizontalPager 的起始位置 */
|
||||||
private const val START_PAGE = Int.MAX_VALUE / 2
|
private const val START_PAGE = Int.MAX_VALUE / 2
|
||||||
@ -33,19 +36,19 @@ fun CalendarPager(
|
|||||||
onDateClick: (LocalDate) -> Unit,
|
onDateClick: (LocalDate) -> Unit,
|
||||||
onMonthChanged: (year: Int, month: Int) -> Unit,
|
onMonthChanged: (year: Int, month: Int) -> Unit,
|
||||||
collapseProgress: Float,
|
collapseProgress: Float,
|
||||||
|
onWeeksChanged: ((Int) -> Unit)? = null,
|
||||||
|
onRowHeightMeasured: ((Int) -> Unit)? = null,
|
||||||
|
pagerState: PagerState,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val initialYearMonth = remember { today.toYearMonth() }
|
val initialYearMonth = remember { today.toYearMonth() }
|
||||||
val pagerState = rememberPagerState(
|
|
||||||
initialPage = START_PAGE,
|
|
||||||
pageCount = { Int.MAX_VALUE }
|
|
||||||
)
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
// Sync settled page to onMonthChanged (skip initial emission to preserve "today" selection)
|
// Sync settled page to onMonthChanged (skip initial emission to preserve "today" selection)
|
||||||
LaunchedEffect(pagerState) {
|
LaunchedEffect(pagerState) {
|
||||||
snapshotFlow { pagerState.settledPage }.drop(1).collect { page ->
|
snapshotFlow { pagerState.settledPage }.drop(1).collect { page ->
|
||||||
val yearMonth = pageToYearMonth(page, initialYearMonth)
|
val yearMonth = pageToYearMonth(page, initialYearMonth)
|
||||||
|
onWeeksChanged?.invoke(calculateWeeksCount(yearMonth.first, yearMonth.second))
|
||||||
onMonthChanged(yearMonth.first, yearMonth.second)
|
onMonthChanged(yearMonth.first, yearMonth.second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,7 +78,8 @@ fun CalendarPager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
collapseProgress = collapseProgress
|
collapseProgress = collapseProgress,
|
||||||
|
onRowHeightMeasured = if (page == pagerState.currentPage) onRowHeightMeasured else null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,3 +100,12 @@ private fun yearMonthToPage(yearMonth: Pair<Int, Int>, initial: Pair<Int, Int>):
|
|||||||
val initialTotal = initial.first * 12 + (initial.second - 1)
|
val initialTotal = initial.first * 12 + (initial.second - 1)
|
||||||
return START_PAGE + (targetTotal - initialTotal)
|
return START_PAGE + (targetTotal - initialTotal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算月份在日历网格中需要的行数(4/5/6)
|
||||||
|
internal fun calculateWeeksCount(year: Int, month: Int): Int {
|
||||||
|
val firstOfMonth = LocalDate(year, month, 1)
|
||||||
|
val offset = firstOfMonth.dayOfWeek.ordinal
|
||||||
|
val nextMonth = if (month == 12) LocalDate(year + 1, 1, 1) else LocalDate(year, month + 1, 1)
|
||||||
|
val daysInMonth = nextMonth.minus(DatePeriod(days = 1)).dayOfMonth
|
||||||
|
return ((offset + daysInMonth - 1) / 7) + 1
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user