Add KDoc for public APIs, suppress monthNumber deprecation, and refine comments

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-15 17:41:54 +08:00
parent 270346e6ef
commit 6351caf776
5 changed files with 118 additions and 16 deletions

View File

@ -19,6 +19,14 @@ import kotlin.time.Clock
import plus.rua.project.ui.COLLAPSE_THRESHOLD
import plus.rua.project.ui.FLING_VELOCITY_THRESHOLD_DP
/**
* 日历日期数据用于网格单元格渲染
*
* @param date 日期
* @param isCurrentMonth 是否属于当前显示月份
* @param isToday 是否为今天
* @param isSelected 是否为选中日期
*/
data class CalendarDay(
val date: LocalDate,
val isCurrentMonth: Boolean,
@ -30,6 +38,7 @@ data class CalendarDay(
* 日历状态管理持有选中日期折叠状态和 ISO 周号计算逻辑
*
* @param coroutineScope 协程作用域用于驱动折叠动画
* @param clock 时钟源默认系统时钟测试时可注入固定时钟
*/
class CalendarViewModel(
private val coroutineScope: CoroutineScope,
@ -47,13 +56,25 @@ class CalendarViewModel(
private val _collapseAnimatable = Animatable(0f)
val collapseProgress: Float get() = _collapseAnimatable.value
val currentYear: Int get() = selectedDate.year
@Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口
val currentMonth: Int get() = selectedDate.month.number
val currentYear: Int get() = selectedDate.year
/**
* 选中指定日期
*
* @param date 目标日期
*/
fun selectDate(date: LocalDate) {
selectedDate = date
}
/**
* 展开状态下拖拽折叠delta 正值推动 progress 1折叠方向
*
* @param delta 拖拽增量已归一化到 [0,1] 区间
*/
fun onDrag(delta: Float) {
coroutineScope.launch {
val new = (_collapseAnimatable.value + delta).coerceIn(0f, 1f)
@ -61,8 +82,13 @@ class CalendarViewModel(
}
}
// 拖拽超过阈值时自动折叠到周视图,否则回弹到月视图
// velocityDpPerSec: 松手时的 fling 速度 (dp/s),正值=上滑(折叠方向),负值=下滑(展开方向)
/**
* 展开状态拖拽结束根据进度和速度决定折叠或回弹
*
* 拖拽超过阈值时自动折叠到周视图否则回弹到月视图
*
* @param velocityDpPerSec 松手时的 fling 速度 (dp/s)正值=上滑折叠方向负值=下滑展开方向
*/
fun onDragEnd(velocityDpPerSec: Float = 0f) {
coroutineScope.launch {
val progress = _collapseAnimatable.value
@ -86,7 +112,11 @@ class CalendarViewModel(
}
}
// 折叠状态下下拉恢复delta 为负值(向下拖)推动 progress 向 0
/**
* 折叠状态下下拉恢复delta 为负值向下拖推动 progress 0
*
* @param delta 拖拽增量已归一化到 [0,1] 区间
*/
fun onExpandDrag(delta: Float) {
coroutineScope.launch {
val new = (_collapseAnimatable.value + delta).coerceIn(0f, 1f)
@ -94,8 +124,13 @@ class CalendarViewModel(
}
}
// 下拉超过阈值时自动展开到月视图,否则回弹到周视图
// velocityDpPerSec: 同上,正值=上滑,负值=下滑
/**
* 折叠状态拖拽结束根据进度和速度决定展开或回弹
*
* 下拉超过阈值时自动展开到月视图否则回弹到周视图
*
* @param velocityDpPerSec 松手时的 fling 速度 (dp/s)正值=上滑负值=下滑
*/
fun onExpandDragEnd(velocityDpPerSec: Float = 0f) {
coroutineScope.launch {
val progress = _collapseAnimatable.value
@ -149,6 +184,16 @@ class CalendarViewModel(
return diff / 7 + 1
}
/**
* 计算给定年月的日历网格数据包含跨月填充至完整行
*
* 网格行数按实际需要计算4/5/6每行7格首行从该月1号所在周的周一开始
*
* @param year 年份
* @param month 月份1-12
* @return 日历网格列表每项包含日期是否当月是否今天是否选中
*/
@Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口
fun getMonthDays(year: Int, month: Int): List<CalendarDay> {
val firstOfMonth = LocalDate(year, month, 1)
val dayOfWeekOffset = firstOfMonth.dayOfWeek.ordinal

View File

@ -26,7 +26,16 @@ import kotlinx.datetime.plus
* 折叠时非选中行高度按 (1-p) 缩放选中行保持原始高度
* 所有行通过手动 y-offset 定位形成向选中行收缩的视觉效果
*
* @param year 年份
* @param month 月份1-12
* @param selectedDate 当前选中日期
* @param today 今天的日期用于高亮标记
* @param onDateClick 日期点击回调
* @param collapseProgress 折叠进度0f=展开1f=折叠
* @param rowHeightPx 从外层传入的锁定行高像素折叠过程中不变
* @param effectiveWeeks 当前有效行数含翻页插值用于计算总高度
* @param onRowHeightMeasured 首次行高测量回调外层据此锁定行高
* @param modifier 外部布局修饰符
*/
@Composable
fun CalendarMonthPage(
@ -143,6 +152,7 @@ private data class DayData(
val isCurrentMonth: Boolean
)
@Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口
private fun generateMonthDays(year: Int, month: Int): List<DayData> {
val firstOfMonth = LocalDate(year, month, 1)
val offset = firstOfMonth.dayOfWeek.ordinal

View File

@ -47,6 +47,7 @@ fun CalendarMonthView(
val viewModel = remember { CalendarViewModel(coroutineScope) }
val today = remember { Clock.System.todayIn(TimeZone.currentSystemDefault()) }
val currentYear by remember { derivedStateOf { viewModel.selectedDate.year } }
@Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口
val currentMonth by remember { derivedStateOf { viewModel.selectedDate.month.number } }
val density = LocalDensity.current
@ -64,6 +65,7 @@ fun CalendarMonthView(
val cardGapPx = with(density) { lerp(CARD_GAP_EXPANDED_DP.toFloat(), CARD_GAP_COLLAPSED_DP.toFloat(), collapseProgress).dp.toPx() }.toInt()
// 翻页时在相邻月份行数之间插值,使 BottomCard 高度平滑过渡
// abs(fraction) > 阈值时启用插值,避免静止时的浮点抖动
val interpolatedWeeks by remember {
derivedStateOf {
val fraction = pagerState.currentPageOffsetFraction
@ -80,7 +82,7 @@ fun CalendarMonthView(
}
// 预估行高DayCell aspectRatio=1宽度 = (screenWidth - horizontalPadding) / 7
// 加上 Row 的 vertical padding (4dp × 2)
// 加上 Row 的 vertical padding (6dp × 2)
// 用于 rowHeightPx 尚未测量时的 fallback避免首次布局高度为 0
val estimatedRowHeightPx = if (screenWidthPx > 0) {
val cellWidth = (screenWidthPx - with(density) { (HORIZONTAL_PADDING_DP * 2).dp.toPx() }) / 7
@ -95,8 +97,8 @@ fun CalendarMonthView(
// 折叠时网格高度公式(与 CalendarMonthPage 一致):
// collapseProgress=0 展开时 gridH = rowH × weekscollapseProgress=1 折叠时 gridH = rowH × 1
// 中间态gridH = rowH × (1 + (weeks-1) × (1-collapseProgress))
// 必须直接计算而非 derivedStateOfeffectiveRowHeightPx 依赖 rowHeightPx state
// derivedStateOf 无法追踪非 State 局部变量变化导致 rowHeightPx 从 0 变为测量值时 gridHeightPx 不更新
// 直接计算而非 derivedStateOfeffectiveRowHeightPx 依赖 rowHeightPx state
// derivedStateOf 无法追踪非 State 局部变量rowHeightPx 从 0 变为测量值时 gridHeightPx 不更新
val gridHeightPx = if (effectiveRowHeightPx > 0) {
val rowH = effectiveRowHeightPx.toFloat()
if (collapseProgress > OFFSET_FRACTION_THRESHOLD) {
@ -110,7 +112,7 @@ fun CalendarMonthView(
val calendarAreaHeightPx = headerHeightPx + gridHeightPx + rowPaddingPx + cardGapPx
val cardHeightPx = if (screenHeightPx > 0 && calendarAreaHeightPx > 0) screenHeightPx - calendarAreaHeightPx else 0
// 行高已知时约束 pager 高度防止内容溢出;否则让 pager 自由扩展以触发首次行高测量
// 行高已知时约束 pager 高度防止内容溢出;否则让 pager 自由扩展以触发首次行高测量
val pagerModifier = if (rowHeightPx > 0 && gridHeightPx > 0) {
Modifier
.height(with(density) { gridHeightPx.toDp() })
@ -164,6 +166,7 @@ fun CalendarMonthView(
onDateClick = { date -> viewModel.selectDate(date) },
onMonthChanged = { year, month ->
// 优先选中当月内的今天否则选中该月1号
@Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口
val date = if (year == today.year && today.month.number == month) today
else LocalDate(year, month, 1)
viewModel.selectDate(date)

View File

@ -17,12 +17,18 @@ import kotlinx.datetime.number
/**
* 月度日历分页器HorizontalPager 实现无限左右滑动切换月份
*
* 使用 Int.MAX_VALUE 页数中心页为起始月份点击跨月日期时自动滚动到对应页
* 跳过初始 snapshotFlow 发射以保留"今天"选中状态
*
* @param selectedDate 当前选中日期
* @param today 今天的日期
* @param onDateClick 日期点击回调
* @param onMonthChanged 月份切换回调滑动到新月份触发
* @param onMonthChanged 月份切换回调滑动到新月份稳定后触发
* @param collapseProgress 折叠进度0f=展开1f=折叠
* @param rowHeightPx 从外层传入的锁定行高像素折叠过程中不变
* @param rowHeightPx 锁定行高像素
* @param effectiveWeeks 当前有效行数含翻页插值
* @param onRowHeightMeasured 首次行高测量回调
* @param pagerState 外层共享的 PagerState用于保持翻页状态
* @param modifier 外部布局修饰符
*/
@Composable
@ -39,10 +45,11 @@ fun CalendarPager(
modifier: Modifier = Modifier
) {
val initialYear = remember { today.year }
@Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口
val initialMonth = remember { today.month.number }
val coroutineScope = rememberCoroutineScope()
// Sync settled page to onMonthChanged (skip initial emission to preserve "today" selection)
// 跳过初始发射,保留首次渲染时的"今天"选中状态
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.settledPage }.drop(1).collect { page ->
val yearMonth = pageToYearMonth(page, initialYear, initialMonth)
@ -63,9 +70,10 @@ fun CalendarPager(
selectedDate = selectedDate,
today = today,
onDateClick = { date ->
onDateClick(date)
// If clicking a date in a different month, scroll to that page
val clickedYear = date.year
onDateClick(date)
// 点击跨月日期时,滚动到该月对应的页
val clickedYear = date.year
@Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口
val clickedMonth = date.month.number
if (clickedYear != year || clickedMonth != month) {
val targetPage = yearMonthToPage(clickedYear, clickedMonth, initialYear, initialMonth)

View File

@ -38,6 +38,10 @@ fun lerp(start: Float, end: Float, fraction: Float): Float = start + (end - star
/**
* 计算月份在日历网格中需要的行数4/5/6
*
* @param year 年份
* @param month 月份1-12
* @return 网格行数
*/
fun calculateWeeksCount(year: Int, month: Int): Int {
val firstOfMonth = LocalDate(year, month, 1)
@ -49,9 +53,14 @@ fun calculateWeeksCount(year: Int, month: Int): Int {
/**
* 根据 pager 页码计算该页月份的行数
*
* @param page 分页器页码
* @param today 今天的日期用于确定起始月份
* @return 网格行数
*/
fun calculateWeeksCountForPage(page: Int, today: LocalDate): Int {
val initialYear = today.year
@Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口
val initialMonth = today.month.number
val offset = page - START_PAGE
val totalMonths = initialYear * 12 + (initialMonth - 1) + offset
@ -62,6 +71,14 @@ fun calculateWeeksCountForPage(page: Int, today: LocalDate): Int {
/**
* 页码转年月
*
* 中心页 (Int.MAX_VALUE/2) 对应起始月份向左递减向右递增
* 自动处理跨年121
*
* @param page 分页器页码
* @param initialYear 起始年份中心页对应的年份
* @param initialMonth 起始月份中心页对应的月份1-12
* @return Pair(year, month)
*/
fun pageToYearMonth(page: Int, initialYear: Int, initialMonth: Int): Pair<Int, Int> {
val offset = page - START_PAGE
@ -71,6 +88,14 @@ fun pageToYearMonth(page: Int, initialYear: Int, initialMonth: Int): Pair<Int, I
/**
* 年月转页码
*
* [pageToYearMonth] 的逆运算用于点击跨月日期时定位目标页
*
* @param year 目标年份
* @param month 目标月份1-12
* @param initialYear 起始年份
* @param initialMonth 起始月份
* @return 分页器页码
*/
fun yearMonthToPage(year: Int, month: Int, initialYear: Int, initialMonth: Int): Int {
val targetTotal = year * 12 + (month - 1)
@ -80,6 +105,11 @@ fun yearMonthToPage(year: Int, month: Int, initialYear: Int, initialMonth: Int):
/**
* 获取日期所在周的周一
*
* ISO 8601 周从周一开始周一返回自身其他日期回退到该周周一
*
* @receiver 目标日期
* @return 该日期所在周的周一
*/
fun LocalDate.toWeekMonday(): LocalDate {
val dayOfWeekOrdinal = dayOfWeek.ordinal
@ -88,6 +118,12 @@ fun LocalDate.toWeekMonday(): LocalDate {
/**
* 根据 pager 页码计算该页对应的周周一日期
*
* 中心页对应参考周一向左/右每页偏移一周用于 WeekPager 的单周视图渲染
*
* @param page 分页器页码
* @param initial 参考周一日期中心页对应的周一
* @return 该页周一的 LocalDate
*/
fun pageToWeekMonday(page: Int, initial: LocalDate): LocalDate {
val offset = page - START_PAGE