perf: 集成 Baseline Profiles,优化启动性能与文档

- 添加 core/proguard-rules.pro 保留关键类方法签名
- 添加 core/src/main/baseline-prof.txt 预编译热点路径
- 引入 androidx.profileinstaller 依赖
- LunarCache 新增 formatLunarDate() 复用缓存数据
- BottomCard 使用 remember 缓存农历描述避免重复计算
- CalendarMonthView 延迟一帧显示 BottomCard,避免首帧阻塞
- README 补充 Baseline Profiles 维护指南与性能监控说明

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-21 10:22:43 +08:00
parent 6aefaf33a6
commit 6582d5970e
8 changed files with 317 additions and 18 deletions

112
README.md
View File

@ -20,3 +20,115 @@
- iOS 入口为 `MainViewController.kt`,Xcode 工程位于 `iosApp/`
线条小狗表情包来自 https://www.douban.com/group/topic/264788645/?_i=9181692phrDzjR,9241256phrDzjR
## 快速开始
```bash
# 编译 Android debug APK
./gradlew :app:assembleDebug
# 安装到设备
./gradlew :app:installDebug
# 编译 release APK含 Baseline Profiles
./gradlew :app:assembleRelease
```
## Baseline Profiles 维护指南
本项目已集成 [Baseline Profiles](https://developer.android.com/topic/performance/baselineprofiles),用于消除冷启动时的 JIT 编译开销。Release APK 构建时会自动将 `core/src/main/baseline-prof.txt` 打包进 `assets/dexopt/baseline.prof`
### 何时需要更新 `baseline-prof.txt`
每次发版前,检查以下清单。任一条件命中,就需要更新 profile
| 检查项 | 是否需要更新 | 说明 |
|--------|-------------|------|
| 新增/修改了首帧渲染的 composable | 必须 | `MainActivity``CalendarMonthView` 启动路径上的任何 composable |
| 修改了 `DayCell.kt` 的方法签名 | 必须 | DayCell 是启动最热点35 次调用) |
| 修改了 `CalendarMonthPage.kt` 的方法签名 | 必须 | 月度网格页面在首帧渲染 |
| 修改了 `CalendarMonthView.kt` 的方法签名 | 必须 | 根 composable |
| 修改了 `LunarCache.kt` 的计算逻辑 | 建议 | 缓存 miss 时会走 compute() 路径 |
| 新增/删除了 `tyme4kt` 的调用 | 建议 | 农历计算是 CPU 密集型 |
| 仅修改 UI 颜色、文字、布局 | 不需要 | 不涉及方法签名变化 |
| 新增设置页、关于页等非首屏页面 | 不需要 | 不在冷启动路径 |
### 如何更新
1. **定位新增/变更的热点方法**
通过 logcat 抓取启动时的 JIT 编译日志:
```bash
adb shell setprop dalvik.vm.extra-opts -verbose:compiler
adb logcat -s "JIT" | grep "plus/rua/project"
```
或抓取带 ART 详细日志的启动 trace
```bash
adb shell am start -W -n plus.rua.project/.MainActivity
adb logcat -d | grep -E "JIT|dex2oat|BaselineProfile"
```
2. **编辑 `core/src/main/baseline-prof.txt`**
新增规则格式:
```
HSPL<类全路径;-><方法名>(<参数类型签名>)<返回类型签名>
```
例如新增一个 composable `NewWidget`
```
HSPLplus/rua/project/ui/NewWidgetKt;->NewWidget(Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;I)V
```
常用签名速查:
| Kotlin 类型 | Profile 签名 |
|------------|-------------|
| `Int` | `I` |
| `Float` | `F` |
| `Boolean` | `Z` |
| `String` | `Ljava/lang/String;` |
| `LocalDate` | `Lkotlinx/datetime/LocalDate;` |
| `Modifier` | `Landroidx/compose/ui/Modifier;` |
| `Composer` | `Landroidx/compose/runtime/Composer;` |
| `Function0<Unit>` | `Lkotlin/jvm/functions/Function0;` |
| `Function1<T, R>` | `Lkotlin/jvm/functions/Function1;` |
| `ShiftKind?` | `Lplus/rua/project/ShiftKind;` |
| `Unit` / 无返回值 | `V` |
3. **验证方法名不被混淆**
如果新增的方法在 `core/proguard-rules.pro` 中没有保留规则,添加:
```proguard
-keepclassmembers class plus.rua.project.ui.NewWidgetKt {
public static void NewWidget(...);
}
```
4. **编译验证**
```bash
./gradlew :core:compileDebugKotlin
./gradlew :app:assembleRelease
```
5. **确认 profile 打包成功**
```bash
unzip -l app/build/outputs/apk/release/app-release-unsigned.apk | grep baseline
# 应看到 assets/dexopt/baseline.prof
```
### 自动化替代方案(推荐后期迁移)
手动维护容易遗漏,长期建议迁移到 **Macrobenchmark 自动生成**
1. 创建 `:macrobenchmark` 模块
2. 编写启动基准测试(自动遍历冷启动路径)
3. `./gradlew :macrobenchmark:connectedBenchmarkAndroidTest`
4. 自动输出 `baseline-prof.txt`,直接替换即可
参考:[Android Baseline Profiles 官方文档](https://developer.android.com/topic/performance/baselineprofiles/create-profile)
## 性能监控
项目内置了 `LunarCache`(农历/节气/节假日缓存)和性能追踪(`ComposeTrace.kt`)。查看 `DEVELOPMENT.md` 了解如何使用 Perfetto/Systrace 进行深度性能分析。

View File

@ -11,6 +11,7 @@ android {
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
consumerProguardFiles("proguard-rules.pro")
}
buildFeatures {
@ -55,6 +56,7 @@ dependencies {
implementation(libs.tyme4kt)
implementation(libs.sketch.compose)
implementation(libs.sketch.animated.gif)
implementation(libs.androidx.profileinstaller)
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${libs.versions.kotlin.get()}")
testImplementation(libs.kotlinx.coroutines.test)

17
core/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,17 @@
# Baseline Profiles 保留规则:确保方法名不被 R8 混淆,使 profile 规则匹配正确
-keepattributes SourceFile,LineNumberTable
# 保留 YaYa 业务类方法名profile 中引用的类)
-keepclassmembers class plus.rua.project.ui.DayCellKt {
public static void DayCell(...);
}
-keepclassmembers class plus.rua.project.LunarCache {
public static plus.rua.project.DayCellInfo getOrCompute(kotlinx.datetime.LocalDate);
public static java.lang.String formatLunarDate(kotlinx.datetime.LocalDate);
}
-keepclassmembers class plus.rua.project.DayCellInfo {
public java.lang.String getAnnotationText();
public boolean getIsAnnotationHighlight();
public java.lang.String getHolidayBadge();
public java.lang.String getLunarMonthName();
}

View File

@ -0,0 +1,142 @@
# YaYa 日历应用 Baseline Profile
# 覆盖冷启动热点路径,消除 JIT 编译导致的启动掉帧
# ========== 业务层热点方法 ==========
# DayCell — 启动时最热的 composable35 次调用)
HSPLplus/rua/project/ui/DayCellKt;->DayCell(Lkotlinx/datetime/LocalDate;ZZZLplus/rua/project/ShiftKind;ZLkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;)V
# LunarCache — 日期计算缓存
HSPLplus/rua/project/LunarCache;->getOrCompute(Lkotlinx/datetime/LocalDate;)Lplus/rua/project/DayCellInfo;
HSPLplus/rua/project/LunarCache;->compute(Lkotlinx/datetime/LocalDate;)Lplus/rua/project/DayCellInfo;
# DayCellInfo 数据类
HSPLplus/rua/project/DayCellInfo;-><init>(Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;)V
HSPLplus/rua/project/DayCellInfo;->getAnnotationText()Ljava/lang/String;
HSPLplus/rua/project/DayCellInfo;->getIsAnnotationHighlight()Z
HSPLplus/rua/project/DayCellInfo;->getHolidayBadge()Ljava/lang/String;
# CalendarMonthPage — 月度网格页面
HSPLplus/rua/project/ui/CalendarMonthPageKt;->CalendarMonthPage(ILkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Lkotlin/jvm/functions/Function1;FIFLkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;)V
HSPLplus/rua/project/ui/CalendarMonthPageKt;->generateMonthDays(II)Ljava/util/List;
# CalendarMonthView — 根 composable
HSPLplus/rua/project/ui/CalendarMonthViewKt;->CalendarMonthView(Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;)V
HSPLplus/rua/project/ui/CalendarMonthViewKt;->access$uiPerfLog(Ljava/lang/String;Ljava/lang/String;)V
# CalendarPager — 分页器
HSPLplus/rua/project/ui/CalendarPagerKt;->CalendarPager(Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;FIFLkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function1;Landroidx/compose/foundation/pager/PagerState;Landroidx/compose/ui/Modifier;)V
# WeekPager — 周视图分页器
HSPLplus/rua/project/ui/WeekPagerKt;->WeekPager(Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZLandroidx/compose/ui/Modifier;)V
# BottomCard — 底部卡片
HSPLplus/rua/project/ui/BottomCardKt;->BottomCard(Lplus/rua/project/CalendarViewModel;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;FLandroidx/compose/ui/Modifier;)V
HSPLplus/rua/project/ui/BottomCardKt;->access$formatLunarDate(Lkotlinx/datetime/LocalDate;)Ljava/lang/String;
# CalendarViewModel — 状态管理
HSPLplus/rua/project/CalendarViewModel;-><init>(Lkotlinx/coroutines/CoroutineScope;Lkotlin/time/Clock;)V
HSPLplus/rua/project/CalendarViewModel;->getSelectedDate()Lkotlinx/datetime/LocalDate;
HSPLplus/rua/project/CalendarViewModel;->getMonthDays(II)Ljava/util/List;
# CalendarUtils — 工具函数
HSPLplus/rua/project/ui/CalendarUtilsKt;->calculateWeeksCount(II)I
HSPLplus/rua/project/ui/CalendarUtilsKt;->calculateWeeksCountForPage(ILkotlinx/datetime/LocalDate;)I
HSPLplus/rua/project/ui/CalendarUtilsKt;->pageToYearMonth(IILkotlinx/datetime/LocalDate;)Lkotlin/Pair;
HSPLplus/rua/project/ui/CalendarUtilsKt;->yearMonthToPage(IIII)I
# ========== Compose 框架关键路径 ==========
# Composer — Compose 运行时核心
HSPLandroidx/compose/runtime/ComposerImpl;->startRestartGroup(I)Landroidx/compose/runtime/Composer;
HSPLandroidx/compose/runtime/ComposerImpl;->endRestartGroup()Landroidx/compose/runtime/ScopeUpdateScope;
HSPLandroidx/compose/runtime/ComposerImpl;->startReplaceableGroup(I)V
HSPLandroidx/compose/runtime/ComposerImpl;->endReplaceableGroup()V
HSPLandroidx/compose/runtime/ComposerImpl;->changed(Ljava/lang/Object;)Z
HSPLandroidx/compose/runtime/ComposerImpl;->rememberedValue()Ljava/lang/Object;
HSPLandroidx/compose/runtime/ComposerImpl;->updateRememberedValue(Ljava/lang/Object;)V
# Recomposer
HSPLandroidx/compose/runtime/Recomposer;->composeInitial$runtime_release(Landroidx/compose/runtime/ControlledComposition;Lkotlin/jvm/functions/Function2;)V
HSPLandroidx/compose/runtime/Recomposer;->runRecomposeAndApplyChanges(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
# AndroidComposeView — 渲染
HSPLandroidx/compose/ui/platform/AndroidComposeView;->draw(Landroid/graphics/Canvas;)V
HSPLandroidx/compose/ui/platform/AndroidComposeView;->onMeasure(II)V
HSPLandroidx/compose/ui/platform/AndroidComposeView;->onLayout(ZIIII)V
HSPLandroidx/compose/ui/platform/AndroidComposeView;->dispatchDraw(Landroid/graphics/Canvas;)V
# remember / derivedStateOf
HSPLandroidx/compose/runtime/EffectsKt;->remember(Ljava/lang/Object;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object;
HSPLandroidx/compose/runtime/EffectsKt;->remember([Ljava/lang/Object;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)Ljava/lang/Object;
HSPLandroidx/compose/runtime/SnapshotStateKt;->derivedStateOf(Lkotlin/jvm/functions/Function0;)Landroidx/compose/runtime/State;
# SideEffect / LaunchedEffect
HSPLandroidx/compose/runtime/EffectsKt;->SideEffect(Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
HSPLandroidx/compose/runtime/EffectsKt;->LaunchedEffect(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
# ========== Compose UI 基础组件 ==========
# Box / Column / Row
HSPLandroidx/compose/foundation/layout/BoxKt;->Box(Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
HSPLandroidx/compose/foundation/layout/ColumnKt;->Column(Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/ui/Alignment$Horizontal;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
HSPLandroidx/compose/foundation/layout/RowKt;->Row(Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/Arrangement$Horizontal;Landroidx/compose/ui/Alignment$Vertical;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
# Text
HSPLandroidx/compose/material3/TextKt;->Text(Ljava/lang/String;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontSynthesis;Landroidx/compose/ui/text/style/TextAlign;Landroidx/compose/ui/text/style/TextDirection;JLandroidx/compose/ui/text/style/TextDecoration;Landroidx/compose/ui/text/style/TextGeometricTransform;Landroidx/compose/ui/text/style/TextIndent;Landroidx/compose/ui/text/PlatformTextStyle;Landroidx/compose/ui/text/style/LineHeightStyle;Landroidx/compose/ui/text/style/LineBreak;Landroidx/compose/ui/text/style/Hyphens;Landroidx/compose/ui/text/IntLocale;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/text/SpanStyle;Landroidx/compose/runtime/Composer;IIII)V
# Surface / MaterialTheme
HSPLandroidx/compose/material3/SurfaceKt;->Surface(Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/Shape;JJFFLandroidx/compose/foundation/BorderStroke;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
HSPLandroidx/compose/material3/MaterialTheme;-><clinit>()V
# Modifier
HSPLandroidx/compose/ui/Modifier$Companion;-><clinit>()V
HSPLandroidx/compose/ui/Modifier$Companion;->getInstance$annotations()V
HSPLandroidx/compose/ui/Modifier;->then(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
# ========== 动画相关 ==========
# updateTransition / animateFloat / animateColor
HSPLandroidx/compose/animation/core/TransitionKt;->updateTransition(Ljava/lang/Object;Ljava/lang/String;Landroidx/compose/runtime/Composer;II)Landroidx/compose/animation/core/Transition;
HSPLandroidx/compose/animation/core/Transition$TransitionAnimationState;-><init>(Landroidx/compose/animation/core/Transition;Ljava/lang/Object;Landroidx/compose/animation/core/TwoWayConverter;Landroidx/compose/animation/core/AnimationVector;)V
HSPLandroidx/compose/animation/core/Transition$TransitionAnimationState;->updateTargetValues$animation_core_release(Ljava/lang/Object;Landroidx/compose/animation/core/AnimationSpec;)V
# animateFloatAsState
HSPLandroidx/compose/animation/core/AnimateAsStateKt;->animateFloatAsState(FLandroidx/compose/animation/core/AnimationSpec;FLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State;
# ========== Pager 相关 ==========
# HorizontalPager
HSPLandroidx/compose/foundation/pager/PagerKt;->HorizontalPager(Landroidx/compose/foundation/pager/PagerState;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/pager/PagerFlingBehavior;ZIILandroidx/compose/foundation/gestures/snapping/SnapPosition;Landroidx/compose/ui/input/nestedscroll/NestedScrollConnection;ZLkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
HSPLandroidx/compose/foundation/pager/PagerState;-><init>(IFLandroidx/compose/foundation/lazy/LazyListState;)V
HSPLandroidx/compose/foundation/pager/PagerState;->getCurrentPage()I
HSPLandroidx/compose/foundation/pager/PagerState;->getCurrentPageOffsetFraction()F
# ========== Sketch 图片加载 ==========
# AsyncImage
HSPLio/github/panpf/sketch4/compose/AsyncImageKt;->AsyncImage(Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;FLandroidx/compose/ui/graphics/ColorFilter;Landroidx/compose/ui/graphics/FilterQuality;ZLkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
# ========== tyme4kt 农历计算 ==========
# SolarDay — 创建和查询
HSPLcom/tyme/solar/SolarDay;->fromYmd(III)Lcom/tyme/solar/SolarDay;
HSPLcom/tyme/solar/SolarDay;->getLunarDay()Lcom/tyme/lunar/LunarDay;
HSPLcom/tyme/solar/SolarDay;->getTermDay()Lcom/tyme/solar/SolarTermDay;
HSPLcom/tyme/solar/SolarDay;->getFestival()Lcom/tyme/solar/SolarFestival;
HSPLcom/tyme/solar/SolarDay;->getLegalHoliday()Lcom/tyme/solar/LegalHoliday;
# LunarDay
HSPLcom/tyme/lunar/LunarDay;->getFestival()Lcom/tyme/lunar/LunarFestival;
HSPLcom/tyme/lunar/LunarDay;->getName()Ljava/lang/String;
HSPLcom/tyme/lunar/LunarDay;->getLunarMonth()Lcom/tyme/lunar/LunarMonth;
# LunarMonth
HSPLcom/tyme/lunar/LunarMonth;->getName()Ljava/lang/String;
# SolarTermDay
HSPLcom/tyme/solar/SolarTermDay;->getDayIndex()I
HSPLcom/tyme/solar/SolarTermDay;->getSolarTerm()Lcom/tyme/solar/SolarTerm;
# SolarTerm
HSPLcom/tyme/solar/SolarTerm;->getName()Ljava/lang/String;

View File

@ -43,6 +43,19 @@ object LunarCache {
trimIfNeeded()
}
/**
* 获取完整农历日期字符串"农历四月初三"
*
* 复用缓存中的 lunarMonthName annotationText避免重复创建 SolarDay
*/
@Synchronized
@Suppress("DEPRECATION") // monthNumber 无替代 API
fun formatLunarDate(date: LocalDate): String {
val info = getOrCompute(date)
val dayText = info.annotationText.removeSuffix("")
return "农历${info.lunarMonthName}${dayText}"
}
private fun trimIfNeeded() {
if (cache.size > MAX_SIZE) {
val toRemove = (cache.size * 0.2).toInt().coerceAtLeast(1)
@ -61,34 +74,35 @@ object LunarCache {
val solarDay = SolarDay.fromYmd(date.year, date.monthNumber, date.day)
val holidayBadge = solarDay.getLegalHoliday()?.let { if (it.isWork()) "" else "" }
val lunarDay = solarDay.getLunarDay()
val lunarMonth = lunarDay.getLunarMonth()
val lunarMonthName = lunarMonth.getName()
// 农历传统节日(仅当天)
val lunarFestival = lunarDay.getFestival()
if (lunarFestival != null) {
return DayCellInfo(lunarFestival.getName(), true, holidayBadge)
return DayCellInfo(lunarFestival.getName(), true, holidayBadge, lunarMonthName)
}
// 节气(当天才显示)
val termDay = solarDay.getTermDay()
if (termDay.getDayIndex() == 0) {
return DayCellInfo(termDay.getSolarTerm().getName(), true, holidayBadge)
return DayCellInfo(termDay.getSolarTerm().getName(), true, holidayBadge, lunarMonthName)
}
// 公历节日(仅当天)
val solarFestival = solarDay.getFestival()
if (solarFestival != null) {
return DayCellInfo(solarFestival.getName(), true, holidayBadge)
return DayCellInfo(solarFestival.getName(), true, holidayBadge, lunarMonthName)
}
// 默认:农历日期
val name = lunarDay.getName()
val text = if (name == "初一") {
val lunarMonth = lunarDay.getLunarMonth()
"${lunarMonth.getName()}"
"${lunarMonthName}"
} else {
name
}
return DayCellInfo(text, false, holidayBadge)
return DayCellInfo(text, false, holidayBadge, lunarMonthName)
}
}
@ -102,5 +116,6 @@ object LunarCache {
data class DayCellInfo(
val annotationText: String,
val isAnnotationHighlight: Boolean,
val holidayBadge: String?
val holidayBadge: String?,
val lunarMonthName: String? = null
)

View File

@ -17,6 +17,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -26,6 +27,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.datetime.LocalDate
import plus.rua.project.CalendarViewModel
import plus.rua.project.LunarCache
import plus.rua.project.ShiftKind
/**
@ -55,7 +57,7 @@ fun BottomCard(
@Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口
val solarDesc = "${selectedDate.monthNumber}${selectedDate.day}"
val lunarDesc = formatLunarDate(selectedDate)
val lunarDesc = remember(selectedDate) { LunarCache.formatLunarDate(selectedDate) }
val shiftMessage = when (viewModel.shiftKindAt(selectedDate)) {
ShiftKind.WORK -> "小小上班,轻松拿下!"
ShiftKind.OFF -> "耶耶耶,美美休息!"

View File

@ -552,16 +552,23 @@ private fun BottomCardArea(
label = "bottomCardSlide"
)
BottomCard(
viewModel = viewModel,
selectedDate = viewModel.selectedDate,
today = today,
dragRangePx = dragRangePx,
modifier = modifier.graphicsLayer {
translationY = slideProgress * 200.dp.toPx()
alpha = 1f - slideProgress
}
)
// P0-J: 延迟一帧显示 BottomCard避免 AnimatedGif 和 lunar 计算阻塞首帧
var frameCount by remember { mutableIntStateOf(0) }
androidx.compose.runtime.SideEffect { frameCount++ }
val shouldShow = frameCount >= 2
if (shouldShow) {
BottomCard(
viewModel = viewModel,
selectedDate = viewModel.selectedDate,
today = today,
dragRangePx = dragRangePx,
modifier = modifier.graphicsLayer {
translationY = slideProgress * 200.dp.toPx()
alpha = 1f - slideProgress
}
)
}
}
@Composable

View File

@ -16,6 +16,7 @@ material3 = "1.10.0-alpha05"
kotlinx-datetime = "0.8.0"
tyme4kt = "1.4.5"
sketch = "4.4.0"
profileinstaller = "1.4.0"
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
@ -35,6 +36,7 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t
tyme4kt = { module = "cn.6tail:tyme4kt", version.ref = "tyme4kt" }
sketch-compose = { module = "io.github.panpf.sketch4:sketch-compose", version.ref = "sketch" }
sketch-animated-gif = { module = "io.github.panpf.sketch4:sketch-animated-gif", version.ref = "sketch" }
androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "profileinstaller" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }