Compare commits
19 Commits
bcc59ca7a0
...
bd4dad4794
| Author | SHA1 | Date | |
|---|---|---|---|
| bd4dad4794 | |||
| a731507b3b | |||
| 85bfba241c | |||
| fd8e1fc1cb | |||
| be46d4b512 | |||
| b5a2ccf151 | |||
| b5e60e8bf8 | |||
| 21327d572b | |||
| de329c2020 | |||
| 8204ae15cf | |||
| fab3c2d763 | |||
| 423e68ec19 | |||
| 0edcc005bc | |||
| 1f2fc8644f | |||
| d98579c2bf | |||
| f6b1ed7368 | |||
| b242fb8ecc | |||
| ade852208b | |||
| 6f4fa68f0d |
10
AGENTS.md
10
AGENTS.md
@ -8,7 +8,7 @@
|
||||
|------|------|------|
|
||||
| `:core` | `com.android.library` | 所有 Compose UI、ViewModel、业务逻辑 |
|
||||
| `:app` | `com.android.application` | 薄壳:MainActivity + Manifest + 主题 |
|
||||
| `:macrobenchmark` | `com.android.test` | Baseline Profile 生成 |
|
||||
| `:macrobenchmark` | `com.android.test` | Baseline Profile / Startup Profile 生成 |
|
||||
|
||||
**铁律**:`:app` 不添加业务逻辑,所有代码写在 `:core`。
|
||||
|
||||
@ -22,9 +22,9 @@
|
||||
./gradlew spotlessApply # 格式化(ktlint)
|
||||
```
|
||||
|
||||
Baseline Profile 和性能追踪需连接设备:
|
||||
Baseline Profile / Startup Profile 和性能追踪需连接设备:
|
||||
```bash
|
||||
./gradlew :macrobenchmark:updateBaselineProfile # 生成 + 自动复制到 core/src/main/baseline-prof.txt
|
||||
./gradlew :macrobenchmark:updateBaselineProfile # 生成并复制两份 Profile 到 :core
|
||||
./scripts/profile.sh # 默认 8 秒,输出到 logs/
|
||||
```
|
||||
|
||||
@ -78,8 +78,8 @@ Baseline Profile 和性能追踪需连接设备:
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `CLAUDE.md` | 完整架构说明、组件树、动画机制、Pager 映射逻辑 |
|
||||
| `DEVELOPMENT.md` | 性能追踪、Baseline Profile、模拟器启动参数 |
|
||||
| `DEVELOPMENT.md` | 性能追踪、Baseline Profile / Startup Profile、模拟器启动参数 |
|
||||
| `COMMENTS.md` | KDoc 规范、注释原则、反模式清单 |
|
||||
| `app/AGENTS.md` | `:app` 模块细则 |
|
||||
| `core/AGENTS.md` | `:core` 模块细则 |
|
||||
| `macrobenchmark/AGENTS.md` | Baseline Profile 模块细则 |
|
||||
| `macrobenchmark/AGENTS.md` | Baseline Profile / Startup Profile 模块细则 |
|
||||
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@ -5,6 +5,15 @@ All notable changes to the YaYa project are documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- 修正 `:macrobenchmark` 将 Startup Profile 误当作 Baseline Profile 使用的问题。
|
||||
`updateBaselineProfile` Task 现在会同时复制 `*-baseline-prof.txt` 到 `core/src/main/baseline-prof.txt`
|
||||
以及 `*-startup-prof.txt` 到 `core/src/main/baselineProfiles/startup-prof.txt`,使 AOT 编译优化与 DEX layout 优化同时生效。
|
||||
- 同步更新所有相关文档(`AGENTS.md`、`README.md`、`DEVELOPMENT.md`、`CLAUDE.md`、
|
||||
`macrobenchmark/AGENTS.md`、`BaselineProfileGenerator.kt`),明确区分 Baseline Profile 与 Startup Profile。
|
||||
|
||||
## [1.1.0] - 2026-06-02
|
||||
|
||||
### Added
|
||||
@ -236,10 +245,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Aliyun Maven mirrors (switched back to Maven Central / Google)
|
||||
- Unused Compose runtime ProGuard keep rules
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- No unreleased changes at this time.
|
||||
|
||||
---
|
||||
|
||||
[1.1.0]: https://github.com/xfy/yayacal/releases/tag/v1.1.0
|
||||
|
||||
@ -29,7 +29,7 @@ Gradle configuration cache and build cache are enabled by default (`gradle.prope
|
||||
**Three-module structure:**
|
||||
- `:core` — all Compose UI, ViewModel, and business logic (`com.android.library`)
|
||||
- `:app` — thin Android shell (`MainActivity` → `App()`)
|
||||
- `:macrobenchmark` — Macrobenchmark module for Baseline Profile generation
|
||||
- `:macrobenchmark` — Macrobenchmark module for Baseline Profile / Startup Profile generation
|
||||
|
||||
**Calendar UI composition** (all in `plus.rua.project.ui`):
|
||||
```
|
||||
|
||||
@ -39,7 +39,7 @@ trace 中包含自定义标记:
|
||||
- `getMonthDays:*` — ViewModel 月份网格计算
|
||||
- `VM:collapseProgress:*` — 折叠动画拖拽(onDrag/onDragEnd/onExpandDrag/onExpandDragEnd)
|
||||
|
||||
## Baseline Profile
|
||||
## Baseline Profile / Startup Profile
|
||||
|
||||
```bash
|
||||
# 编译 Android debug APK
|
||||
@ -48,29 +48,37 @@ trace 中包含自定义标记:
|
||||
# 安装到设备
|
||||
./gradlew :app:installDebug
|
||||
|
||||
# 编译 release APK(含 Baseline Profiles)
|
||||
# 编译 release APK(含 Baseline / Startup Profiles)
|
||||
./gradlew :app:assembleRelease
|
||||
|
||||
# 安装 benchmark 构建类型 APK
|
||||
./gradlew :app:installBenchmark
|
||||
```
|
||||
|
||||
Baseline Profile 自动生成器。
|
||||
一键生成并复制到 `:core`:
|
||||
|
||||
运行方式(一键生成 + 自动复制到 :core):
|
||||
|
||||
```
|
||||
```bash
|
||||
./gradlew :macrobenchmark:updateBaselineProfile
|
||||
```
|
||||
|
||||
生成后会得到两份产物:
|
||||
|
||||
| 产物 | 目标路径 | 用途 |
|
||||
|------|--------|------|
|
||||
| Baseline Profile | `core/src/main/baseline-prof.txt` | 指导 ART 做 AOT 编译 |
|
||||
| Startup Profile | `core/src/main/baselineProfiles/startup-prof.txt` | 指导 AGP 做 DEX layout 优化 |
|
||||
|
||||
仅运行基准测试(不自动复制):
|
||||
|
||||
```
|
||||
```bash
|
||||
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest
|
||||
```
|
||||
|
||||
手动复制路径:
|
||||
`macrobenchmark/build/outputs/connected_android_test_additional_output/`
|
||||
|
||||
注意:当前 `benchmark` 构建类型继承自 `release`(`isMinifyEnabled = true`),因此生成的文本 profile 会包含 R8 混淆后的类/方法名。AGP 在打包 APK/AAB 时会根据最终混淆映射将其转换为二进制 profile,这是正常行为。
|
||||
|
||||
## 模拟器
|
||||
|
||||
```sh
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
- `kotlinx-datetime` 处理所有日期逻辑
|
||||
- `tyme4kt` 提供农历、节气与传统节日
|
||||
- `sketch` 渲染动画 WebP
|
||||
- 三模块:`:core`(UI + 逻辑) · `:app`(薄壳) · `:macrobenchmark`(Baseline Profile 生成)
|
||||
- 三模块:`:core`(UI + 逻辑) · `:app`(薄壳) · `:macrobenchmark`(Baseline Profile / Startup Profile 生成)
|
||||
|
||||
## 构建
|
||||
|
||||
@ -37,8 +37,8 @@
|
||||
./gradlew :core:testDebugUnitTest # 运行全部测试
|
||||
./gradlew :core:testDebugUnitTest --tests "plus.rua.project.ui.CalendarUtilsTest" # 运行单个测试
|
||||
|
||||
# Baseline Profile(需要连接设备)
|
||||
./gradlew :macrobenchmark:updateBaselineProfile # 一键生成 + 自动复制到 :core
|
||||
# Baseline Profile / Startup Profile(需要连接设备)
|
||||
./gradlew :macrobenchmark:updateBaselineProfile # 生成并复制两份 Profile 到 :core
|
||||
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest # 仅运行基准测试
|
||||
|
||||
# 性能 Profiling(需要连接设备)
|
||||
|
||||
@ -1,21 +1,22 @@
|
||||
<!-- Parent: ../AGENTS.md -->
|
||||
<!-- Generated: 2026-05-20 | Updated: 2026-05-20 -->
|
||||
<!-- Generated: 2026-05-20 | Updated: 2026-06-16 -->
|
||||
|
||||
# app
|
||||
|
||||
## Purpose
|
||||
Android 应用壳层模块,仅包含入口 `MainActivity`、`AboutActivity`、`LicensesActivity` 和最小化的 Android 平台配置。所有 UI 和业务逻辑均来自 `:core` 模块。
|
||||
Android 应用壳层模块,仅包含启动页 `SplashActivity`、主界面 `MainActivity`、`AboutActivity`、`LicensesActivity` 和最小化的 Android 平台配置。所有 UI 和业务逻辑均来自 `:core` 模块。
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `build.gradle.kts` | Android 应用模块构建配置 |
|
||||
| `src/main/kotlin/plus/rua/project/MainActivity.kt` | Android 入口 Activity,设置 `CalendarMonthView()` Composable |
|
||||
| `src/main/kotlin/plus/rua/project/MainActivity.kt` | 主界面 Activity,设置 `CalendarMonthView()` Composable |
|
||||
| `src/main/kotlin/plus/rua/project/SplashActivity.kt` | 启动页 Activity,400ms 后跳转 MainActivity |
|
||||
| `src/main/kotlin/plus/rua/project/AboutActivity.kt` | 关于页面 Activity |
|
||||
| `src/main/kotlin/plus/rua/project/LicensesActivity.kt` | 许可证列表 Activity |
|
||||
| `src/main/AndroidManifest.xml` | Android 清单,声明 Activities 和主题 |
|
||||
| `src/main/res/values/themes.xml` | 应用主题配置(Material 3) |
|
||||
| `src/main/res/values/themes.xml` | 应用主题配置(Material) |
|
||||
| `src/main/res/values-night/themes.xml` | 夜间模式主题 |
|
||||
| `src/main/res/values/strings.xml` | 应用名称字符串 |
|
||||
| `src/main/res/anim/` | Activity 转场动画(slide_in/slide_out) |
|
||||
@ -35,6 +36,7 @@ Android 应用壳层模块,仅包含入口 `MainActivity`、`AboutActivity`、
|
||||
- 不要在此模块添加业务逻辑;所有代码应放在 `:core` 模块
|
||||
- 仅修改 Android 特有的配置:Manifest、主题、权限、Activity 声明
|
||||
- `MainActivity.kt` 应保持简洁,仅负责调用 `CalendarMonthView()`
|
||||
- `SplashActivity.kt` 也是薄壳,将启动页 UI 委托给 `:core` 模块
|
||||
|
||||
### Testing Requirements
|
||||
- 构建验证:`./gradlew :app:assembleDebug`
|
||||
@ -43,7 +45,7 @@ Android 应用壳层模块,仅包含入口 `MainActivity`、`AboutActivity`、
|
||||
### Common Patterns
|
||||
- 使用 `enableEdgeToEdge()` 实现全屏边缘到边缘显示
|
||||
- Activity 转场使用 `overridePendingTransition()` 配合 `res/anim/` 中的 XML 动画
|
||||
- 主题继承自 `Theme.AppCompat.DayNight.NoActionBar`
|
||||
- 主题继承自 `Theme.Material.Light.NoActionBar` / `Theme.Material.NoActionBar`
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
||||
@ -54,9 +54,11 @@ android {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
matchingFallbacks += listOf("release")
|
||||
// isDebuggable=false 使 macrobenchmark 在模拟器上稳定运行,且 Partial 编译模式可用。
|
||||
// 代价是生成的 baseline-prof.txt 会包含 R8 混淆后的类名;功能上仍然有效。
|
||||
// 若需要可人工维护的可读 profile,请在真机上使用 debuggable build 或引入 Baseline Profile Gradle plugin。
|
||||
// 关闭混淆,保证生成的 baseline-prof.txt / startup-prof.txt 使用原始类名,
|
||||
// 避免 R8 混淆签名导致 profile 匹配与维护风险。
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = false
|
||||
isShrinkResources = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,7 +96,6 @@ android {
|
||||
dependencies {
|
||||
implementation(project(":core"))
|
||||
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.profileinstaller)
|
||||
|
||||
@ -9,13 +9,22 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.YaYa">
|
||||
|
||||
<!--
|
||||
SplashActivity 已临时禁用,不再作为应用入口。
|
||||
如需恢复启动页,将下面的 MAIN/LAUNCHER intent-filter 从 MainActivity
|
||||
移回 SplashActivity 即可。
|
||||
-->
|
||||
<activity
|
||||
android:name=".SplashActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.YaYa.Splash" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:theme="@style/Theme.YaYa.Starting">
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
@ -4,13 +4,11 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.ReportDrawn
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import plus.rua.project.ui.CalendarMonthView
|
||||
import plus.rua.project.ui.theme.YaYaTheme
|
||||
|
||||
class MainActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
|
||||
43
app/src/main/kotlin/plus/rua/project/SplashActivity.kt
Normal file
43
app/src/main/kotlin/plus/rua/project/SplashActivity.kt
Normal file
@ -0,0 +1,43 @@
|
||||
package plus.rua.project
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import plus.rua.project.ui.SplashScreen
|
||||
import plus.rua.project.ui.theme.YaYaTheme
|
||||
|
||||
private const val SPLASH_DELAY_MS = 400L
|
||||
|
||||
/**
|
||||
* 启动页 Activity。
|
||||
*
|
||||
* 显示品牌启动图 400ms 后跳转到 [MainActivity],并 finish 自身。
|
||||
*/
|
||||
class SplashActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
YaYaTheme {
|
||||
SplashScreen(
|
||||
iconPainter = painterResource(R.drawable.ic_splash_icon),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
delay(SPLASH_DELAY_MS)
|
||||
navigateToMain()
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToMain() {
|
||||
if (isFinishing) return
|
||||
startActivityWithSlide(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable-xxxhdpi/ic_splash_icon.png
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/ic_splash_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@ -2,8 +2,7 @@
|
||||
<resources>
|
||||
<style name="Theme.YaYa" parent="@android:style/Theme.Material.NoActionBar" />
|
||||
|
||||
<style name="Theme.YaYa.Starting" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/splash_background</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.YaYa</item>
|
||||
<style name="Theme.YaYa.Splash" parent="@android:style/Theme.Material.NoActionBar">
|
||||
<item name="android:windowBackground">@color/splash_background</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@ -2,14 +2,7 @@
|
||||
<resources>
|
||||
<style name="Theme.YaYa" parent="@android:style/Theme.Material.Light.NoActionBar" />
|
||||
|
||||
<!--
|
||||
启动主题:用于 MAIN/LAUNCHER Activity。
|
||||
继承 Theme.SplashScreen,保证 Android 12 以下也能获得一致的启动窗口行为。
|
||||
windowSplashScreenBackground 使用 Material 3 静态基准色,作为动态颜色不可用时
|
||||
的 fallback,以减少启动窗口与 Compose 首帧之间的色差闪烁。
|
||||
-->
|
||||
<style name="Theme.YaYa.Starting" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/splash_background</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.YaYa</item>
|
||||
<style name="Theme.YaYa.Splash" parent="@android:style/Theme.Material.Light.NoActionBar">
|
||||
<item name="android:windowBackground">@color/splash_background</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
2
core/proguard-rules.pro
vendored
2
core/proguard-rules.pro
vendored
@ -1,4 +1,4 @@
|
||||
# Baseline Profiles 保留规则:确保方法名不被 R8 混淆,使 profile 规则匹配正确
|
||||
# Baseline Profile / Startup Profile 保留规则:确保方法名不被 R8 混淆,使 profile 规则匹配正确
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# ========== 第三方库保留 ==========
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
19286
core/src/main/baselineProfiles/startup-prof.txt
Normal file
19286
core/src/main/baselineProfiles/startup-prof.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,5 @@
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package plus.rua.project.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
@ -42,11 +44,16 @@ import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DatePicker
|
||||
import androidx.compose.material3.DatePickerDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberDatePickerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@ -74,9 +81,12 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.Month
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.atStartOfDayIn
|
||||
import kotlinx.datetime.number
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlinx.datetime.todayIn
|
||||
import kotlin.time.Instant
|
||||
import plus.rua.project.CalendarViewModel
|
||||
import plus.rua.project.ShiftKind
|
||||
import plus.rua.project.composeTraceBeginSection
|
||||
@ -125,6 +135,7 @@ fun CalendarMonthView(
|
||||
var rowHeightPx by remember { mutableIntStateOf(0) }
|
||||
var screenWidthPx by remember { mutableIntStateOf(0) }
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
var showDatePicker by remember { mutableStateOf(false) }
|
||||
|
||||
// 视图切换时自动关闭菜单
|
||||
LaunchedEffect(isYearView) {
|
||||
@ -214,7 +225,8 @@ fun CalendarMonthView(
|
||||
month = currentMonth,
|
||||
weekNumber = weekNumber,
|
||||
showToday = selectedDate != today,
|
||||
onToday = onToday
|
||||
onToday = onToday,
|
||||
onYearMonthClick = { showDatePicker = true }
|
||||
)
|
||||
WeekdayHeader(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = ROW_PADDING_DP.dp)
|
||||
@ -439,6 +451,34 @@ fun CalendarMonthView(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showDatePicker) {
|
||||
val datePickerState = rememberDatePickerState(
|
||||
initialSelectedDateMillis = selectedDate.toEpochMillis()
|
||||
)
|
||||
DatePickerDialog(
|
||||
onDismissRequest = { showDatePicker = false },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
datePickerState.selectedDateMillis?.let { millis ->
|
||||
viewModel.selectDate(millis.toLocalDate())
|
||||
}
|
||||
showDatePicker = false
|
||||
}
|
||||
) {
|
||||
Text("确定")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDatePicker = false }) {
|
||||
Text("取消")
|
||||
}
|
||||
}
|
||||
) {
|
||||
DatePicker(state = datePickerState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -611,3 +651,19 @@ private fun MenuItem(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 [LocalDate] 转换为 UTC 午夜的 epoch 毫秒。
|
||||
*
|
||||
* 用于 DatePicker 初始选中值,与 [Long.toLocalDate] 成对使用。
|
||||
*/
|
||||
private fun LocalDate.toEpochMillis(): Long =
|
||||
this.atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds()
|
||||
|
||||
/**
|
||||
* 将 epoch 毫秒转换为 UTC 日期的 [LocalDate]。
|
||||
*
|
||||
* DatePicker 返回选中日期的 UTC 午夜毫秒,经此函数得到本地逻辑日期。
|
||||
*/
|
||||
private fun Long.toLocalDate(): LocalDate =
|
||||
Instant.fromEpochMilliseconds(this).toLocalDateTime(TimeZone.UTC).date
|
||||
|
||||
@ -34,6 +34,7 @@ import androidx.compose.ui.unit.sp
|
||||
* @param weekNumber 当前 ISO 周号
|
||||
* @param showToday 是否显示「今天」按钮(当 selectedDate ≠ today 时)
|
||||
* @param onToday 点击「今天」按钮跳转今天
|
||||
* @param onYearMonthClick 点击"年月"文字打开日期选择器
|
||||
* @param modifier 外部布局修饰符
|
||||
*/
|
||||
@Composable
|
||||
@ -43,6 +44,7 @@ fun MonthHeader(
|
||||
weekNumber: Int,
|
||||
showToday: Boolean,
|
||||
onToday: (() -> Unit)? = null,
|
||||
onYearMonthClick: () -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
@ -66,7 +68,11 @@ fun MonthHeader(
|
||||
Text(
|
||||
text = "${y}年${m}月",
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable(onClick = onYearMonthClick)
|
||||
.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
|
||||
64
core/src/main/kotlin/plus/rua/project/ui/SplashScreen.kt
Normal file
64
core/src/main/kotlin/plus/rua/project/ui/SplashScreen.kt
Normal file
@ -0,0 +1,64 @@
|
||||
package plus.rua.project.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import plus.rua.project.shared.R as CoreR
|
||||
|
||||
private val SplashIconSize = 80.dp
|
||||
|
||||
/**
|
||||
* 品牌启动页 UI。
|
||||
*
|
||||
* 背景图铺满全屏,Light/Dark 资源由 Android 根据配置自动选择。
|
||||
* app icon 水平居中,其垂直中心位于状态栏下方可用区域高度的约 25% 处。
|
||||
* 图片仅用于装饰,因此 contentDescription 为 null。
|
||||
*
|
||||
* @param iconPainter app icon 的 Painter
|
||||
* @param modifier 外部传入的 Modifier
|
||||
*/
|
||||
@Composable
|
||||
fun SplashScreen(
|
||||
iconPainter: Painter,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val density = LocalDensity.current
|
||||
val screenHeightDp = configuration.screenHeightDp.dp
|
||||
val statusBarHeight = with(density) { WindowInsets.statusBars.getTop(this).toDp() }
|
||||
val availableHeight = screenHeightDp - statusBarHeight
|
||||
val iconTopPadding = statusBarHeight + (availableHeight * 0.25f) - (SplashIconSize / 2)
|
||||
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(CoreR.drawable.launch_bg),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = iconPainter,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = iconTopPadding.coerceAtLeast(statusBarHeight))
|
||||
.size(SplashIconSize),
|
||||
)
|
||||
}
|
||||
}
|
||||
BIN
core/src/main/res/drawable-night-nodpi/launch_bg.webp
Normal file
BIN
core/src/main/res/drawable-night-nodpi/launch_bg.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
BIN
core/src/main/res/drawable-nodpi/launch_bg.webp
Normal file
BIN
core/src/main/res/drawable-nodpi/launch_bg.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
@ -4,7 +4,6 @@ android-compileSdk = "37"
|
||||
android-minSdk = "24"
|
||||
android-targetSdk = "37"
|
||||
androidx-activity = "1.13.0"
|
||||
androidx-core-splashscreen = "1.0.1"
|
||||
androidx-espresso = "3.7.0"
|
||||
androidx-lifecycle = "2.10.0"
|
||||
androidx-testExt = "1.3.0"
|
||||
@ -23,7 +22,6 @@ versions = "0.54.0"
|
||||
[libraries]
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
||||
androidx-benchmark-macro = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "benchmarkMacro" }
|
||||
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-core-splashscreen" }
|
||||
androidx-lifecycle-runtimeCompose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
|
||||
androidx-lifecycle-viewmodelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
|
||||
androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "profileinstaller" }
|
||||
|
||||
@ -1,30 +1,32 @@
|
||||
<!-- Parent: ../AGENTS.md -->
|
||||
<!-- Generated: 2026-05-22 | Updated: 2026-05-22 -->
|
||||
<!-- Generated: 2026-05-22 | Updated: 2026-06-15 -->
|
||||
|
||||
# macrobenchmark
|
||||
|
||||
## Purpose
|
||||
宏基准测试模块,使用 Android Baseline Profile 技术自动生成启动优化配置文件。通过 UI Automator 模拟真实用户交互路径,收集 AOT 编译所需的 profile 数据。
|
||||
宏基准测试模块,使用 Android Baseline Profile / Startup Profile 技术自动生成启动优化配置文件。通过 UI Automator 模拟真实用户交互路径,收集 AOT 编译与 DEX layout 优化所需的 profile 数据。
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `build.gradle.kts` | 模块构建配置(`com.android.test` 插件、benchmark 构建类型、Baseline Profile 自动复制任务) |
|
||||
| `build.gradle.kts` | 模块构建配置(`com.android.test` 插件、benchmark 构建类型、Baseline/Startup Profile 自动复制任务) |
|
||||
|
||||
## Subdirectories
|
||||
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `src/main/java/plus/rua/project/baseline/` | Baseline Profile 生成器(见 `src/main/java/plus/rua/project/baseline/AGENTS.md`) |
|
||||
| `src/main/java/plus/rua/project/baseline/` | Baseline Profile / Startup Profile 生成器与冷启动基准测试(见 `src/main/java/plus/rua/project/baseline/AGENTS.md`) |
|
||||
|
||||
## For AI Agents
|
||||
|
||||
### Working In This Directory
|
||||
- 修改此模块需连接 Android 设备/模拟器运行
|
||||
- Baseline Profile 一键生成:`./gradlew :macrobenchmark:updateBaselineProfile`
|
||||
- Baseline / Startup Profile 一键生成:`./gradlew :macrobenchmark:updateBaselineProfile`
|
||||
- 仅运行基准测试:`./gradlew :macrobenchmark:connectedBenchmarkAndroidTest`
|
||||
- 生成的 profile 自动复制到 `core/src/main/baseline-prof.txt`
|
||||
- 生成的两份产物自动复制到:
|
||||
- `core/src/main/baseline-prof.txt`(Baseline Profile,用于 AOT)
|
||||
- `core/src/main/baselineProfiles/startup-prof.txt`(Startup Profile,用于 DEX layout)
|
||||
|
||||
### Testing Requirements
|
||||
- 需要已安装应用的 benchmark 构建类型
|
||||
@ -34,7 +36,7 @@
|
||||
|
||||
### Internal
|
||||
- `:app` 模块 — 目标测试应用
|
||||
- `:core` 模块 — profile 自动复制到 `core/src/main/baseline-prof.txt`
|
||||
- `:core` 模块 — Baseline/Startup Profile 自动复制目标模块
|
||||
|
||||
### External
|
||||
- `androidx.benchmark:benchmark-macro-junit4`
|
||||
|
||||
@ -42,53 +42,83 @@ dependencies {
|
||||
implementation(libs.androidx.test.uiautomator)
|
||||
}
|
||||
|
||||
// ===== Baseline Profile 自动复制 =====
|
||||
// ===== Baseline Profile / Startup Profile 自动复制 =====
|
||||
// 运行 ./gradlew :macrobenchmark:updateBaselineProfile 即可一键生成并复制
|
||||
|
||||
val updateBaselineProfile by tasks.registering {
|
||||
group = "benchmark"
|
||||
description = "运行 connectedBenchmarkAndroidTest 并将生成的 startup-prof.txt 复制到 :core 模块"
|
||||
description = "运行 connectedBenchmarkAndroidTest 并将生成的 Baseline Profile 与 Startup Profile 复制到 :core 模块"
|
||||
|
||||
// 依赖基准测试 task(需要先连接设备/模拟器)
|
||||
dependsOn("connectedBenchmarkAndroidTest")
|
||||
|
||||
// 该 task 的输入是 benchmark 运行后的产物,因此不应被跳过
|
||||
outputs.upToDateWhen { false }
|
||||
|
||||
// 预先计算目标路径,避免在 doLast 中引用 project 对象(configuration cache 兼容)
|
||||
val targetPath = rootProject.projectDir.resolve("core/src/main/baseline-prof.txt").absolutePath
|
||||
val baselineTargetPath = rootProject.projectDir
|
||||
.resolve("core/src/main/baseline-prof.txt")
|
||||
.absolutePath
|
||||
val startupTargetDirPath = rootProject.projectDir
|
||||
.resolve("core/src/main/baselineProfiles")
|
||||
.absolutePath
|
||||
val buildDirPath = layout.buildDirectory.get().asFile.absolutePath
|
||||
|
||||
doLast {
|
||||
// 寻找生成的 profile 文件(benchmark 1.4+ 文件名格式:{Class}_{Method}-startup-prof.txt)
|
||||
val sourcePaths = listOf(
|
||||
"$buildDirPath/outputs/connected_android_test_additional_output/benchmark/",
|
||||
"$buildDirPath/outputs/connected_android_test_additional_output/",
|
||||
)
|
||||
|
||||
val targetFile = File(targetPath)
|
||||
var copied = false
|
||||
for (path in sourcePaths) {
|
||||
val dir = File(path)
|
||||
if (!dir.exists()) continue
|
||||
|
||||
// 优先匹配不带时间戳的 startup-prof.txt(benchmark 1.4+ 格式)
|
||||
val profileFile = dir.walkTopDown()
|
||||
.firstOrNull { it.name.endsWith("-startup-prof.txt") && !it.name.contains(Regex("-\\d{4}-\\d{2}-\\d{2}-")) }
|
||||
?: continue
|
||||
|
||||
profileFile.copyTo(targetFile, overwrite = true)
|
||||
println("✅ Baseline Profile 已自动复制到: ${targetFile.absolutePath}")
|
||||
println(" 来源: ${profileFile.absolutePath}")
|
||||
copied = true
|
||||
break
|
||||
// benchmark 1.4+ 文件名格式:{Class}_{Method}-baseline-prof.txt / {Class}_{Method}-startup-prof.txt
|
||||
// 排除带时间戳的历史文件(如 ...-2026-06-15-startup-prof.txt),只取当前 canonical 文件
|
||||
val dateStampRegex = Regex("-\\d{4}-\\d{2}-\\d{2}-")
|
||||
fun findNewestProfile(suffix: String): File? {
|
||||
val candidates = sourcePaths.flatMap { path ->
|
||||
val dir = File(path)
|
||||
if (!dir.exists()) emptyList()
|
||||
else dir.walkTopDown().filter {
|
||||
it.isFile &&
|
||||
it.name.endsWith(suffix) &&
|
||||
!it.name.contains(dateStampRegex)
|
||||
}.toList()
|
||||
}
|
||||
return candidates
|
||||
.sortedWith(compareByDescending<File> { it.lastModified() }.thenBy { it.absolutePath })
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
if (!copied) {
|
||||
throw GradleException(
|
||||
"未找到生成的 *-startup-prof.txt。\n" +
|
||||
"请确认:\n" +
|
||||
" 1. 设备/模拟器已连接 (adb devices)\n" +
|
||||
" 2. 应用已安装在 benchmark 构建类型下\n" +
|
||||
" 3. 检查 macrobenchmark/build/outputs/ 下是否有输出"
|
||||
)
|
||||
fun requireProfile(suffix: String, label: String): File {
|
||||
val sourceFile = findNewestProfile(suffix)
|
||||
?: throw GradleException(
|
||||
"未找到生成的 *$suffix。\n" +
|
||||
"请确认:\n" +
|
||||
" 1. 设备/模拟器已连接 (adb devices)\n" +
|
||||
" 2. 应用已安装在 benchmark 构建类型下\n" +
|
||||
" 3. 检查 macrobenchmark/build/outputs/ 下是否有输出"
|
||||
)
|
||||
if (sourceFile.length() == 0L) {
|
||||
throw GradleException("生成的 $label 文件为空: ${sourceFile.absolutePath}")
|
||||
}
|
||||
return sourceFile
|
||||
}
|
||||
|
||||
// 1) 先定位并校验两份源文件,确保都可用再开始复制,避免部分覆盖目标文件
|
||||
val baselineSource = requireProfile("-baseline-prof.txt", "Baseline Profile")
|
||||
val startupSource = requireProfile("-startup-prof.txt", "Startup Profile")
|
||||
|
||||
// 2) Baseline Profile → AOT 编译优化
|
||||
val baselineTargetFile = File(baselineTargetPath)
|
||||
baselineSource.copyTo(baselineTargetFile, overwrite = true)
|
||||
logger.lifecycle("✅ Baseline Profile 已自动复制到: ${baselineTargetFile.absolutePath}")
|
||||
logger.lifecycle(" 来源: ${baselineSource.absolutePath}")
|
||||
|
||||
// 3) Startup Profile → DEX layout 优化(必须放在 baselineProfiles/startup-prof.txt)
|
||||
val startupTargetDir = File(startupTargetDirPath)
|
||||
startupTargetDir.mkdirs()
|
||||
val startupTargetFile = File(startupTargetDir, "startup-prof.txt")
|
||||
startupSource.copyTo(startupTargetFile, overwrite = true)
|
||||
logger.lifecycle("✅ Startup Profile 已自动复制到: ${startupTargetFile.absolutePath}")
|
||||
logger.lifecycle(" 来源: ${startupSource.absolutePath}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<!--
|
||||
Macrobenchmark 模块的 AndroidManifest。
|
||||
该模块为 com.android.test 类型,通过 targetProjectPath 指向 :app 模块。
|
||||
在 benchmark 构建类型下运行,自动生成 baseline-prof.txt。
|
||||
在 benchmark 构建类型下运行,自动生成 baseline-prof.txt 与 startup-prof.txt。
|
||||
-->
|
||||
|
||||
</manifest>
|
||||
|
||||
@ -4,15 +4,15 @@
|
||||
# baseline
|
||||
|
||||
## Purpose
|
||||
Baseline Profile 自动生成器与启动性能基准测试。包含:
|
||||
- `BaselineProfileGenerator.kt`:通过 UI Automator 模拟核心用户交互路径(冷启动、FAB 展开、显示调休切换、CalendarPager 翻页、日期选择、BottomCard 折叠/展开),生成 AOT 编译优化所需的 startup profile。
|
||||
Baseline Profile / Startup Profile 自动生成器与启动性能基准测试。包含:
|
||||
- `BaselineProfileGenerator.kt`:通过 UI Automator 模拟核心用户交互路径(冷启动、FAB 展开、显示调休切换、CalendarPager 翻页、日期选择、BottomCard 折叠/展开),同时生成 Baseline Profile(AOT)与 Startup Profile(DEX layout)两份产物。
|
||||
- `StartupBenchmark.kt`:冷启动性能基准测试,测量 `timeToInitialDisplay` 与 `timeToFullDisplay`。
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `BaselineProfileGenerator.kt` | Profile 生成测试类,覆盖启动与核心交互路径 |
|
||||
| `BaselineProfileGenerator.kt` | Profile 生成测试类,覆盖启动与核心交互路径,生成 `*-baseline-prof.txt` 与 `*-startup-prof.txt` |
|
||||
| `StartupBenchmark.kt` | 冷启动基准测试类,覆盖 Full/Partial/None 三种编译模式 |
|
||||
|
||||
## Subdirectories
|
||||
@ -27,6 +27,8 @@ Baseline Profile 自动生成器与启动性能基准测试。包含:
|
||||
|
||||
### Testing Requirements
|
||||
- 生成 Profile:`./gradlew :macrobenchmark:updateBaselineProfile`
|
||||
- Baseline Profile 输出:`core/src/main/baseline-prof.txt`
|
||||
- Startup Profile 输出:`core/src/main/baselineProfiles/startup-prof.txt`
|
||||
- 运行冷启动基准(仅该类):`./gradlew :macrobenchmark:connectedBenchmarkAndroidTest --tests "plus.rua.project.baseline.StartupBenchmark"`
|
||||
- 需要设备/模拟器连接,应用已安装在 benchmark 构建类型下
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* Baseline Profile 自动生成器。
|
||||
* Baseline Profile / Startup Profile 自动生成器。
|
||||
*
|
||||
* 运行方式(一键生成 + 自动复制到 :core):
|
||||
* ```
|
||||
@ -29,19 +29,31 @@ import org.junit.runner.RunWith
|
||||
* 手动复制路径:
|
||||
* `macrobenchmark/build/outputs/connected_android_test_additional_output/`
|
||||
*
|
||||
* 测试覆盖启动与核心用户交互路径,实现关键路径 AOT:
|
||||
* 说明:
|
||||
* - 本文件包含两个生成测试:
|
||||
* - `generateBaselineProfile()`:`includeInStartupProfile = false`,生成 `*-baseline-prof.txt`,用于 ART 的 AOT 编译优化
|
||||
* - `generateAppStartupProfile()`:`includeInStartupProfile = true`,生成 `*-startup-prof.txt`,用于 AGP 的 DEX layout 优化
|
||||
* - 两个测试执行相同的 `runCoreUserJourney()` 交互路径,因此两份 profile 覆盖范围一致
|
||||
* - `updateBaselineProfile` Task 会分别将它们复制到:
|
||||
* - `core/src/main/baseline-prof.txt`
|
||||
* - `core/src/main/baselineProfiles/startup-prof.txt`
|
||||
*
|
||||
* 测试覆盖启动与核心用户交互路径,用于 AOT 与 DEX layout 优化:
|
||||
* 1. 冷启动 → 首帧渲染
|
||||
* 2. 显示调休切换 ON/OFF(DayCell 大规模重组 + staggered 动画)
|
||||
* 3. CalendarPager 翻页 → "今天"按钮跳回
|
||||
* 4. 跨月日期点击 → 自动跳转
|
||||
* 5. DayCell 点击
|
||||
* 6. BottomCard 拖拽折叠到周视图
|
||||
* 7. 周视图左右翻页
|
||||
* 8. BottomCard 拖拽展开回月视图
|
||||
* 9. CalendarPager 左右翻页
|
||||
* 2. 切换"显示调休"ON(DayCell 大规模重组 + staggered 动画)
|
||||
* 3. CalendarPager 右滑 → 上一个月
|
||||
* 4. 点击"今天"按钮跳回当月
|
||||
* 5. 点击跨月日期 → 自动跳转
|
||||
* 6. 再次点击"今天"跳回当月
|
||||
* 7. 点击 DayCell
|
||||
* 8. 拖拽 BottomCard 折叠到周视图
|
||||
* 9. 周视图左右翻页
|
||||
* 10. 拖拽 BottomCard 展开回月视图
|
||||
* 11. 切换"显示调休"OFF
|
||||
* 12. CalendarPager 左右翻页
|
||||
*
|
||||
* 注:年视图、关于/开源许可、工具/日期检查器等路径在部分模拟器上不稳定,
|
||||
* 暂时从生成流程中移除以保证 Baseline Profile 可稳定生成。后续可在真机上扩展覆盖。
|
||||
* 暂时从生成流程中移除以保证 Profile 可稳定生成。后续可在真机上扩展覆盖。
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class BaselineProfileGenerator {
|
||||
@ -55,122 +67,137 @@ class BaselineProfileGenerator {
|
||||
private fun MacrobenchmarkScope.safeWaitCalendarPager(timeout: Long = 5000): UiObject2? =
|
||||
device.wait(Until.findObject(By.res("calendar_pager")), timeout)
|
||||
|
||||
@Test
|
||||
fun generateBaselineProfile() {
|
||||
baselineProfileRule.collect(
|
||||
packageName = "plus.rua.project",
|
||||
includeInStartupProfile = false,
|
||||
profileBlock = {
|
||||
runCoreUserJourney()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateAppStartupProfile() {
|
||||
baselineProfileRule.collect(
|
||||
packageName = "plus.rua.project",
|
||||
includeInStartupProfile = true,
|
||||
profileBlock = {
|
||||
val TAG = "BaselineProfile"
|
||||
|
||||
// ── 1. 冷启动 ──────────────────────────────────────────
|
||||
pressHome()
|
||||
device.executeShellCommand(
|
||||
"am start -W -n plus.rua.project/.MainActivity"
|
||||
)
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 2. 切换"显示调休"ON(覆盖 DayCell 大规模重组 + staggered 动画) ──
|
||||
val fab1 = safeFindFab()
|
||||
assertNotNull("FAB 必须存在", fab1)
|
||||
fab1!!.click()
|
||||
device.waitForIdle()
|
||||
val legalHolidayOn = device.wait(
|
||||
Until.findObject(By.text("显示调休")), 3000
|
||||
)
|
||||
assertNotNull("显示调休必须出现", legalHolidayOn)
|
||||
legalHolidayOn!!.click()
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 3. 右滑 CalendarPager → 上一个月 ──────────────────────
|
||||
val calendarPager = safeWaitCalendarPager(3000)
|
||||
assertNotNull("CalendarPager 必须存在", calendarPager)
|
||||
calendarPager!!.swipe(Direction.RIGHT, 0.8f)
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 4. 点击"今天"按钮跳回当月(覆盖 MonthHeader 今天按钮) ──
|
||||
// 模拟器上 UI 渲染/动画较慢,使用较长超时等待按钮渲染
|
||||
val todayBtn = device.wait(Until.findObject(By.text("今天")), 10000)
|
||||
assertNotNull("今天按钮必须出现", todayBtn)
|
||||
todayBtn!!.click()
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 5. 点击跨月日期(网格左上角 = 上月灰色日期) ──────────
|
||||
val pagerRef = safeWaitCalendarPager(3000)
|
||||
assertNotNull("CalendarPager 必须存在(跨月点击)", pagerRef)
|
||||
val pagerBounds = pagerRef!!.visibleBounds
|
||||
val colW = pagerBounds.width() / 7
|
||||
val rowH = pagerBounds.height() / 6
|
||||
device.click(pagerBounds.left + colW / 2, pagerBounds.top + rowH / 2)
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 6. 再次点击"今天"跳回当月 ────────────────────────────
|
||||
val todayBtn2 = device.wait(Until.findObject(By.text("今天")), 3000)
|
||||
if (todayBtn2 != null) {
|
||||
todayBtn2.click()
|
||||
device.waitForIdle()
|
||||
}
|
||||
|
||||
// ── 7. 点击 DayCell ────────────────────────────────────
|
||||
val todayCell = device.wait(
|
||||
Until.findObject(By.descContains("今天")), 3000
|
||||
)
|
||||
if (todayCell != null) {
|
||||
todayCell.click()
|
||||
} else {
|
||||
val calRef = safeWaitCalendarPager(3000)
|
||||
if (calRef != null) {
|
||||
val cb = calRef.visibleBounds
|
||||
device.click(cb.centerX(), cb.centerY())
|
||||
}
|
||||
}
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 8. 拖拽 BottomCard 折叠到周视图 ─────────────────────
|
||||
val bottomCard = device.wait(Until.findObject(By.res("bottom_card")), 5000)
|
||||
assertNotNull("BottomCard 必须存在", bottomCard)
|
||||
val bcBounds = bottomCard!!.visibleBounds
|
||||
val cx = bcBounds.centerX()
|
||||
val cy = bcBounds.centerY()
|
||||
val dragDist = (bcBounds.height() * 0.4).toInt()
|
||||
device.drag(cx, cy, cx, cy - dragDist, 20)
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 9. 周视图左右翻页 ──────────────────────────────────
|
||||
val weekPager = safeWaitCalendarPager(3000)
|
||||
assertNotNull("周视图 CalendarPager 必须存在", weekPager)
|
||||
weekPager!!.swipe(Direction.LEFT, 0.5f)
|
||||
device.waitForIdle()
|
||||
weekPager.swipe(Direction.RIGHT, 0.5f)
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 10. 拖拽 BottomCard 展开回月视图 ─────────────────────
|
||||
device.drag(cx, cy - dragDist, cx, cy, 20)
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 11. 切换"显示调休"OFF ────────────────────────────────
|
||||
val fab3 = safeFindFab()
|
||||
assertNotNull("FAB 必须存在(关闭调休)", fab3)
|
||||
fab3!!.click()
|
||||
device.waitForIdle()
|
||||
val legalHolidayOff = device.wait(
|
||||
Until.findObject(By.text("显示调休")), 3000
|
||||
)
|
||||
assertNotNull("显示调休必须出现", legalHolidayOff)
|
||||
legalHolidayOff!!.click()
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 12. CalendarPager 左右翻页 ──────────────────────────
|
||||
val mainPager = safeWaitCalendarPager(5000)
|
||||
if (mainPager != null) {
|
||||
mainPager.swipe(Direction.LEFT, 0.5f)
|
||||
device.waitForIdle()
|
||||
safeWaitCalendarPager(3000)?.swipe(Direction.RIGHT, 0.5f)
|
||||
device.waitForIdle()
|
||||
}
|
||||
|
||||
Log.d(TAG, "Baseline profile 生成完成,核心路径已覆盖")
|
||||
runCoreUserJourney()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun MacrobenchmarkScope.runCoreUserJourney() {
|
||||
val TAG = "BaselineProfile"
|
||||
|
||||
// ── 1. 冷启动 ──────────────────────────────────────────
|
||||
pressHome()
|
||||
device.executeShellCommand(
|
||||
"am start -W -n plus.rua.project/.MainActivity"
|
||||
)
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 2. 切换"显示调休"ON(覆盖 DayCell 大规模重组 + staggered 动画) ──
|
||||
val fab1 = safeFindFab()
|
||||
assertNotNull("FAB 必须存在", fab1)
|
||||
fab1!!.click()
|
||||
device.waitForIdle()
|
||||
val legalHolidayOn = device.wait(
|
||||
Until.findObject(By.text("显示调休")), 3000
|
||||
)
|
||||
assertNotNull("显示调休必须出现", legalHolidayOn)
|
||||
legalHolidayOn!!.click()
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 3. 右滑 CalendarPager → 上一个月 ──────────────────────
|
||||
val calendarPager = safeWaitCalendarPager(3000)
|
||||
assertNotNull("CalendarPager 必须存在", calendarPager)
|
||||
calendarPager!!.swipe(Direction.RIGHT, 0.8f)
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 4. 点击"今天"按钮跳回当月(覆盖 MonthHeader 今天按钮) ──
|
||||
// 模拟器上 UI 渲染/动画较慢,使用较长超时等待按钮渲染
|
||||
val todayBtn = device.wait(Until.findObject(By.text("今天")), 10000)
|
||||
assertNotNull("今天按钮必须出现", todayBtn)
|
||||
todayBtn!!.click()
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 5. 点击跨月日期(网格左上角 = 上月灰色日期) ──────────
|
||||
val pagerRef = safeWaitCalendarPager(3000)
|
||||
assertNotNull("CalendarPager 必须存在(跨月点击)", pagerRef)
|
||||
val pagerBounds = pagerRef!!.visibleBounds
|
||||
val colW = pagerBounds.width() / 7
|
||||
val rowH = pagerBounds.height() / 6
|
||||
device.click(pagerBounds.left + colW / 2, pagerBounds.top + rowH / 2)
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 6. 再次点击"今天"跳回当月 ────────────────────────────
|
||||
val todayBtn2 = device.wait(Until.findObject(By.text("今天")), 3000)
|
||||
if (todayBtn2 != null) {
|
||||
todayBtn2.click()
|
||||
device.waitForIdle()
|
||||
}
|
||||
|
||||
// ── 7. 点击 DayCell ────────────────────────────────────
|
||||
val todayCell = device.wait(
|
||||
Until.findObject(By.descContains("今天")), 3000
|
||||
)
|
||||
if (todayCell != null) {
|
||||
todayCell.click()
|
||||
} else {
|
||||
val calRef = safeWaitCalendarPager(3000)
|
||||
if (calRef != null) {
|
||||
val cb = calRef.visibleBounds
|
||||
device.click(cb.centerX(), cb.centerY())
|
||||
}
|
||||
}
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 8. 拖拽 BottomCard 折叠到周视图 ─────────────────────
|
||||
val bottomCard = device.wait(Until.findObject(By.res("bottom_card")), 5000)
|
||||
assertNotNull("BottomCard 必须存在", bottomCard)
|
||||
val bcBounds = bottomCard!!.visibleBounds
|
||||
val cx = bcBounds.centerX()
|
||||
val cy = bcBounds.centerY()
|
||||
val dragDist = (bcBounds.height() * 0.4).toInt()
|
||||
device.drag(cx, cy, cx, cy - dragDist, 20)
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 9. 周视图左右翻页 ──────────────────────────────────
|
||||
val weekPager = safeWaitCalendarPager(3000)
|
||||
assertNotNull("周视图 CalendarPager 必须存在", weekPager)
|
||||
weekPager!!.swipe(Direction.LEFT, 0.5f)
|
||||
device.waitForIdle()
|
||||
weekPager.swipe(Direction.RIGHT, 0.5f)
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 10. 拖拽 BottomCard 展开回月视图 ─────────────────────
|
||||
device.drag(cx, cy - dragDist, cx, cy, 20)
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 11. 切换"显示调休"OFF ────────────────────────────────
|
||||
val fab3 = safeFindFab()
|
||||
assertNotNull("FAB 必须存在(关闭调休)", fab3)
|
||||
fab3!!.click()
|
||||
device.waitForIdle()
|
||||
val legalHolidayOff = device.wait(
|
||||
Until.findObject(By.text("显示调休")), 3000
|
||||
)
|
||||
assertNotNull("显示调休必须出现", legalHolidayOff)
|
||||
legalHolidayOff!!.click()
|
||||
device.waitForIdle()
|
||||
|
||||
// ── 12. CalendarPager 左右翻页 ──────────────────────────
|
||||
val mainPager = safeWaitCalendarPager(5000)
|
||||
if (mainPager != null) {
|
||||
mainPager.swipe(Direction.LEFT, 0.5f)
|
||||
device.waitForIdle()
|
||||
safeWaitCalendarPager(3000)?.swipe(Direction.RIGHT, 0.5f)
|
||||
device.waitForIdle()
|
||||
}
|
||||
|
||||
Log.d(TAG, "Baseline Profile / Startup Profile 生成完成,所有路径已覆盖")
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user