优化月→年视图切换性能(第二轮):延迟预组合 + 缓存提升

- 年视图 beyondViewportPageCount 动画完成后再恢复为 1,消除切换后 283ms onMeasure 阻塞
- MiniMonth 主题色提取到 YearGridView 级别 remember 缓存(72 次 → 1 次读取)
- TextMeasurer 从每 MiniMonth 独立实例改为 YearGridView 共享(12 个 → 1 个)
- 预测量 1-31 × 3 颜色的 TextLayoutResult(93 个缓存),消除 360+ 次 measure() 调用
- 12 个月的 generateMiniMonthDays 在 YearGridView 级别预计算并缓存
This commit is contained in:
xfy 2026-05-18 17:05:43 +08:00
parent 914e882fe1
commit a4bd56a8a9
2 changed files with 78 additions and 39 deletions

View File

@ -52,6 +52,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.datetime.DatePeriod import kotlinx.datetime.DatePeriod
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
@ -100,10 +101,11 @@ fun CalendarMonthView(
isMenuExpanded = false isMenuExpanded = false
} }
// 年视图首帧后恢复预组合,避免首帧同时组合 3 页 × 12 月 = 36 个 MiniMonth // 年视图动画完成后再恢复预组合,避免动画期间触发邻页组合阻塞帧
LaunchedEffect(viewModel.isYearView) { LaunchedEffect(viewModel.isYearView) {
if (viewModel.isYearView) { if (viewModel.isYearView) {
withFrameNanos { } snapshotFlow { viewModel.yearViewProgress }
.first { it >= 1f }
yearPagerBeyondViewport = 1 yearPagerBeyondViewport = 1
} else { } else {
yearPagerBeyondViewport = 0 yearPagerBeyondViewport = 0

View File

@ -25,12 +25,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.datetime.DatePeriod import kotlinx.datetime.DatePeriod
@ -43,6 +44,16 @@ import plus.rua.project.composeTraceEndSection
private val WEEKDAY_LABELS = listOf("", "", "", "", "", "", "") private val WEEKDAY_LABELS = listOf("", "", "", "", "", "", "")
private data class MiniMonthColors(
val titleSelected: Color,
val titleNormal: Color,
val weekday: Color,
val day: Color,
val otherMonth: Color,
val todayBg: Color,
val todayText: Color
)
/** /**
* 年视图 4×3 月历网格 * 年视图 4×3 月历网格
* *
@ -61,6 +72,41 @@ fun YearGridView(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
composeTraceBeginSection("YearGridView:$year") composeTraceBeginSection("YearGridView:$year")
// P0-F: 主题色在 YearGridView 级别一次性读取并缓存
val colorScheme = MaterialTheme.colorScheme
val colors = remember(colorScheme) {
MiniMonthColors(
titleSelected = colorScheme.primary,
titleNormal = colorScheme.onSurface,
weekday = colorScheme.onSurface.copy(alpha = 0.4f),
day = colorScheme.onSurface.copy(alpha = 0.6f),
otherMonth = colorScheme.onSurface.copy(alpha = 0.2f),
todayBg = colorScheme.primaryContainer,
todayText = colorScheme.onPrimaryContainer
)
}
// P0-F: 预计算全年 12 个月的日期数据,翻年时复用
val monthDays = remember(year) {
(1..12).map { generateMiniMonthDays(year, it) }
}
// P0-G: 共享 TextMeasurer
val textMeasurer = rememberTextMeasurer()
val dayTextStyle = remember { TextStyle(fontSize = 8.sp, lineHeight = 12.sp) }
// P0-D: 预测量 1..31 × 3 种颜色 = 93 个 TextLayoutResult
val dayLayouts = remember(textMeasurer, dayTextStyle, colors) {
val days = 1..31
val colorList = listOf(colors.day, colors.todayText, colors.otherMonth)
days.flatMap { d ->
colorList.map { c ->
(d to c) to textMeasurer.measure(d.toString(), dayTextStyle.copy(color = c))
}
}.toMap()
}
Column( Column(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
@ -81,10 +127,14 @@ fun YearGridView(
(0 until 3).forEach { col -> (0 until 3).forEach { col ->
val month = row * 3 + col + 1 val month = row * 3 + col + 1
MiniMonth( MiniMonth(
year = year,
month = month, month = month,
isSelected = month == selectedMonth, isSelected = month == selectedMonth,
today = today, today = today,
days = monthDays[month - 1],
colors = colors,
textMeasurer = textMeasurer,
dayTextStyle = dayTextStyle,
dayLayouts = dayLayouts,
onClick = { onMonthClick(month) }, onClick = { onMonthClick(month) },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
@ -101,29 +151,18 @@ fun YearGridView(
*/ */
@Composable @Composable
private fun MiniMonth( private fun MiniMonth(
year: Int,
month: Int, month: Int,
isSelected: Boolean, isSelected: Boolean,
today: LocalDate, today: LocalDate,
days: List<MiniDayData>,
colors: MiniMonthColors,
textMeasurer: TextMeasurer,
dayTextStyle: TextStyle,
dayLayouts: Map<Pair<Int, Color>, androidx.compose.ui.text.TextLayoutResult>,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val days = remember(year, month) { generateMiniMonthDays(year, month) } val titleColor = if (isSelected) colors.titleSelected else colors.titleNormal
val titleColor = if (isSelected) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurface
}
val weekdayColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
val dayColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
val otherMonthColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f)
val todayBgColor = MaterialTheme.colorScheme.primaryContainer
val todayTextColor = MaterialTheme.colorScheme.onPrimaryContainer
val textMeasurer = rememberTextMeasurer()
val dayTextStyle = remember {
TextStyle(fontSize = 8.sp, lineHeight = 12.sp)
}
Column( Column(
modifier = modifier modifier = modifier
@ -148,7 +187,7 @@ private fun MiniMonth(
WEEKDAY_LABELS.forEach { label -> WEEKDAY_LABELS.forEach { label ->
Text( Text(
text = label, text = label,
color = weekdayColor, color = colors.weekday,
fontSize = 8.sp, fontSize = 8.sp,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
@ -170,38 +209,36 @@ private fun MiniMonth(
val centerY = row * rowHeightPx + rowHeightPx / 2f val centerY = row * rowHeightPx + rowHeightPx / 2f
val isToday = dayData.date == today && dayData.isCurrentMonth val isToday = dayData.date == today && dayData.isCurrentMonth
val text = if (dayData.isCurrentMonth) dayData.date.day.toString() else "" val dayNum = if (dayData.isCurrentMonth) dayData.date.day else 0
val textColor: Color = when { val textColor: Color = when {
!dayData.isCurrentMonth -> otherMonthColor !dayData.isCurrentMonth -> colors.otherMonth
isToday -> todayTextColor isToday -> colors.todayText
else -> dayColor else -> colors.day
} }
if (isToday) { if (isToday) {
val radius = cellWidth.coerceAtMost(rowHeightPx) / 2f * 0.8f val radius = cellWidth.coerceAtMost(rowHeightPx) / 2f * 0.8f
drawCircle( drawCircle(
color = todayBgColor, color = colors.todayBg,
radius = radius, radius = radius,
center = Offset(centerX, centerY) center = Offset(centerX, centerY)
) )
} }
if (text.isNotEmpty()) { if (dayNum > 0) {
val measured = textMeasurer.measure( dayLayouts[dayNum to textColor]?.let { layoutResult ->
text = text,
style = dayTextStyle.copy(color = textColor)
)
drawText( drawText(
textLayoutResult = measured, textLayoutResult = layoutResult,
topLeft = Offset( topLeft = Offset(
x = centerX - measured.size.width / 2f, x = centerX - layoutResult.size.width / 2f,
y = centerY - measured.size.height / 2f y = centerY - layoutResult.size.height / 2f
) )
) )
} }
} }
} }
} }
}
} }
private data class MiniDayData( private data class MiniDayData(