perf: 优化 Compose 渲染性能

- DayCell: 5 个 animate*AsState 合并为 updateTransition,减少 80% 动画状态对象
- CalendarMonthPage: produceState/remember key 从 List/Map 改为 year/month 原始值
- CalendarPager: derivedStateOf 提取 currentPageOffsetFraction,beyondViewportPageCount 设为 1
- CalendarViewModel: 三层嵌套 combine 扁平化为 6 参数 combine

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-25 15:36:18 +08:00
parent e934d33cfa
commit 28f777abe4
4 changed files with 104 additions and 51 deletions

View File

@ -3,6 +3,7 @@ package plus.rua.project
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -51,6 +52,37 @@ data class CalendarUiState(
val showLegalHoliday: Boolean
)
/**
* 将六个 [Flow] 合并为一个 [Flow]使用 [transform] 处理最新值
*
* kotlinx-coroutines 1.11 仅内置到 5 参数的 [combine] 重载
* 此扩展用于扁平化 6 StateFlow 的合并避免多层嵌套产生的中间流
*/
private inline fun <T1, T2, T3, T4, T5, T6, R> combine(
flow: Flow<T1>,
flow2: Flow<T2>,
flow3: Flow<T3>,
flow4: Flow<T4>,
flow5: Flow<T5>,
flow6: Flow<T6>,
crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R
): Flow<R> = combine(
combine(flow, flow2, flow3, flow4, flow5) { t1, t2, t3, t4, t5 ->
Quintuple(t1, t2, t3, t4, t5)
},
flow6
) { quintuple, t6 ->
transform(quintuple.first, quintuple.second, quintuple.third, quintuple.fourth, quintuple.fifth, t6)
}
private data class Quintuple<T1, T2, T3, T4, T5>(
val first: T1,
val second: T2,
val third: T3,
val fourth: T4,
val fifth: T5
)
/**
* 日历状态管理持有选中日期折叠状态和 ISO 周号计算逻辑
*
@ -137,17 +169,20 @@ class CalendarViewModel(
/** 聚合 UI 状态,减少 Compose 层分散订阅导致的重组。 */
val uiState: StateFlow<CalendarUiState> = combine(
combine(_selectedDate, _isCollapsed, ::Pair),
combine(_isYearView, _yearViewYear, ::Pair),
combine(_collapseProgress, _showLegalHoliday, ::Pair)
) { dateCollapsed, yearViewYearPair, progressHoliday ->
_selectedDate,
_isCollapsed,
_isYearView,
_yearViewYear,
_collapseProgress,
_showLegalHoliday
) { selectedDate, isCollapsed, isYearView, yearViewYear, collapseProgress, showLegalHoliday ->
CalendarUiState(
selectedDate = dateCollapsed.first,
isCollapsed = dateCollapsed.second,
isYearView = yearViewYearPair.first,
yearViewYear = yearViewYearPair.second,
collapseProgress = progressHoliday.first,
showLegalHoliday = progressHoliday.second
selectedDate = selectedDate,
isCollapsed = isCollapsed,
isYearView = isYearView,
yearViewYear = yearViewYear,
collapseProgress = collapseProgress,
showLegalHoliday = showLegalHoliday
)
}.stateIn(
viewModelScope,
@ -245,7 +280,7 @@ class CalendarViewModel(
}
/**
* 折叠状态下拉恢复delta 为负值向下拖推动 progress 0
* 折叠状态下拉恢复delta 为负值向下拖推动 progress 0
*
* @param delta 拖拽增量已归一化到 [0,1] 区间
*/

View File

@ -72,7 +72,8 @@ fun CalendarMonthPage(
val holidayBadges by produceState(
initialValue = emptyMap<LocalDate, String?>(),
key1 = days
key1 = year,
key2 = month
) {
val map = mutableMapOf<LocalDate, String?>()
for (dayData in days) {
@ -82,7 +83,7 @@ fun CalendarMonthPage(
value = map
}
val holidayEdges = remember(holidayBadges, days) {
val holidayEdges = remember(holidayBadges, year, month) {
val map = mutableMapOf<LocalDate, HolidayEdgeInfo>()
for (dayData in days) {
val date = dayData.date
@ -99,7 +100,7 @@ fun CalendarMonthPage(
}
val weeks = remember(days) { days.chunked(7) }
val anchorIndex = remember(weeks, selectedDate) {
val anchorIndex = remember(year, month, selectedDate) {
weeks.indexOfFirst { week -> week.any { it.date == selectedDate } }
}
val totalHeightDp = if (rowHeightPx > 0) {

View File

@ -5,6 +5,8 @@ import androidx.compose.foundation.pager.PagerDefaults
import androidx.compose.foundation.pager.PagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
@ -66,14 +68,21 @@ fun CalendarPager(
}
}
val currentPageOffsetFraction by remember {
derivedStateOf { pagerState.currentPageOffsetFraction }
}
val currentPage by remember {
derivedStateOf { pagerState.currentPage }
}
HorizontalPager(
state = pagerState,
beyondViewportPageCount = 0,
beyondViewportPageCount = 1,
flingBehavior = PagerDefaults.flingBehavior(state = pagerState),
modifier = modifier
) { page ->
val pageOffset = abs(pagerState.currentPageOffsetFraction)
val isCurrentPage = page == pagerState.currentPage
val pageOffset = abs(currentPageOffsetFraction)
val isCurrentPage = page == currentPage
val alpha = if (isCurrentPage) {
1f - pageOffset
} else {

View File

@ -1,9 +1,10 @@
package plus.rua.project.ui
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@ -91,52 +92,61 @@ fun DayCell(
else -> DayCellState.NORMAL
}
val revealProgress by animateFloatAsState(
targetValue = when (currentState) {
val transition = updateTransition(targetState = currentState, label = "DayCell")
val revealProgress by transition.animateFloat(
transitionSpec = { tween(150, easing = FastOutSlowInEasing) },
label = "revealProgress"
) { state ->
when (state) {
DayCellState.SELECTED, DayCellState.SELECTED_TODAY -> 1f
else -> 0f
},
animationSpec = tween(150, easing = FastOutSlowInEasing),
label = "revealProgress"
)
}
}
val contentColor by animateColorAsState(
targetValue = when (currentState) {
val contentColor by transition.animateColor(
transitionSpec = { tween(150, easing = FastOutSlowInEasing) },
label = "contentColor"
) { state ->
when (state) {
DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer
DayCellState.SELECTED -> MaterialTheme.colorScheme.primary
DayCellState.TODAY -> MaterialTheme.colorScheme.primary
DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
DayCellState.NORMAL -> MaterialTheme.colorScheme.onSurface
},
animationSpec = tween(150, easing = FastOutSlowInEasing),
label = "contentColor"
)
}
}
// 选中今天:实心填充 primaryContainer;其他状态不填充。
val selectedFillColor by animateColorAsState(
targetValue = when (currentState) {
val selectedFillColor by transition.animateColor(
transitionSpec = { tween(150, easing = FastOutSlowInEasing) },
label = "selectedFillColor"
) { state ->
when (state) {
DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.primaryContainer
else -> Color.Transparent
},
animationSpec = tween(150, easing = FastOutSlowInEasing),
label = "selectedFillColor"
)
}
}
// 选中非今天:绘制描边圆,避免遮挡右上角角标。
val selectedOutlineAlpha by animateFloatAsState(
targetValue = when (currentState) {
val selectedOutlineAlpha by transition.animateFloat(
transitionSpec = { tween(150, easing = FastOutSlowInEasing) },
label = "selectedOutlineAlpha"
) { state ->
when (state) {
DayCellState.SELECTED -> 1f
else -> 0f
},
animationSpec = tween(150, easing = FastOutSlowInEasing),
label = "selectedOutlineAlpha"
)
}
}
val selectedOutlineColor = MaterialTheme.colorScheme.primary
val lunarColor by animateColorAsState(
targetValue = if (isAnnotationHighlight) {
when (currentState) {
val lunarColor by transition.animateColor(
transitionSpec = { tween(150, easing = FastOutSlowInEasing) },
label = "lunarColor"
) { state ->
if (isAnnotationHighlight) {
when (state) {
DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy(
alpha = 0.85f
)
@ -146,7 +156,7 @@ fun DayCell(
DayCellState.NORMAL -> MaterialTheme.colorScheme.error.copy(alpha = 0.7f)
}
} else {
when (currentState) {
when (state) {
DayCellState.SELECTED_TODAY -> MaterialTheme.colorScheme.onPrimaryContainer.copy(
alpha = 0.7f
)
@ -155,10 +165,8 @@ fun DayCell(
DayCellState.OTHER_MONTH -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.26f)
DayCellState.NORMAL -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
}
},
animationSpec = tween(150, easing = FastOutSlowInEasing),
label = "lunarColor"
)
}
}
val holidayBgColor = when (holidayBadge) {
"" -> MaterialTheme.colorScheme.error.copy(alpha = 0.10f)