From ed1935c9fbb26fa3ed8ef1f7b18266e6a2363248 Mon Sep 17 00:00:00 2001 From: meyou <2636699780@qq.com> Date: Mon, 18 May 2026 23:08:12 +0800 Subject: [PATCH] =?UTF-8?q?DayCell=20SolarDay=20=E9=9D=99=E6=80=81?= =?UTF-8?q?=E7=BC=93=E5=AD=98=EF=BC=9A=E9=81=BF=E5=85=8D=20Pager=20?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E6=97=B6=E9=87=8D=E5=A4=8D=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E8=A7=A6=E5=8F=91=20GC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 每个 DayCell 创建时调用两次 SolarDay.fromYmd() 计算节日/农历信息。 Pager 缓存页的大量 DayCell 同时重建时产生大量临时对象,加剧 GC 压力。 修复:在 DayCell.kt 中增加进程级静态缓存 dayCellInfoCache,按日期缓存 computeDayCellInfo() 的结果。首次计算后永久复用,消除重复对象创建。 --- .../plus/rua/project/CalendarViewModel.kt | 34 +++++--- .../plus/rua/project/ui/CalendarMonthView.kt | 78 ++++++++++------- .../kotlin/plus/rua/project/ui/DayCell.kt | 85 ++++++++++--------- 3 files changed, 112 insertions(+), 85 deletions(-) diff --git a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt index d0df349..eeb795a 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt @@ -108,12 +108,13 @@ class CalendarViewModel( /** * 切换年视图。仅在展开态可用。 * - * 月/年视图始终共存于组合树中,由 alpha 控制可见性。 - * 翻转 isYearView 后启动 Animatable 动画,驱动对应方向视图的 scale/alpha 变化。 + * 切换瞬间立即翻转 isYearView,让对应方向的目标视图立刻接管渲染, + * 当前视图被直接移除;动画只作用在目标视图的 scale/alpha 上。 */ fun toggleYearView() { yearViewJob?.cancel() yearViewJob = coroutineScope.launch { + // 折叠态先展开回月视图,再切换年视图 if (isCollapsed) { _collapseAnimatable.animateTo( 0f, spring(dampingRatio = 0.8f, stiffness = 400f) @@ -121,19 +122,32 @@ class CalendarViewModel( isCollapsed = false } if (isYearView) { - // 年 → 月:动画驱动 yearViewProgress 1f→0f,月视图同步放大/淡入 - _yearViewAnimatable.animateTo( - 0f, tween(400, easing = FastOutSlowInEasing) - ) + // 年 → 月:先启动动画(年视图开始淡出),等一帧后翻转 isYearView(月视图开始组合) + composeTraceBeginSection("YearView→MonthView") + _yearViewAnimatable.snapTo(1f) + val animJob = launch { + _yearViewAnimatable.animateTo( + 0f, tween(400, easing = FastOutSlowInEasing) + ) + } + withFrameNanos { } isYearView = false + animJob.join() + composeTraceEndSection() } else { - // 月 → 年:动画驱动 yearViewProgress 0f→1f,年视图同步缩小/淡入 + // 月 → 年:先启动动画(月视图开始缩小),等一帧后翻转 isYearView(年视图开始组合) + composeTraceBeginSection("MonthView→YearView") yearViewYear = selectedDate.year _yearViewAnimatable.snapTo(0f) - _yearViewAnimatable.animateTo( - 1f, tween(400, easing = FastOutSlowInEasing) - ) + val animJob = launch { + _yearViewAnimatable.animateTo( + 1f, tween(400, easing = FastOutSlowInEasing) + ) + } + withFrameNanos { } isYearView = true + animJob.join() + composeTraceEndSection() } } } 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 4f14a5f..11d6366 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -59,6 +59,8 @@ import kotlinx.datetime.number import kotlinx.datetime.plus import kotlinx.datetime.todayIn import plus.rua.project.CalendarViewModel +import plus.rua.project.composeTraceBeginSection +import plus.rua.project.composeTraceEndSection import kotlin.math.abs import kotlin.time.Clock @@ -206,33 +208,36 @@ fun CalendarMonthView( screenHeightPx = size.height } ) { - // 月视图层:始终存在于组合树中,通过 alpha 控制可见性/触摸,避免 isYearView - // 切换时触发整棵树销毁(Compose:onForgotten 600ms)。scale 动画保留在 graphicsLayer。 - val monthProgress = 1f - viewModel.yearViewProgress - val layoutReady = rowHeightPx > 0 - val monthAlpha = if (layoutReady) monthProgress.coerceIn(0f, 1f) else 0f - Box( - modifier = Modifier - .fillMaxSize() - .alpha(monthAlpha) - .graphicsLayer { - val scale = lerp(0.3f, 1f, monthProgress) - scaleX = scale - scaleY = scale - transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY) - } - ) { + // 月视图层:仅在非年视图时渲染,年视图激活时立即移除。 + if (!viewModel.isYearView) { + composeTraceBeginSection("MonthView:Compose") val dragRangeMinPx = with(density) { DRAG_RANGE_MIN_DP.dp.toPx() } val dragRangePx = if (effectiveRowHeightPx > 0) { maxOf((effectiveWeeks - 1) * effectiveRowHeightPx.toFloat(), dragRangeMinPx) } else { dragRangeMinPx } - Column( + + val monthProgress = 1f - viewModel.yearViewProgress + // 组合阶段计算:lambda 捕获快照值,避免 draw 阶段读到已更新的 rowHeightPx + // 但 layout 仍用旧值导致行堆叠 + val layoutReady = rowHeightPx > 0 + Box( modifier = Modifier .fillMaxSize() - .padding(horizontal = HORIZONTAL_PADDING_DP.dp) + .graphicsLayer { + val scale = lerp(0.3f, 1f, monthProgress) + scaleX = scale + scaleY = scale + alpha = if (layoutReady) monthProgress.coerceIn(0f, 1f) else 0f + transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY) + } ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = HORIZONTAL_PADDING_DP.dp) + ) { MonthHeader( year = currentYear, month = currentMonth, @@ -314,21 +319,25 @@ fun CalendarMonthView( ) } } - // 年视图层:始终存在于组合树中,通过 alpha 控制可见性/触摸。 - val yearProgress = viewModel.yearViewProgress - val yearAlpha = yearProgress.coerceIn(0f, 1f) - Column( - modifier = Modifier - .fillMaxSize() - .alpha(yearAlpha) - .graphicsLayer { - val scale = lerp(3.3f, 1f, yearProgress) - scaleX = scale - scaleY = scale - transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY) - } - .padding(horizontal = HORIZONTAL_PADDING_DP.dp) - ) { + composeTraceEndSection() + } + + // 年视图层:标题固定,HorizontalPager 只包裹网格。 + if (viewModel.isYearView) { + val yearProgress = viewModel.yearViewProgress + composeTraceBeginSection("YearView:Compose") + Column( + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + val scale = lerp(3.3f, 1f, yearProgress) + scaleX = scale + scaleY = scale + alpha = yearProgress.coerceIn(0f, 1f) + transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY) + } + .padding(horizontal = HORIZONTAL_PADDING_DP.dp) + ) { YearHeader( year = viewModel.yearViewYear, onYearChange = { newYear -> @@ -374,6 +383,9 @@ fun CalendarMonthView( ) } } + composeTraceEndSection() + } + // FAB 浮动按钮 FloatingActionButton( onClick = { isMenuExpanded = !isMenuExpanded }, diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt index d12570d..a8071a4 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/DayCell.kt @@ -38,6 +38,44 @@ import com.tyme.solar.SolarDay import kotlinx.datetime.LocalDate import plus.rua.project.ShiftKind +// P0-C: 静态缓存 SolarDay 计算结果,避免 Pager 滑动/切换时重复创建对象触发 GC +@Suppress("DEPRECATION") // monthNumber 无替代 API +private fun computeDayCellInfo(date: LocalDate): Triple { + val solarDay = SolarDay.fromYmd(date.year, date.monthNumber, date.day) + val holidayBadge = solarDay.getLegalHoliday()?.let { if (it.isWork()) "班" else "休" } + val lunarDay = solarDay.getLunarDay() + + // 农历传统节日(仅当天) + val lunarFestival = lunarDay.getFestival() + if (lunarFestival != null) { + return Triple(lunarFestival.getName(), true, holidayBadge) + } + + // 节气(当天才显示) + val termDay = solarDay.getTermDay() + if (termDay.getDayIndex() == 0) { + return Triple(termDay.getSolarTerm().getName(), true, holidayBadge) + } + + // 公历节日(仅当天) + val solarFestival = solarDay.getFestival() + if (solarFestival != null) { + return Triple(solarFestival.getName(), true, holidayBadge) + } + + // 默认:农历日期 + val name = lunarDay.getName() + val text = if (name == "初一") { + val lunarMonth = lunarDay.getLunarMonth() + "${lunarMonth.getName()}月" + } else { + name + } + return Triple(text, false, holidayBadge) +} + +private val dayCellInfoCache = mutableMapOf>() + enum class DayCellState { NORMAL, OTHER_MONTH, TODAY, SELECTED, SELECTED_TODAY } @@ -124,53 +162,16 @@ fun DayCell( val selectedOutlineColor = MaterialTheme.colorScheme.primary - data class DayAnnotation(val text: String, val isHighlight: Boolean) - - val holidayBadge = remember(date) { - @Suppress("DEPRECATION") // monthNumber 无替代 API - val solarDay = SolarDay.fromYmd(date.year, date.monthNumber, date.day) - solarDay.getLegalHoliday()?.let { if (it.isWork()) "班" else "休" } - } - - val annotation = remember(date) { - @Suppress("DEPRECATION") // monthNumber 无替代 API - val solarDay = SolarDay.fromYmd(date.year, date.monthNumber, date.day) - val lunarDay = solarDay.getLunarDay() - - // 农历传统节日(仅当天) - val lunarFestival = lunarDay.getFestival() - if (lunarFestival != null) { - return@remember DayAnnotation(lunarFestival.getName(), true) - } - - // 节气(当天才显示) - val termDay = solarDay.getTermDay() - if (termDay.getDayIndex() == 0) { - return@remember DayAnnotation(termDay.getSolarTerm().getName(), true) - } - - // 公历节日(仅当天) - val solarFestival = solarDay.getFestival() - if (solarFestival != null) { - return@remember DayAnnotation(solarFestival.getName(), true) - } - - // 默认:农历日期 - val name = lunarDay.getName() - val text = if (name == "初一") { - val lunarMonth = lunarDay.getLunarMonth() - "${lunarMonth.getName()}月" - } else { - name - } - DayAnnotation(text, false) + // P0-C: 使用静态缓存避免每次重组时重复创建 SolarDay 对象 + val (annotationText, isAnnotationHighlight, holidayBadge) = remember(date) { + dayCellInfoCache.getOrPut(date) { computeDayCellInfo(date) } } val lunarColor by transition.animateColor( transitionSpec = { tween(150, easing = FastOutSlowInEasing) }, label = "lunarColor" ) { state -> - if (annotation.isHighlight) { + if (isAnnotationHighlight) { when (state) { DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy( alpha = 0.85f @@ -250,7 +251,7 @@ fun DayCell( style = MaterialTheme.typography.bodyMedium ) Text( - text = annotation.text, + text = annotationText, textAlign = TextAlign.Center, color = lunarColor, fontSize = 7.sp,