perf: 优化日历翻页重组、农历批量查询与 Canvas 绘制内存分配

This commit is contained in:
xfy 2026-07-23 16:30:27 +08:00
parent 69bffd8d20
commit e16b7e6587
8 changed files with 101 additions and 62 deletions

View File

@ -147,9 +147,12 @@ class CalendarViewModel(
/** 聚合 UI 状态,减少 Compose 层分散订阅导致的重组。 */
val uiState: StateFlow<CalendarUiState> = combine(
combine(_selectedDate, _isCollapsed, _isYearView) { s, c, y -> Triple(s, c, y) },
combine(_yearViewYear, _collapseProgress, _showLegalHoliday) { y, p, h -> Triple(y, p, h) }
) { (selectedDate, isCollapsed, isYearView), (yearViewYear, collapseProgress, showLegalHoliday) ->
_selectedDate,
_isCollapsed,
_isYearView,
_yearViewYear,
combine(_collapseProgress, _showLegalHoliday) { cp, h -> cp to h }
) { selectedDate, isCollapsed, isYearView, yearViewYear, (collapseProgress, showLegalHoliday) ->
CalendarUiState(
selectedDate = selectedDate,
isCollapsed = isCollapsed,

View File

@ -31,6 +31,31 @@ class LunarCache(
trimIfNeeded()
computed
}
/**
* 批量获取指定日期的信息一次性加锁并返回 Map
*
* @param dates 日期列表
* @return 日期 DayCellInfo 的映射
*/
suspend fun getOrComputeBatch(dates: List<LocalDate>): Map<LocalDate, DayCellInfo> = mutex.withLock {
val result = HashMap<LocalDate, DayCellInfo>(dates.size)
var modified = false
for (date in dates) {
val cached = cache[date]
if (cached != null) {
result[date] = cached
} else {
val computed = compute(date)
cache[date] = computed
result[date] = computed
modified = true
}
}
if (modified) {
trimIfNeeded()
}
result
}
/**
* 批量预计算并填充缓存

View File

@ -147,15 +147,29 @@ object PhotoProcessor {
var result = rotate(source, rotationDegrees)
// 2. 裁剪
if (cropLeft != null && cropRight != null) {
result = crop(result, cropLeft, cropTop, cropRight, cropBottom)
val cropped = crop(result, cropLeft, cropTop, cropRight, cropBottom)
if (result != source && result != cropped) {
result.recycle()
}
result = cropped
}
// 3. 合成手写笔触
if (strokes.isNotEmpty()) {
result = drawStrokes(result, strokes, displayWidth, displayHeight)
val withStrokes = drawStrokes(result, strokes, displayWidth, displayHeight)
if (result != source && result != withStrokes) {
result.recycle()
}
result = withStrokes
}
// 4. 落盘JPEG 90% 质量)
FileOutputStream(destFile).use { out ->
result.compress(Bitmap.CompressFormat.JPEG, 90, out)
try {
FileOutputStream(destFile).use { out ->
result.compress(Bitmap.CompressFormat.JPEG, 90, out)
}
} finally {
if (result != source && !result.isRecycled) {
result.recycle()
}
}
return destFile.absolutePath
}

View File

@ -80,11 +80,7 @@ fun CalendarMonthPage(
key1 = year,
key2 = month
) {
val map = mutableMapOf<LocalDate, DayCellInfo>()
for (dayData in days) {
map[dayData.date] = LunarCache.default.getOrCompute(dayData.date)
}
value = map
value = LunarCache.default.getOrComputeBatch(days.map { it.date })
}
val holidayEdges = remember(lunarDataMap, year, month) {

View File

@ -14,7 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import plus.rua.project.composeTraceBeginSection
import plus.rua.project.composeTraceEndSection
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.graphicsLayer
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDate
@ -69,12 +69,6 @@ fun CalendarPager(
}
}
val currentPageOffsetFraction by remember {
derivedStateOf { pagerState.currentPageOffsetFraction }
}
val currentPage by remember {
derivedStateOf { pagerState.currentPage }
}
HorizontalPager(
state = pagerState,
@ -82,13 +76,6 @@ fun CalendarPager(
flingBehavior = PagerDefaults.flingBehavior(state = pagerState),
modifier = modifier.testTag("calendar_pager")
) { page ->
val pageOffset = abs(currentPageOffsetFraction)
val isCurrentPage = page == currentPage
val alpha = if (isCurrentPage) {
1f - pageOffset
} else {
pageOffset
}
val (year, month) = pageToYearMonth(page, initialYear, initialMonth)
composeTraceBeginSection("CalendarPager:Page:$year-$month")
CalendarMonthPage(
@ -117,7 +104,11 @@ fun CalendarPager(
shiftKindAt = shiftKindAt,
showLegalHoliday = showLegalHoliday,
onRowHeightMeasured = onRowHeightMeasured,
modifier = Modifier.alpha(alpha)
modifier = Modifier.graphicsLayer {
val pageOffset = abs(pagerState.currentPageOffsetFraction)
val isCurrentPage = page == pagerState.currentPage
alpha = if (isCurrentPage) 1f - pageOffset else pageOffset
}
)
composeTraceEndSection()
}

View File

@ -79,7 +79,13 @@ fun getMonthGridInfo(year: Int, month: Int): MonthGridInfo {
* @return 网格行数
*/
fun calculateWeeksCount(year: Int, month: Int): Int {
return getMonthGridInfo(year, month).rows
val offset = LocalDate(year, Month(month), 1).dayOfWeek.ordinal
val daysInMonth = when (month) {
2 -> if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) 29 else 28
4, 6, 9, 11 -> 30
else -> 31
}
return ((offset + daysInMonth - 1) / 7) + 1
}
/**

View File

@ -15,7 +15,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.drop
import kotlinx.datetime.DatePeriod
@ -79,17 +79,14 @@ fun WeekPager(
modifier = modifier
) { page ->
composeTraceBeginSection("WeekPager:Page")
val pageOffset = abs(pagerState.currentPageOffsetFraction)
val isCurrentPage = page == pagerState.currentPage
val alpha = if (isCurrentPage) {
1f - pageOffset
} else {
pageOffset
}
val weekMonday = pageToWeekMonday(page, initialWeekMonday)
Row(
modifier = Modifier
.alpha(alpha)
.graphicsLayer {
val pageOffset = abs(pagerState.currentPageOffsetFraction)
val isCurrentPage = page == pagerState.currentPage
alpha = if (isCurrentPage) 1f - pageOffset else pageOffset
}
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.padding(vertical = ROW_PADDING_DP.dp)

View File

@ -106,30 +106,32 @@ fun YearGridView(
val dayTextStyle = remember { TextStyle(fontSize = 8.sp, lineHeight = 12.sp) }
// 延迟文本测量到下一帧,避免首帧阻塞
val dayLayouts by produceState<Map<Pair<Int, Color>, androidx.compose.ui.text.TextLayoutResult>?>(null, textMeasurer, dayTextStyle, colors) {
val dayLayouts by produceState<Map<Int, androidx.compose.ui.text.TextLayoutResult>?>(null, textMeasurer, dayTextStyle, colors) {
val days = 1..31
val colorList = listOf(colors.day, colors.todayText, colors.otherMonth)
value = days.flatMap { d ->
colorList.map { c ->
(d to c) to textMeasurer.measure(d.toString(), dayTextStyle.copy(color = c))
val colorTypes = listOf(0 to colors.day, 1 to colors.todayText, 2 to colors.otherMonth)
val map = HashMap<Int, androidx.compose.ui.text.TextLayoutResult>(32 * 3)
days.forEach { d ->
colorTypes.forEach { (type, c) ->
map[d * 3 + type] = textMeasurer.measure(d.toString(), dayTextStyle.copy(color = c))
}
}.toMap()
}
value = map
}
val titleLayouts by produceState<Map<Pair<Int, Boolean>, androidx.compose.ui.text.TextLayoutResult>?>(null, textMeasurer, colors) {
value = (1..12).flatMap { month ->
val titleLayouts by produceState<Map<Int, androidx.compose.ui.text.TextLayoutResult>?>(null, textMeasurer, colors) {
val map = HashMap<Int, androidx.compose.ui.text.TextLayoutResult>(13 * 2)
(1..12).forEach { month ->
val text = "${month}"
listOf(
(month to true) to textMeasurer.measure(
text,
TextStyle(fontSize = 10.sp, color = colors.titleSelected, fontWeight = FontWeight.Bold)
),
(month to false) to textMeasurer.measure(
text,
TextStyle(fontSize = 10.sp, color = colors.titleNormal)
)
map[month * 2 + 1] = textMeasurer.measure(
text,
TextStyle(fontSize = 10.sp, color = colors.titleSelected, fontWeight = FontWeight.Bold)
)
}.toMap()
map[month * 2] = textMeasurer.measure(
text,
TextStyle(fontSize = 10.sp, color = colors.titleNormal)
)
}
value = map
}
val weekdayLayouts by produceState<Map<String, androidx.compose.ui.text.TextLayoutResult>?>(null, textMeasurer, colors) {
@ -193,8 +195,8 @@ private fun MiniMonth(
today: LocalDate,
days: List<MiniDayData>,
colors: MiniMonthColors,
dayLayouts: Map<Pair<Int, Color>, androidx.compose.ui.text.TextLayoutResult>?,
titleLayouts: Map<Pair<Int, Boolean>, androidx.compose.ui.text.TextLayoutResult>?,
dayLayouts: Map<Int, androidx.compose.ui.text.TextLayoutResult>?,
titleLayouts: Map<Int, androidx.compose.ui.text.TextLayoutResult>?,
weekdayLayouts: Map<String, androidx.compose.ui.text.TextLayoutResult>?,
onClick: () -> Unit,
modifier: Modifier = Modifier
@ -226,7 +228,7 @@ private fun MiniMonth(
val cellWidth = size.width / 7f
// 1. 绘制标题
val titleLayout = tl[month to isSelected]!!
val titleLayout = tl[month * 2 + (if (isSelected) 1 else 0)]!!
drawText(
textLayoutResult = titleLayout,
topLeft = Offset(
@ -259,9 +261,14 @@ private fun MiniMonth(
val isToday = dayData.date == today && dayData.isCurrentMonth
val dayNum = if (dayData.isCurrentMonth) dayData.date.day else 0
val textColor: Color = when {
!dayData.isCurrentMonth -> colors.otherMonth
isToday -> colors.todayText
val colorType = when {
!dayData.isCurrentMonth -> 2
isToday -> 1
else -> 0
}
val textColor: Color = when (colorType) {
2 -> colors.otherMonth
1 -> colors.todayText
else -> colors.day
}
@ -275,7 +282,7 @@ private fun MiniMonth(
}
if (dayNum > 0) {
dl[dayNum to textColor]?.let { layoutResult ->
dl[dayNum * 3 + colorType]?.let { layoutResult ->
drawText(
textLayoutResult = layoutResult,
topLeft = Offset(