DayCell SolarDay 静态缓存:避免 Pager 切换时重复创建对象触发 GC
每个 DayCell 创建时调用两次 SolarDay.fromYmd() 计算节日/农历信息。 Pager 缓存页的大量 DayCell 同时重建时产生大量临时对象,加剧 GC 压力。 修复:在 DayCell.kt 中增加进程级静态缓存 dayCellInfoCache,按日期缓存 computeDayCellInfo() 的结果。首次计算后永久复用,消除重复对象创建。
This commit is contained in:
parent
fab0a5eba8
commit
ed1935c9fb
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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<String, Boolean, String?> {
|
||||
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<LocalDate, Triple<String, Boolean, String?>>()
|
||||
|
||||
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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user