diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f876a95..2fe124e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) } diff --git a/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt b/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt index 45b2148..aa4b649 100644 --- a/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt @@ -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") } /** diff --git a/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthPage.kt b/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthPage.kt index d43d1dc..810bae8 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthPage.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthPage.kt @@ -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( diff --git a/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthView.kt index c68210a..e50d8b9 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -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(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, diff --git a/core/src/main/kotlin/plus/rua/project/ui/CalendarPager.kt b/core/src/main/kotlin/plus/rua/project/ui/CalendarPager.kt index fffa161..5d9d6f6 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/CalendarPager.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/CalendarPager.kt @@ -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) } diff --git a/core/src/main/kotlin/plus/rua/project/ui/YearGridView.kt b/core/src/main/kotlin/plus/rua/project/ui/YearGridView.kt index cc2fb8a..e9765b8 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/YearGridView.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/YearGridView.kt @@ -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(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 } ) ) } diff --git a/core/src/main/kotlin/plus/rua/project/util/AnimLog.kt b/core/src/main/kotlin/plus/rua/project/util/AnimLog.kt new file mode 100644 index 0000000..432be1a --- /dev/null +++ b/core/src/main/kotlin/plus/rua/project/util/AnimLog.kt @@ -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) + } +}