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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -51,6 +52,37 @@ data class CalendarUiState(
val showLegalHoliday: Boolean 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 周号计算逻辑 * 日历状态管理持有选中日期折叠状态和 ISO 周号计算逻辑
* *
@ -137,17 +169,20 @@ class CalendarViewModel(
/** 聚合 UI 状态,减少 Compose 层分散订阅导致的重组。 */ /** 聚合 UI 状态,减少 Compose 层分散订阅导致的重组。 */
val uiState: StateFlow<CalendarUiState> = combine( val uiState: StateFlow<CalendarUiState> = combine(
combine(_selectedDate, _isCollapsed, ::Pair), _selectedDate,
combine(_isYearView, _yearViewYear, ::Pair), _isCollapsed,
combine(_collapseProgress, _showLegalHoliday, ::Pair) _isYearView,
) { dateCollapsed, yearViewYearPair, progressHoliday -> _yearViewYear,
_collapseProgress,
_showLegalHoliday
) { selectedDate, isCollapsed, isYearView, yearViewYear, collapseProgress, showLegalHoliday ->
CalendarUiState( CalendarUiState(
selectedDate = dateCollapsed.first, selectedDate = selectedDate,
isCollapsed = dateCollapsed.second, isCollapsed = isCollapsed,
isYearView = yearViewYearPair.first, isYearView = isYearView,
yearViewYear = yearViewYearPair.second, yearViewYear = yearViewYear,
collapseProgress = progressHoliday.first, collapseProgress = collapseProgress,
showLegalHoliday = progressHoliday.second showLegalHoliday = showLegalHoliday
) )
}.stateIn( }.stateIn(
viewModelScope, viewModelScope,
@ -245,7 +280,7 @@ class CalendarViewModel(
} }
/** /**
* 折叠状态下拉恢复delta 为负值向下拖推动 progress 0 * 折叠状态下拉恢复delta 为负值向下拖推动 progress 0
* *
* @param delta 拖拽增量已归一化到 [0,1] 区间 * @param delta 拖拽增量已归一化到 [0,1] 区间
*/ */

View File

@ -72,7 +72,8 @@ fun CalendarMonthPage(
val holidayBadges by produceState( val holidayBadges by produceState(
initialValue = emptyMap<LocalDate, String?>(), initialValue = emptyMap<LocalDate, String?>(),
key1 = days key1 = year,
key2 = month
) { ) {
val map = mutableMapOf<LocalDate, String?>() val map = mutableMapOf<LocalDate, String?>()
for (dayData in days) { for (dayData in days) {
@ -82,7 +83,7 @@ fun CalendarMonthPage(
value = map value = map
} }
val holidayEdges = remember(holidayBadges, days) { val holidayEdges = remember(holidayBadges, year, month) {
val map = mutableMapOf<LocalDate, HolidayEdgeInfo>() val map = mutableMapOf<LocalDate, HolidayEdgeInfo>()
for (dayData in days) { for (dayData in days) {
val date = dayData.date val date = dayData.date
@ -99,7 +100,7 @@ fun CalendarMonthPage(
} }
val weeks = remember(days) { days.chunked(7) } 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 } } weeks.indexOfFirst { week -> week.any { it.date == selectedDate } }
} }
val totalHeightDp = if (rowHeightPx > 0) { 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.foundation.pager.PagerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow 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( HorizontalPager(
state = pagerState, state = pagerState,
beyondViewportPageCount = 0, beyondViewportPageCount = 1,
flingBehavior = PagerDefaults.flingBehavior(state = pagerState), flingBehavior = PagerDefaults.flingBehavior(state = pagerState),
modifier = modifier modifier = modifier
) { page -> ) { page ->
val pageOffset = abs(pagerState.currentPageOffsetFraction) val pageOffset = abs(currentPageOffsetFraction)
val isCurrentPage = page == pagerState.currentPage val isCurrentPage = page == currentPage
val alpha = if (isCurrentPage) { val alpha = if (isCurrentPage) {
1f - pageOffset 1f - pageOffset
} else { } else {

View File

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