diff --git a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt index d2dcd61..29b373b 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt @@ -16,6 +16,7 @@ import kotlinx.datetime.number import kotlinx.datetime.plus import kotlinx.datetime.todayIn import kotlin.time.Clock +import plus.rua.project.ui.COLLAPSE_THRESHOLD data class CalendarDay( val date: LocalDate, @@ -56,11 +57,11 @@ class CalendarViewModel(private val coroutineScope: CoroutineScope) { } } - // 拖拽超过 50% 时自动折叠到周视图,否则回弹到月视图 + // 拖拽超过阈值时自动折叠到周视图,否则回弹到月视图 fun onDragEnd() { coroutineScope.launch { val current = _collapseAnimatable.value - if (current > 0.5f) { + if (current > COLLAPSE_THRESHOLD) { _collapseAnimatable.animateTo( targetValue = 1f, animationSpec = spring(dampingRatio = 0.8f, stiffness = 400f) @@ -83,11 +84,11 @@ class CalendarViewModel(private val coroutineScope: CoroutineScope) { } } - // 下拉超过 50% 时自动展开到月视图,否则回弹到周视图 + // 下拉超过阈值时自动展开到月视图,否则回弹到周视图 fun onExpandDragEnd() { coroutineScope.launch { val current = _collapseAnimatable.value - if (current < 0.5f) { + if (current < COLLAPSE_THRESHOLD) { _collapseAnimatable.animateTo( targetValue = 0f, animationSpec = spring(dampingRatio = 0.8f, stiffness = 400f) @@ -151,4 +152,4 @@ class CalendarViewModel(private val coroutineScope: CoroutineScope) { ) } } -} +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/BottomCard.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/BottomCard.kt index 12111fb..33f51bb 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/BottomCard.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/BottomCard.kt @@ -32,7 +32,7 @@ fun BottomCard( modifier: Modifier = Modifier ) { val density = LocalDensity.current - val dragRange = with(density) { 200.dp.toPx() } + val dragRange = with(density) { DRAG_RANGE_DP.dp.toPx() } Surface( modifier = modifier @@ -82,4 +82,4 @@ fun BottomCard( ) } } -} +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt index b3c79bf..6b888db 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt @@ -111,7 +111,7 @@ fun CalendarMonthPage( else Modifier ) .offset(y = yOffsetDp) - .padding(vertical = 4.dp) + .padding(vertical = ROW_PADDING_DP.dp) .then( if (weekIndex == 0 && rowHeightPx == 0) { Modifier.onSizeChanged { size -> @@ -157,4 +157,4 @@ private fun generateMonthDays(year: Int, month: Int): List { isCurrentMonth = date.month.number == month && date.year == year ) } -} +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index 7c6509e..b4fa034 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -31,10 +31,6 @@ import kotlin.math.abs import kotlin.time.Clock import plus.rua.project.CalendarViewModel -private const val START_PAGE = Int.MAX_VALUE / 2 -private const val ROW_PADDING_DP = 4 -private const val HORIZONTAL_PADDING_DP = 16 - /** * 日历主界面,包含月/周视图切换和折叠动画。 * @@ -50,8 +46,8 @@ fun CalendarMonthView( val coroutineScope = rememberCoroutineScope() val viewModel = remember { CalendarViewModel(coroutineScope) } val today = remember { Clock.System.todayIn(TimeZone.currentSystemDefault()) } - var currentYear by remember { mutableIntStateOf(viewModel.currentYear) } - var currentMonth by remember { mutableIntStateOf(viewModel.currentMonth) } + val currentYear by remember { derivedStateOf { viewModel.selectedDate.year } } + val currentMonth by remember { derivedStateOf { viewModel.selectedDate.month.number } } val density = LocalDensity.current var monthHeaderHeightPx by remember { mutableIntStateOf(0) } @@ -66,24 +62,19 @@ fun CalendarMonthView( val headerHeightPx = monthHeaderHeightPx + weekdayHeaderHeightPx val rowPaddingPx = with(density) { ROW_PADDING_DP.dp.toPx() }.toInt() - // 滑动偏移插值行数 - // 以 currentPage 为基准页,offsetFraction 表示基准页与可视区域左边缘的偏移: - // offsetFraction > 0:基准页偏右,可视区域露出下一页(page+1) - // offsetFraction < 0:基准页偏左,可视区域露出上一页(page-1) - // 过渡进度 = abs(offsetFraction),目标页 = page ± 1。 - // 当 currentPage 跳变(如从 Jul 跳到 Aug),基准页行数也随之跳变, - // 但 abs(offsetFraction) 同时从 ~0.5 降到 ~0.5(连续),所以插值结果连续: - // 跳变前: cp=Jul(5行), off=+0.49 → base=5, target=Aug(6), lerp(5,6,0.49)=5.49 - // 跳变后: cp=Aug(6行), off=-0.47 → base=6, target=Jul(5), lerp(6,5,0.47)=5.47 ← 连续! - val offsetFraction by remember { derivedStateOf { pagerState.currentPageOffsetFraction } } - val interpolatedWeeks = if (abs(offsetFraction) > 0.01f) { - val cp = pagerState.currentPage - val baseWeeks = calculateWeeksCountForPage(cp, today) - val targetPage = cp + if (offsetFraction > 0) 1 else -1 - val targetWeeks = calculateWeeksCountForPage(targetPage, today) - lerp(baseWeeks.toFloat(), targetWeeks.toFloat(), abs(offsetFraction)) - } else { - calculateWeeksCountForPage(pagerState.currentPage, today).toFloat() + val interpolatedWeeks by remember { + derivedStateOf { + val fraction = pagerState.currentPageOffsetFraction + if (abs(fraction) > OFFSET_FRACTION_THRESHOLD) { + val cp = pagerState.currentPage + val baseWeeks = calculateWeeksCountForPage(cp, today) + val targetPage = cp + if (fraction > 0) 1 else -1 + val targetWeeks = calculateWeeksCountForPage(targetPage, today) + lerp(baseWeeks.toFloat(), targetWeeks.toFloat(), abs(fraction)) + } else { + calculateWeeksCountForPage(pagerState.currentPage, today).toFloat() + } + } } // 预估行高:DayCell aspectRatio=1,宽度 = (screenWidth - horizontalPadding) / 7 @@ -100,14 +91,18 @@ fun CalendarMonthView( // gridH = rowH × (1 + (weeks-1) × (1-p)) val effectiveWeeks = interpolatedWeeks - val gridHeightPx = if (effectiveRowHeightPx > 0) { - val rowH = effectiveRowHeightPx.toFloat() - if (p > 0.01f) { - (rowH * (1 + (effectiveWeeks - 1) * (1f - p))).toInt() - } else { - (rowH * effectiveWeeks).toInt() + val gridHeightPx by remember { + derivedStateOf { + if (effectiveRowHeightPx > 0) { + val rowH = effectiveRowHeightPx.toFloat() + if (p > OFFSET_FRACTION_THRESHOLD) { + (rowH * (1 + (effectiveWeeks - 1) * (1f - p))).toInt() + } else { + (rowH * effectiveWeeks).toInt() + } + } else 0 } - } else 0 + } val calendarAreaHeightPx = headerHeightPx + gridHeightPx + rowPaddingPx val cardHeightPx = if (screenHeightPx > 0 && calendarAreaHeightPx > 0) screenHeightPx - calendarAreaHeightPx else 0 @@ -154,8 +149,6 @@ fun CalendarMonthView( val weekSunday = weekMonday.plus(DatePeriod(days = 6)) val date = if (today in weekMonday..weekSunday) today else weekMonday viewModel.selectDate(date) - currentYear = date.year - currentMonth = date.month.number } ) } else { @@ -167,14 +160,12 @@ fun CalendarMonthView( val date = if (year == today.year && today.month.number == month) today else LocalDate(year, month, 1) viewModel.selectDate(date) - currentYear = year - currentMonth = month }, collapseProgress = viewModel.collapseProgress, rowHeightPx = rowHeightPx, effectiveWeeks = effectiveWeeks, onRowHeightMeasured = { h -> - if (h > 0 && rowHeightPx == 0) rowHeightPx = h + if (h > 0) rowHeightPx = h }, pagerState = pagerState, modifier = pagerModifier @@ -192,16 +183,4 @@ fun CalendarMonthView( ) } } -} - -private fun lerp(start: Float, end: Float, fraction: Float): Float = start + (end - start) * fraction - -private fun calculateWeeksCountForPage(page: Int, today: LocalDate): Int { - val initialYear = today.year - val initialMonth = today.month.number - 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) -} +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt index 4033d04..f52bf50 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt @@ -11,14 +11,9 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch -import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate -import kotlinx.datetime.minus import kotlinx.datetime.number -/** 无限分页中心页,用于 HorizontalPager 的起始位置 */ -private const val START_PAGE = Int.MAX_VALUE / 2 - /** * 月度日历分页器,HorizontalPager 实现无限左右滑动切换月份。 * @@ -43,13 +38,14 @@ fun CalendarPager( pagerState: PagerState, modifier: Modifier = Modifier ) { - val initialYearMonth = remember { today.toYearMonth() } + val initialYear = remember { today.year } + 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, initialYearMonth) + val yearMonth = pageToYearMonth(page, initialYear, initialMonth) onMonthChanged(yearMonth.first, yearMonth.second) } } @@ -60,7 +56,7 @@ fun CalendarPager( flingBehavior = PagerDefaults.flingBehavior(state = pagerState), modifier = modifier ) { page -> - val (year, month) = pageToYearMonth(page, initialYearMonth) + val (year, month) = pageToYearMonth(page, initialYear, initialMonth) CalendarMonthPage( year = year, month = month, @@ -69,9 +65,10 @@ fun CalendarPager( onDateClick = { date -> onDateClick(date) // If clicking a date in a different month, scroll to that page - val clickedYearMonth = date.toYearMonth() - if (clickedYearMonth != pageToYearMonth(page, initialYearMonth)) { - val targetPage = yearMonthToPage(clickedYearMonth, initialYearMonth) + val clickedYear = date.year + val clickedMonth = date.month.number + if (clickedYear != year || clickedMonth != month) { + val targetPage = yearMonthToPage(clickedYear, clickedMonth, initialYear, initialMonth) if (targetPage != pagerState.currentPage) { coroutineScope.launch { pagerState.animateScrollToPage(targetPage) @@ -85,29 +82,4 @@ fun CalendarPager( onRowHeightMeasured = onRowHeightMeasured ) } -} - -private fun LocalDate.toYearMonth(): Pair = Pair(year, month.number) - -// 页码→年月:偏移量 + 初始月份的绝对月数,再拆分回年月 -private fun pageToYearMonth(page: Int, initial: Pair): Pair { - val offset = page - START_PAGE - val totalMonths = initial.first * 12 + (initial.second - 1) + offset - return Pair(totalMonths / 12, totalMonths % 12 + 1) -} - -// 年月→页码:目标与初始的绝对月数差 + 起始页 -private fun yearMonthToPage(yearMonth: Pair, initial: Pair): Int { - val targetTotal = yearMonth.first * 12 + (yearMonth.second - 1) - val initialTotal = initial.first * 12 + (initial.second - 1) - 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)).day - return ((offset + daysInMonth - 1) / 7) + 1 -} +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarUtils.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarUtils.kt new file mode 100644 index 0000000..5e8fa58 --- /dev/null +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarUtils.kt @@ -0,0 +1,86 @@ +package plus.rua.project.ui + +import kotlinx.datetime.DatePeriod +import kotlinx.datetime.LocalDate +import kotlinx.datetime.minus +import kotlinx.datetime.number +import kotlinx.datetime.plus + +/** 无限分页中心页,用于 HorizontalPager 的起始位置 */ +const val START_PAGE = Int.MAX_VALUE / 2 + +/** 折叠判定阈值:progress > 此值时折叠,< 此值时展开 */ +const val COLLAPSE_THRESHOLD = 0.5f + +/** 滑动偏移插值阈值:abs(offsetFraction) > 此值时启用插值 */ +const val OFFSET_FRACTION_THRESHOLD = 0.01f + +/** 行内 vertical padding (dp) */ +const val ROW_PADDING_DP = 4 + +/** 日历网格水平 padding (dp) */ +const val HORIZONTAL_PADDING_DP = 16 + +/** BottomCard 拖拽手势范围 (dp) */ +const val DRAG_RANGE_DP = 200 + +/** 线性插值 */ +fun lerp(start: Float, end: Float, fraction: Float): Float = start + (end - start) * fraction + +/** + * 计算月份在日历网格中需要的行数(4/5/6)。 + */ +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)).day + return ((offset + daysInMonth - 1) / 7) + 1 +} + +/** + * 根据 pager 页码计算该页月份的行数。 + */ +fun calculateWeeksCountForPage(page: Int, today: LocalDate): Int { + val initialYear = today.year + val initialMonth = today.month.number + 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) +} + +/** + * 页码转年月。 + */ +fun pageToYearMonth(page: Int, initialYear: Int, initialMonth: Int): Pair { + val offset = page - START_PAGE + val totalMonths = initialYear * 12 + (initialMonth - 1) + offset + return Pair(totalMonths / 12, totalMonths % 12 + 1) +} + +/** + * 年月转页码。 + */ +fun yearMonthToPage(year: Int, month: Int, initialYear: Int, initialMonth: Int): Int { + val targetTotal = year * 12 + (month - 1) + val initialTotal = initialYear * 12 + (initialMonth - 1) + return START_PAGE + (targetTotal - initialTotal) +} + +/** + * 获取日期所在周的周一。 + */ +fun LocalDate.toWeekMonday(): LocalDate { + val dayOfWeekOrdinal = dayOfWeek.ordinal + return minus(DatePeriod(days = dayOfWeekOrdinal)) +} + +/** + * 根据 pager 页码计算该页对应的周周一日期。 + */ +fun pageToWeekMonday(page: Int, initial: LocalDate): LocalDate { + val offset = page - START_PAGE + return initial.plus(DatePeriod(days = offset * 7)) +} diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt index 6c75cac..770ad70 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt @@ -9,20 +9,14 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.launch import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate -import kotlinx.datetime.minus import kotlinx.datetime.plus -/** 无限分页中心页,用于 HorizontalPager 的起始位置 */ -private const val START_PAGE = Int.MAX_VALUE / 2 - /** * 周视图分页器,折叠状态下显示选中日期所在周,支持左右滑动切换周。 * @@ -78,14 +72,4 @@ fun WeekPager( } } } -} - -private fun LocalDate.toWeekMonday(): LocalDate { - val dayOfWeekOrdinal = dayOfWeek.ordinal // Monday=0 ... Sunday=6 - return minus(DatePeriod(days = dayOfWeekOrdinal)) -} - -private fun pageToWeekMonday(page: Int, initial: LocalDate): LocalDate { - val offset = page - START_PAGE - return initial.plus(DatePeriod(days = offset * 7)) } \ No newline at end of file