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:
parent
e934d33cfa
commit
28f777abe4
@ -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] 区间
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user