避免月/年视图切换时整棵树销毁:共存 + Modifier.alpha 控制

Perfetto trace 显示 Compose:onForgotten 耗时 600ms,根因是
if(!isYearView)/if(isYearView) 条件渲染导致整棵子树在切换时被销毁重建。

修复:
1. 月视图和年视图始终共存于组合树中
2. 通过 Modifier.alpha() 控制可见性和触摸事件分发
3. graphicsLayer 仅保留 scale 动画,alpha 移出到 Modifier 层
4. 简化 toggleYearView():移除 withFrameNanos/animJob.join() 的复杂协程逻辑

两个视图通过 yearViewProgress 驱动的交叉淡入淡出同步切换,
消除 onForgotten 的组件销毁开销。
This commit is contained in:
meyou 2026-05-18 23:05:01 +08:00
parent 9fd877485f
commit fab0a5eba8
No known key found for this signature in database
2 changed files with 43 additions and 69 deletions

View File

@ -108,13 +108,12 @@ class CalendarViewModel(
/** /**
* 切换年视图仅在展开态可用 * 切换年视图仅在展开态可用
* *
* 切换瞬间立即翻转 isYearView让对应方向的目标视图立刻接管渲染 * /年视图始终共存于组合树中 alpha 控制可见性
* 当前视图被直接移除动画只作用在目标视图的 scale/alpha * 翻转 isYearView 后启动 Animatable 动画驱动对应方向视图的 scale/alpha 变化
*/ */
fun toggleYearView() { fun toggleYearView() {
yearViewJob?.cancel() yearViewJob?.cancel()
yearViewJob = coroutineScope.launch { yearViewJob = coroutineScope.launch {
// 折叠态先展开回月视图,再切换年视图
if (isCollapsed) { if (isCollapsed) {
_collapseAnimatable.animateTo( _collapseAnimatable.animateTo(
0f, spring(dampingRatio = 0.8f, stiffness = 400f) 0f, spring(dampingRatio = 0.8f, stiffness = 400f)
@ -122,32 +121,19 @@ class CalendarViewModel(
isCollapsed = false isCollapsed = false
} }
if (isYearView) { if (isYearView) {
// 年 → 月:先启动动画(年视图开始淡出),等一帧后翻转 isYearView月视图开始组合 // 年 → 月:动画驱动 yearViewProgress 1f→0f月视图同步放大/淡入
composeTraceBeginSection("YearView→MonthView") _yearViewAnimatable.animateTo(
_yearViewAnimatable.snapTo(1f) 0f, tween(400, easing = FastOutSlowInEasing)
val animJob = launch { )
_yearViewAnimatable.animateTo(
0f, tween(400, easing = FastOutSlowInEasing)
)
}
withFrameNanos { }
isYearView = false isYearView = false
animJob.join()
composeTraceEndSection()
} else { } else {
// 月 → 年:先启动动画(月视图开始缩小),等一帧后翻转 isYearView年视图开始组合 // 月 → 年:动画驱动 yearViewProgress 0f→1f年视图同步缩小/淡入
composeTraceBeginSection("MonthView→YearView")
yearViewYear = selectedDate.year yearViewYear = selectedDate.year
_yearViewAnimatable.snapTo(0f) _yearViewAnimatable.snapTo(0f)
val animJob = launch { _yearViewAnimatable.animateTo(
_yearViewAnimatable.animateTo( 1f, tween(400, easing = FastOutSlowInEasing)
1f, tween(400, easing = FastOutSlowInEasing) )
)
}
withFrameNanos { }
isYearView = true isYearView = true
animJob.join()
composeTraceEndSection()
} }
} }
} }

View File

@ -59,8 +59,6 @@ import kotlinx.datetime.number
import kotlinx.datetime.plus import kotlinx.datetime.plus
import kotlinx.datetime.todayIn import kotlinx.datetime.todayIn
import plus.rua.project.CalendarViewModel import plus.rua.project.CalendarViewModel
import plus.rua.project.composeTraceBeginSection
import plus.rua.project.composeTraceEndSection
import kotlin.math.abs import kotlin.math.abs
import kotlin.time.Clock import kotlin.time.Clock
@ -208,36 +206,33 @@ fun CalendarMonthView(
screenHeightPx = size.height screenHeightPx = size.height
} }
) { ) {
// 月视图层:仅在非年视图时渲染,年视图激活时立即移除。 // 月视图层:始终存在于组合树中,通过 alpha 控制可见性/触摸,避免 isYearView
if (!viewModel.isYearView) { // 切换时触发整棵树销毁Compose:onForgotten 600ms。scale 动画保留在 graphicsLayer。
composeTraceBeginSection("MonthView:Compose") 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)
}
) {
val dragRangeMinPx = with(density) { DRAG_RANGE_MIN_DP.dp.toPx() } val dragRangeMinPx = with(density) { DRAG_RANGE_MIN_DP.dp.toPx() }
val dragRangePx = if (effectiveRowHeightPx > 0) { val dragRangePx = if (effectiveRowHeightPx > 0) {
maxOf((effectiveWeeks - 1) * effectiveRowHeightPx.toFloat(), dragRangeMinPx) maxOf((effectiveWeeks - 1) * effectiveRowHeightPx.toFloat(), dragRangeMinPx)
} else { } else {
dragRangeMinPx dragRangeMinPx
} }
Column(
val monthProgress = 1f - viewModel.yearViewProgress
// 组合阶段计算lambda 捕获快照值,避免 draw 阶段读到已更新的 rowHeightPx
// 但 layout 仍用旧值导致行堆叠
val layoutReady = rowHeightPx > 0
Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.graphicsLayer { .padding(horizontal = HORIZONTAL_PADDING_DP.dp)
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( MonthHeader(
year = currentYear, year = currentYear,
month = currentMonth, month = currentMonth,
@ -319,25 +314,21 @@ fun CalendarMonthView(
) )
} }
} }
composeTraceEndSection() // 年视图层:始终存在于组合树中,通过 alpha 控制可见性/触摸。
} val yearProgress = viewModel.yearViewProgress
val yearAlpha = yearProgress.coerceIn(0f, 1f)
// 年视图层标题固定HorizontalPager 只包裹网格。 Column(
if (viewModel.isYearView) { modifier = Modifier
val yearProgress = viewModel.yearViewProgress .fillMaxSize()
composeTraceBeginSection("YearView:Compose") .alpha(yearAlpha)
Column( .graphicsLayer {
modifier = Modifier val scale = lerp(3.3f, 1f, yearProgress)
.fillMaxSize() scaleX = scale
.graphicsLayer { scaleY = scale
val scale = lerp(3.3f, 1f, yearProgress) transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY)
scaleX = scale }
scaleY = scale .padding(horizontal = HORIZONTAL_PADDING_DP.dp)
alpha = yearProgress.coerceIn(0f, 1f) ) {
transformOrigin = TransformOrigin(anchorPivotX, anchorPivotY)
}
.padding(horizontal = HORIZONTAL_PADDING_DP.dp)
) {
YearHeader( YearHeader(
year = viewModel.yearViewYear, year = viewModel.yearViewYear,
onYearChange = { newYear -> onYearChange = { newYear ->
@ -383,9 +374,6 @@ fun CalendarMonthView(
) )
} }
} }
composeTraceEndSection()
}
// FAB 浮动按钮 // FAB 浮动按钮
FloatingActionButton( FloatingActionButton(
onClick = { isMenuExpanded = !isMenuExpanded }, onClick = { isMenuExpanded = !isMenuExpanded },