perf: 引入 logd 条件日志工具,增强动画调试并优化 sharedElement 缓存

- 新增 AnimLog.kt,提供 BuildConfig.DEBUG 条件控制的 logd 日志工具,
  支持 lambda 延迟求值以避免 release 模式下的字符串拼接开销
- 全模块替换 android.util.Log.d 为 logd,并将日志重构为 lambda 形式
- CalendarViewModel: toggleYearView / selectMonthFromYearView 添加纳秒级
  耗时追踪日志
- CalendarMonthView / CalendarMonthPage / CalendarPager / YearGridView:
  增加重组/进入/离开/页面切换/动画状态变化的详细调试日志
- 折叠动画/滑动进度日志添加状态去重,避免频繁重复输出
- P0: 缓存 sharedElement tween 实例(CalendarMonthView + YearGridView),
  避免每次重组创建新实例导致动画重新计算
- P0: YearPager pageYear 使用 remember 稳定计算,避免 settledPage 与
  yearViewYear 不同步导致抖动
- 移除 YearPager crossFadeAlpha,改为无透明度渐变
- app/build.gradle.kts 添加 profileinstaller 依赖

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-26 18:27:07 +08:00
parent 20425e392c
commit 9a0222b4a2
7 changed files with 143 additions and 30 deletions

View File

@ -93,6 +93,7 @@ dependencies {
implementation(platform(libs.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.profileinstaller)
debugImplementation(libs.compose.uiToolingPreview)
debugImplementation(libs.compose.uiTooling)
}

View File

@ -20,7 +20,7 @@ import kotlinx.datetime.plus
import kotlinx.datetime.todayIn
import plus.rua.project.ui.COLLAPSE_THRESHOLD
import plus.rua.project.ui.getMonthGridInfo
import android.util.Log
import plus.rua.project.util.logd
import kotlin.time.Clock
private const val TAG_VM = "CalendarExpand"
@ -210,17 +210,27 @@ class CalendarViewModel(
* 当前视图被直接移除动画只作用在目标视图的 scale/alpha
*/
fun toggleYearView() {
val t0 = System.nanoTime()
if (_isYearView.value) {
logd(TAG_VM, "[toggleYearView] ===== START Year→Month t=$t0 =====")
composeTraceBeginSection("YearView→MonthView")
_yearViewProgress.value = 0f
logd(TAG_VM, "[toggleYearView] yearViewProgress=0 dt=${(System.nanoTime() - t0) / 1_000_000}ms")
_isYearView.value = false
logd(TAG_VM, "[toggleYearView] isYearView=false dt=${(System.nanoTime() - t0) / 1_000_000}ms")
composeTraceEndSection()
logd(TAG_VM, "[toggleYearView] ===== END Year→Month total=${(System.nanoTime() - t0) / 1_000_000}ms =====")
} else {
logd(TAG_VM, "[toggleYearView] ===== START Month→Year t=$t0 =====")
composeTraceBeginSection("MonthView→YearView")
_yearViewYear.value = _selectedDate.value.year
logd(TAG_VM, "[toggleYearView] yearViewYear=${_yearViewYear.value} dt=${(System.nanoTime() - t0) / 1_000_000}ms")
_yearViewProgress.value = 1f
logd(TAG_VM, "[toggleYearView] yearViewProgress=1 dt=${(System.nanoTime() - t0) / 1_000_000}ms")
_isYearView.value = true
logd(TAG_VM, "[toggleYearView] isYearView=true dt=${(System.nanoTime() - t0) / 1_000_000}ms")
composeTraceEndSection()
logd(TAG_VM, "[toggleYearView] ===== END Month→Year total=${(System.nanoTime() - t0) / 1_000_000}ms =====")
}
}
@ -236,13 +246,20 @@ class CalendarViewModel(
*/
@Suppress("DEPRECATION") // monthNumber 无替代 API
fun selectMonthFromYearView(month: Int) {
val t0 = System.nanoTime()
logd(TAG_VM, "[selectMonthFromYearView] ===== START month=$month t=$t0 =====")
composeTraceBeginSection("YearView:SelectMonth")
val date = if (_yearViewYear.value == today.year && today.month.number == month) today
else LocalDate(_yearViewYear.value, month, 1)
logd(TAG_VM, "[selectMonthFromYearView] targetDate=$date dt=${(System.nanoTime() - t0) / 1_000_000}ms")
_selectedDate.value = date
logd(TAG_VM, "[selectMonthFromYearView] selectedDate set dt=${(System.nanoTime() - t0) / 1_000_000}ms")
_isYearView.value = false
logd(TAG_VM, "[selectMonthFromYearView] isYearView=false dt=${(System.nanoTime() - t0) / 1_000_000}ms")
_yearViewProgress.value = 0f
logd(TAG_VM, "[selectMonthFromYearView] yearViewProgress=0 dt=${(System.nanoTime() - t0) / 1_000_000}ms")
composeTraceEndSection()
logd(TAG_VM, "[selectMonthFromYearView] ===== END total=${(System.nanoTime() - t0) / 1_000_000}ms =====")
}
fun incrementYear() {
@ -290,7 +307,7 @@ class CalendarViewModel(
fun onExpandDrag(delta: Float) {
val old = _collapseProgress.value
_collapseProgress.value = (_collapseProgress.value + delta).coerceIn(0f, 1f)
Log.d(TAG_VM, "onExpandDrag: delta=$delta old=$old new=${_collapseProgress.value}")
logd(TAG_VM, "onExpandDrag: delta=$delta old=$old new=${_collapseProgress.value}")
}
/**
@ -309,7 +326,7 @@ class CalendarViewModel(
_collapseProgress.value = 1f
"COLLAPSED (bounce back)"
}
Log.d(TAG_VM, "onExpandDragEnd: progress=$progress threshold=${1 - COLLAPSE_THRESHOLD} result=$result")
logd(TAG_VM, "onExpandDragEnd: progress=$progress threshold=${1 - COLLAPSE_THRESHOLD} result=$result")
}
/**

View File

@ -21,7 +21,7 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import android.util.Log
import plus.rua.project.util.logd
import kotlinx.datetime.DatePeriod
import kotlinx.datetime.LocalDate
import kotlinx.datetime.minus
@ -109,13 +109,13 @@ fun CalendarMonthPage(
// 全局动画参数日志(每次重组)
val pageFrameNs = System.nanoTime()
Log.d(
TAG_CMP,
"Page[$year-$month]: anchorIndex=$anchorIndex weeksSize=${weeks.size} " +
val totalCells = weeks.size * 7
logd(TAG_CMP) {
"Page[$year-$month]: anchorIndex=$anchorIndex weeksSize=${weeks.size} totalCells=$totalCells " +
"phase1End=${if (anchorIndex > 0 && weeks.size > 1) anchorIndex.toFloat() / (weeks.size - 1) else 0f} " +
"effectiveWeeks=$effectiveWeeks rowHeightPx=$rowHeightPx " +
"collapseProgress=$collapseProgress frameNs=$pageFrameNs"
)
"collapseProgress=$collapseProgress lunarMapSize=${lunarDataMap.size} frameNs=$pageFrameNs"
}
val totalHeightDp = if (rowHeightPx > 0) {
val h = rowHeightPx.toFloat()
@ -213,8 +213,7 @@ private fun WeekRow(
}
val frameTimeNs = System.nanoTime()
Log.d(
TAG_CMP,
logd(TAG_CMP) {
"WeekRow[$weekIndex]: " +
"isAnchor=$isAnchor isAbove=$isAbove isBelow=$isBelow " +
"phase1=$phase1 phase2=$phase2 phase1End=$phase1End " +
@ -222,7 +221,7 @@ private fun WeekRow(
"yOffsetPx=$yOffsetPx rowAlpha=$rowAlpha " +
"collapseProgress=$collapseProgress " +
"frameNs=$frameTimeNs"
)
}
if (rowAlpha > 0.01f) {
Row(

View File

@ -82,7 +82,7 @@ import plus.rua.project.composeTraceEndSection
import kotlin.math.abs
import kotlin.time.Clock
import androidx.lifecycle.viewmodel.compose.viewModel
import android.util.Log
import plus.rua.project.util.logd
/**
* 日历主界面包含月/周视图切换折叠动画和年视图共享元素转场
@ -119,8 +119,13 @@ fun CalendarMonthView(
animationSpec = spring(stiffness = Spring.StiffnessMedium),
label = "collapseProgress"
)
var lastLoggedCollapse by remember { mutableStateOf(-1f) }
SideEffect {
Log.d("CalendarExpandAnim", "View: target=$collapseProgress animated=$animatedCollapseProgress isCollapsed=$isCollapsed")
if (kotlin.math.abs(lastLoggedCollapse - collapseProgress) > 0.001f) {
lastLoggedCollapse = collapseProgress
logd("AnimLog", "[Collapse] target=$collapseProgress animated=$animatedCollapseProgress isCollapsed=$isCollapsed")
}
logd("AnimLog", "[MonthView] isYearView=$isYearView isCollapsed=$isCollapsed collapseProgress=$collapseProgress animated=$animatedCollapseProgress selectedDate=$selectedDate yearViewYear=$yearViewYear")
}
val density = LocalDensity.current
@ -178,6 +183,13 @@ fun CalendarMonthView(
) {
SharedTransitionLayout {
val sharedScope = this
var lastLoggedTargetState by remember { mutableStateOf(false) }
SideEffect {
if (lastLoggedTargetState != isYearView) {
lastLoggedTargetState = isYearView
logd("AnimLog", "[AnimatedContent] ★ targetState CHANGE isYearView=$isYearView t=${System.nanoTime()}")
}
}
AnimatedContent(
targetState = isYearView,
label = "month_year_transition",
@ -191,6 +203,13 @@ fun CalendarMonthView(
modifier = Modifier.fillMaxSize()
) { yearViewActive ->
if (!yearViewActive) {
androidx.compose.runtime.DisposableEffect(Unit) {
val t = System.nanoTime()
logd("AnimLog", "[MonthView] ★★★ ENTER composable t=$t")
onDispose {
logd("AnimLog", "[MonthView] ★★★ LEAVE composable alive=${(System.nanoTime() - t) / 1_000_000}ms")
}
}
composeTraceBeginSection("MonthView:Compose")
composeTraceBeginSection("CalendarPagerArea")
val layoutReady = rowHeightPx > 0
@ -239,6 +258,8 @@ fun CalendarMonthView(
{ h: Int -> if (h > 0) rowHeightPx = h }
}
with(sharedScope) {
// P0: 缓存 sharedElement tween避免每次重组创建新实例导致动画重新计算
val sharedTween = remember { tween<androidx.compose.ui.geometry.Rect>(400, easing = FastOutSlowInEasing) }
CalendarPagerArea(
selectedDate = selectedDate,
today = today,
@ -257,9 +278,7 @@ fun CalendarMonthView(
key = "month_grid_${currentYear}_${currentMonth}"
),
animatedVisibilityScope = this@AnimatedContent,
boundsTransform = { _, _ ->
tween(400, easing = FastOutSlowInEasing)
}
boundsTransform = { _, _ -> sharedTween }
)
.clipToBounds()
)
@ -276,6 +295,13 @@ fun CalendarMonthView(
composeTraceEndSection()
composeTraceEndSection()
} else {
androidx.compose.runtime.DisposableEffect(Unit) {
val t = System.nanoTime()
logd("AnimLog", "[YearView] ★★★ ENTER composable t=$t")
onDispose {
logd("AnimLog", "[YearView] ★★★ LEAVE composable alive=${(System.nanoTime() - t) / 1_000_000}ms")
}
}
composeTraceBeginSection("YearView:Compose")
Column(
modifier = Modifier
@ -294,6 +320,13 @@ fun CalendarMonthView(
}
}
)
var lastLoggedYearPage by remember { mutableIntStateOf(-1) }
SideEffect {
if (lastLoggedYearPage != yearPagerState.currentPage) {
lastLoggedYearPage = yearPagerState.currentPage
logd("AnimLog", "[YearPager] page=${yearPagerState.currentPage} settledPage=${yearPagerState.settledPage} offset=${yearPagerState.currentPageOffsetFraction}")
}
}
HorizontalPager(
state = yearPagerState,
beyondViewportPageCount = 0,
@ -302,19 +335,21 @@ fun CalendarMonthView(
.fillMaxWidth()
.weight(1f)
) { page ->
val pageOffset = abs(yearPagerState.currentPageOffsetFraction)
val isCurrentPage = page == yearPagerState.currentPage
val crossFadeAlpha = if (isCurrentPage) {
1f - pageOffset
} else {
pageOffset
// P0: 稳定 pageYear 计算,避免 settledPage/yearViewYear 不同步导致抖动
val pageYear = remember(page, yearViewYear, yearPagerState.settledPage) {
yearViewYear + (page - yearPagerState.settledPage)
}
val isCurrentPage = page == yearPagerState.currentPage
if (isCurrentPage) {
logd("AnimLog") { "[YearPager] Compose page=$page year=$pageYear" }
}
val pageYear = yearViewYear + (page - yearPagerState.settledPage)
YearGridView(
year = pageYear,
selectedMonth = if (pageYear == currentYear) currentMonth else 0,
today = today,
onMonthClick = { month ->
val clickT = System.nanoTime()
logd("AnimLog") { "[YearGridView] MonthClick month=$month year=$pageYear t=$clickT" }
viewModel.selectMonthFromYearView(month)
@Suppress("DEPRECATION") // monthNumber 无替代 API
val targetPage = yearMonthToPage(
@ -322,12 +357,13 @@ fun CalendarMonthView(
today.year, today.month.number
)
if (targetPage != pagerState.currentPage) {
logd("AnimLog") { "[YearPager] scrollToPage target=$targetPage" }
coroutineScope.launch { pagerState.scrollToPage(targetPage) }
}
},
sharedTransitionScope = sharedScope,
animatedVisibilityScope = this@AnimatedContent,
modifier = Modifier.alpha(crossFadeAlpha)
modifier = Modifier
)
}
}
@ -477,12 +513,13 @@ private fun CalendarPagerArea(
pagerState: PagerState,
modifier: Modifier = Modifier
) {
val t0 = System.nanoTime()
val density = LocalDensity.current
val interpolatedWeeks by remember {
derivedStateOf {
val fraction = pagerState.currentPageOffsetFraction
if (abs(fraction) > OFFSET_FRACTION_THRESHOLD) {
val result = if (abs(fraction) > OFFSET_FRACTION_THRESHOLD) {
val cp = pagerState.currentPage
val baseWeeks = calculateWeeksCountForPage(cp, today)
val targetPage = cp + if (fraction > 0) 1 else -1
@ -491,6 +528,8 @@ private fun CalendarPagerArea(
} else {
calculateWeeksCountForPage(pagerState.currentPage, today).toFloat()
}
logd("AnimLog", "[PagerArea] interpolatedWeeks=$result fraction=$fraction page=${pagerState.currentPage}")
result
}
}
@ -514,6 +553,8 @@ private fun CalendarPagerArea(
}
} else 0
logd("AnimLog", "[PagerArea] gridHeightPx=$gridHeightPx effectiveRowHeightPx=$effectiveRowHeightPx effectiveWeeks=$effectiveWeeks collapseProgress=$collapseProgress screenW=$screenWidthPx rowH=$rowHeightPx dt=${(System.nanoTime() - t0) / 1_000_000}ms")
val pagerModifier = if (rowHeightPx > 0 && gridHeightPx > 0) {
Modifier
.height(with(density) { gridHeightPx.toDp() })
@ -559,17 +600,27 @@ private fun BottomCardArea(
animationSpec = tween(350, delayMillis = 100, easing = FastOutSlowInEasing),
label = "bottomCardSlide"
)
var lastLoggedSlide by remember { mutableStateOf(-1f) }
SideEffect {
if (kotlin.math.abs(lastLoggedSlide - slideProgress) > 0.001f) {
lastLoggedSlide = slideProgress
logd("AnimLog", "[BottomCard] slideProgress=$slideProgress isYearView=$isYearView")
}
}
// 延迟一帧显示 BottomCard避免 AnimatedGif 和 lunar 计算阻塞首帧
var hasLoaded by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
delay(32)
hasLoaded = true
logd("AnimLog", "[BottomCard] hasLoaded=true after delay")
}
val shouldShow = hasLoaded
val uiState by viewModel.uiState.collectAsState()
val shiftKind = viewModel.shiftKindAt(uiState.selectedDate)
logd("AnimLog", "[BottomCard] shouldShow=$shouldShow isYearView=$isYearView slideProgress=$slideProgress dragRangePx=$dragRangePx rowHeightPx=$rowHeightPx")
if (shouldShow) {
BottomCard(
isCollapsed = uiState.isCollapsed,

View File

@ -5,12 +5,16 @@ 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.SideEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import plus.rua.project.util.logd
import androidx.compose.ui.draw.alpha
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
@ -75,6 +79,14 @@ fun CalendarPager(
derivedStateOf { pagerState.currentPage }
}
var lastLoggedPage by remember { mutableIntStateOf(-1) }
SideEffect {
if (lastLoggedPage != pagerState.currentPage) {
lastLoggedPage = pagerState.currentPage
logd("AnimLog", "[CalendarPager] page=${pagerState.currentPage} settledPage=${pagerState.settledPage} offsetFraction=${pagerState.currentPageOffsetFraction}")
}
}
HorizontalPager(
state = pagerState,
beyondViewportPageCount = 0,
@ -89,12 +101,16 @@ fun CalendarPager(
pageOffset
}
val (year, month) = pageToYearMonth(page, initialYear, initialMonth)
if (isCurrentPage) {
logd("AnimLog", "[CalendarPager] Compose page=$page ($year-$month) alpha=$alpha pageOffset=$pageOffset")
}
CalendarMonthPage(
year = year,
month = month,
selectedDate = selectedDate,
today = today,
onDateClick = { date ->
val clickT = System.nanoTime()
onDateClick(date)
// 点击跨月日期时,滚动到该月对应的页
val clickedYear = date.year
@ -105,6 +121,7 @@ fun CalendarPager(
val targetPage =
yearMonthToPage(clickedYear, clickedMonth, initialYear, initialMonth)
if (targetPage != pagerState.currentPage) {
logd("AnimLog", "[CalendarPager] Cross-month click date=$date targetPage=$targetPage t=$clickT")
coroutineScope.launch {
pagerState.animateScrollToPage(targetPage)
}

View File

@ -29,6 +29,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import plus.rua.project.util.logd
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -87,6 +88,14 @@ fun YearGridView(
animatedVisibilityScope: AnimatedVisibilityScope,
modifier: Modifier = Modifier
) {
val enterT = System.nanoTime()
logd("AnimLog", "[YearGridView] ★★★ ENTER year=$year selectedMonth=$selectedMonth t=$enterT")
androidx.compose.runtime.DisposableEffect(year) {
logd("AnimLog", "[YearGridView] DisposableEffect attached year=$year")
onDispose {
logd("AnimLog", "[YearGridView] ★★★ LEAVE year=$year alive=${(System.nanoTime() - enterT) / 1_000_000}ms")
}
}
composeTraceBeginSection("YearGridView:$year")
// P0-F: 主题色在 YearGridView 级别一次性读取并缓存
@ -165,6 +174,9 @@ fun YearGridView(
(0 until 3).forEach { col ->
val month = row * 3 + col + 1
with(sharedTransitionScope) {
// P0: 缓存 sharedElement tween避免每次重组创建新实例
val miniMonthTween = remember { tween<androidx.compose.ui.geometry.Rect>(400, easing = FastOutSlowInEasing) }
val seKey = "month_grid_${year}_$month"
MiniMonth(
year = year,
month = month,
@ -180,12 +192,10 @@ fun YearGridView(
.weight(1f)
.sharedElement(
sharedContentState = rememberSharedContentState(
key = "month_grid_${year}_$month"
key = seKey
),
animatedVisibilityScope = animatedVisibilityScope,
boundsTransform = { _, _ ->
tween(400, easing = FastOutSlowInEasing)
}
boundsTransform = { _, _ -> miniMonthTween }
)
)
}

View File

@ -0,0 +1,18 @@
package plus.rua.project.util
import android.util.Log
import plus.rua.project.shared.BuildConfig
@Suppress("NOTHING_TO_INLINE")
inline fun logd(tag: String, message: () -> String) {
if (BuildConfig.DEBUG) {
Log.d(tag, message())
}
}
@Suppress("NOTHING_TO_INLINE")
inline fun logd(tag: String, message: String) {
if (BuildConfig.DEBUG) {
Log.d(tag, message)
}
}