Pass effectiveWeeks from CalendarMonthView through CalendarPager to CalendarMonthPage so both use the same formula H*(1+(weeks-1)*(1-p)). Fix interpolatedWeeks to anchor on settledPage instead of currentPage, preventing direction/fraction discontinuity when currentPage jumps during swipe transitions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
218 lines
9.6 KiB
Kotlin
218 lines
9.6 KiB
Kotlin
package plus.rua.project.ui
|
||
|
||
import androidx.compose.foundation.layout.Box
|
||
import androidx.compose.foundation.layout.Column
|
||
import androidx.compose.foundation.layout.fillMaxSize
|
||
import androidx.compose.foundation.layout.fillMaxWidth
|
||
import androidx.compose.foundation.layout.height
|
||
import androidx.compose.foundation.layout.padding
|
||
import androidx.compose.foundation.layout.statusBarsPadding
|
||
import androidx.compose.foundation.pager.rememberPagerState
|
||
import androidx.compose.runtime.Composable
|
||
import androidx.compose.runtime.derivedStateOf
|
||
import androidx.compose.runtime.getValue
|
||
import androidx.compose.runtime.mutableIntStateOf
|
||
import androidx.compose.runtime.remember
|
||
import androidx.compose.runtime.rememberCoroutineScope
|
||
import androidx.compose.runtime.setValue
|
||
import androidx.compose.ui.Alignment
|
||
import androidx.compose.ui.Modifier
|
||
import androidx.compose.ui.draw.clipToBounds
|
||
import androidx.compose.ui.layout.onSizeChanged
|
||
import androidx.compose.ui.platform.LocalDensity
|
||
import androidx.compose.ui.unit.dp
|
||
import kotlinx.datetime.DatePeriod
|
||
import kotlinx.datetime.LocalDate
|
||
import kotlinx.datetime.TimeZone
|
||
import kotlinx.datetime.plus
|
||
import kotlinx.datetime.todayIn
|
||
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 TAG = "CalMonthView"
|
||
|
||
/**
|
||
* 日历主界面,包含月/周视图切换和折叠动画。
|
||
*
|
||
* 折叠时日历从月视图收缩为周视图(1行),BottomCard 同步上移填充空间。
|
||
* 支持动态行数(4/5/6行),滑动切换月份时 BottomCard 跟手移动。
|
||
*
|
||
* @param modifier 外部布局修饰符
|
||
*/
|
||
@Composable
|
||
fun CalendarMonthView(
|
||
modifier: Modifier = Modifier
|
||
) {
|
||
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 density = LocalDensity.current
|
||
|
||
var monthHeaderHeightPx by remember { mutableIntStateOf(0) }
|
||
var weekdayHeaderHeightPx by remember { mutableIntStateOf(0) }
|
||
var rowHeightPx by remember { mutableIntStateOf(0) }
|
||
@Suppress("DEPRECATION") // monthNumber 无替代 API,kotlinx-datetime 尚未提供新接口
|
||
var currentWeeksCount by remember { mutableIntStateOf(calculateWeeksCount(today.year, today.monthNumber)) }
|
||
var screenWidthPx by remember { mutableIntStateOf(0) }
|
||
var screenHeightPx by remember { mutableIntStateOf(0) }
|
||
|
||
val pagerState = rememberPagerState(initialPage = START_PAGE, pageCount = { Int.MAX_VALUE })
|
||
|
||
val p = viewModel.collapseProgress
|
||
val headerHeightPx = monthHeaderHeightPx + weekdayHeaderHeightPx
|
||
val rowPaddingPx = with(density) { ROW_PADDING_DP.dp.toPx() }.toInt()
|
||
|
||
// 滑动偏移插值行数
|
||
// 始终以 settledPage 为锚点,currentPage - settledPage 确定方向(-1/0/+1),
|
||
// abs(offsetFraction) 为过渡进度。
|
||
// 这样在 currentPage 跳变前后,方向和进度都是连续的:
|
||
// 跳变前: sp=8月, cp=8月, diff=0, offsetFraction>0 → 目标9月, fraction 0→0.5
|
||
// 跳变后: sp=8月, cp=9月, diff=+1 → 目标9月, fraction 0.5→0
|
||
val offsetFraction by remember { derivedStateOf { pagerState.currentPageOffsetFraction } }
|
||
val interpolatedWeeks = if (abs(offsetFraction) > 0.01f) {
|
||
val sp = pagerState.settledPage
|
||
val diff = pagerState.currentPage - sp // -1, 0, or +1
|
||
val targetPage = if (diff != 0) sp + diff else sp + if (offsetFraction > 0) 1 else -1
|
||
val baseWeeks = calculateWeeksCountForPage(sp, today)
|
||
val targetWeeks = calculateWeeksCountForPage(targetPage, today)
|
||
lerp(baseWeeks.toFloat(), targetWeeks.toFloat(), abs(offsetFraction))
|
||
} else {
|
||
currentWeeksCount.toFloat()
|
||
}
|
||
|
||
// 预估行高:DayCell aspectRatio=1,宽度 = (screenWidth - horizontalPadding) / 7
|
||
// 加上 Row 的 vertical padding (4dp × 2)
|
||
val estimatedRowHeightPx = if (screenWidthPx > 0) {
|
||
val cellWidth = (screenWidthPx - with(density) { 32.dp.toPx() }) / 7
|
||
val rowPadding = with(density) { 8.dp.toPx() }
|
||
(cellWidth + rowPadding).toInt()
|
||
} else 0
|
||
|
||
val effectiveRowHeightPx = if (rowHeightPx > 0) rowHeightPx else estimatedRowHeightPx
|
||
|
||
// 折叠时网格高度公式(与 CalendarMonthPage 一致):
|
||
// 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()
|
||
}
|
||
} else 0
|
||
|
||
val calendarAreaHeightPx = headerHeightPx + gridHeightPx + rowPaddingPx
|
||
val cardHeightPx = if (screenHeightPx > 0 && calendarAreaHeightPx > 0) screenHeightPx - calendarAreaHeightPx else 0
|
||
|
||
println("[$TAG] p=$p rowH=$rowHeightPx estRowH=$estimatedRowHeightPx effRowH=$effectiveRowHeightPx " +
|
||
"headerH=$headerHeightPx gridH=$gridHeightPx calAreaH=$calendarAreaHeightPx " +
|
||
"screenH=$screenHeightPx cardH=$cardHeightPx " +
|
||
"currentWeeks=$currentWeeksCount interpolatedWeeks=$interpolatedWeeks effectiveWeeks=$effectiveWeeks " +
|
||
"offsetFraction=$offsetFraction currentPage=${pagerState.currentPage} settledPage=${pagerState.settledPage}")
|
||
|
||
// 当 rowHeightPx 已知时,用计算的高度约束 pager;否则让 pager 自由扩展以测量行高
|
||
val pagerModifier = if (rowHeightPx > 0 && gridHeightPx > 0) {
|
||
Modifier
|
||
.height(with(density) { gridHeightPx.toDp() })
|
||
.clipToBounds()
|
||
} else {
|
||
Modifier
|
||
}
|
||
|
||
Box(
|
||
modifier = modifier
|
||
.fillMaxSize()
|
||
.statusBarsPadding()
|
||
.onSizeChanged { size ->
|
||
screenWidthPx = size.width
|
||
screenHeightPx = size.height
|
||
}
|
||
) {
|
||
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||
MonthHeader(
|
||
year = currentYear,
|
||
month = currentMonth,
|
||
weekNumber = viewModel.getIsoWeekNumber(viewModel.selectedDate),
|
||
modifier = Modifier.onSizeChanged { size ->
|
||
monthHeaderHeightPx = size.height
|
||
}
|
||
)
|
||
WeekdayHeader(
|
||
modifier = Modifier.fillMaxWidth().onSizeChanged { size ->
|
||
weekdayHeaderHeightPx = size.height
|
||
}.padding(bottom = ROW_PADDING_DP.dp)
|
||
)
|
||
// 完全折叠且无动画时显示 WeekPager,否则显示 CalendarPager(含下拉恢复过程)
|
||
if (viewModel.isCollapsed && viewModel.collapseProgress >= 1f) {
|
||
WeekPager(
|
||
selectedDate = viewModel.selectedDate,
|
||
today = today,
|
||
onDateClick = { date -> viewModel.selectDate(date) },
|
||
onWeekChanged = { weekMonday ->
|
||
val weekSunday = weekMonday.plus(DatePeriod(days = 6))
|
||
val date = if (today >= weekMonday && today <= weekSunday) today else weekMonday
|
||
viewModel.selectDate(date)
|
||
currentYear = date.year
|
||
@Suppress("DEPRECATION") // monthNumber 无替代 API,kotlinx-datetime 尚未提供新接口
|
||
currentMonth = date.monthNumber
|
||
}
|
||
)
|
||
} else {
|
||
CalendarPager(
|
||
selectedDate = viewModel.selectedDate,
|
||
today = today,
|
||
onDateClick = { date -> viewModel.selectDate(date) },
|
||
onMonthChanged = { year, month ->
|
||
val date = if (year == today.year && today.monthNumber == month) today
|
||
else LocalDate(year, month, 1)
|
||
viewModel.selectDate(date)
|
||
currentYear = year
|
||
currentMonth = month
|
||
},
|
||
collapseProgress = viewModel.collapseProgress,
|
||
rowHeightPx = rowHeightPx,
|
||
effectiveWeeks = effectiveWeeks,
|
||
onWeeksChanged = { weeks ->
|
||
currentWeeksCount = weeks
|
||
},
|
||
onRowHeightMeasured = { h ->
|
||
if (h > 0 && rowHeightPx == 0) rowHeightPx = h
|
||
},
|
||
pagerState = pagerState,
|
||
modifier = pagerModifier
|
||
)
|
||
}
|
||
}
|
||
|
||
if (cardHeightPx > 0) {
|
||
BottomCard(
|
||
viewModel = viewModel,
|
||
modifier = Modifier
|
||
.fillMaxWidth()
|
||
.height(with(density) { cardHeightPx.toDp() })
|
||
.align(Alignment.BottomCenter)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
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)
|
||
}
|