From 4eadc479eb206119f7b2ba3321a349ee0a5ea6f8 Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 27 May 2026 10:03:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20testTag=20?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=EF=BC=8C=E6=89=A9=E5=B1=95?= =?UTF-8?q?=20Baseline=20Profile=20=E8=A6=86=E7=9B=96=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为 CalendarPager、BottomCard、FAB、工具页、日期检查器等关键 UI 元素添加 testTag, 启用 testTagsAsResourceId 支持 UI Automator 通过 resource-id 定位。 BaselineProfileGenerator 使用 testTag 选择器重写,覆盖工具页、日期检查器、 DatePicker 等更多导航路径。清理 DEVELOPMENT.md 移除已过时的性能瓶颈描述。 Co-Authored-By: Claude Opus 4.7 --- DEVELOPMENT.md | 38 ++------- .../kotlin/plus/rua/project/ui/BottomCard.kt | 2 + .../plus/rua/project/ui/CalendarMonthView.kt | 7 +- .../plus/rua/project/ui/CalendarPager.kt | 3 +- .../plus/rua/project/ui/DateCheckerScreen.kt | 11 ++- .../kotlin/plus/rua/project/ui/DayCell.kt | 6 +- .../kotlin/plus/rua/project/ui/ToolsScreen.kt | 8 +- .../plus/rua/project/ui/YearGridView.kt | 5 +- .../baseline/BaselineProfileGenerator.kt | 77 ++++++++++++++++--- 9 files changed, 106 insertions(+), 51 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 4272a49..4cd8ece 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -17,47 +17,21 @@ 输出文件保存在 `logs/` 目录: -| 文件 | 说明 | -|------|------| +| 文件 | 说明 | +| ------------------------ | ----------------------------------------------- | | `trace_*.perfetto-trace` | Perfetto trace,在 https://ui.perfetto.dev 打开 | -| `framestats_*.txt` | GPU 帧统计 | -| `meminfo_*.txt` | 内存快照 | -| `report_*.md` | 追踪报告摘要 | +| `framestats_*.txt` | GPU 帧统计 | +| `meminfo_*.txt` | 内存快照 | +| `report_*.md` | 追踪报告摘要 | trace 中包含自定义标记: + - `MonthView:Compose` — 月视图重组 - `YearView:Compose` — 年视图重组 - `YearGridView:*` — 年视图网格组合(首帧耗时关键指标) - `generateMiniMonthDays:*` — 月份网格计算 - `VM:collapseProgress` — 折叠动画 -### 已知性能瓶颈 - -#### AnimatedGif 持续解码 - -`AnimatedGif` 中的 250×250 WebP 动画在 `repeatCount` 未限制时会以 11-14 FPS 无限循环解码, -持续消耗 CPU/GPU。已通过 `ImageOptions { repeatCount(2) }` 限制播放 3 次后停止。 - -#### YearGridView 首帧 168ms - -切换到年视图时,`YearGridView` 首次组合需要: -- 创建 12 个 MiniMonth composable(含 `sharedElement` + `clickable` + `Canvas` 等 modifier 节点) -- 124 次文本测量(93 日期 + 24 标题 + 7 星期) -- `SharedTransitionLayout` 注册 12 个共享元素 - -文本测量已通过 `produceState` 延迟到第二帧执行,首帧 Canvas 渲染为空。 -剩余 ~140ms 是 12 个 composable 节点创建 + layout 的 Compose 运行时开销, -需要通过 **Baseline Profile** 预编译相关类来优化。 - -确保 `macrobenchmark` 的 `BaselineProfileGenerator` 覆盖以下路径: -- 冷启动 → FAB → 年视图(触发 YearGridView 首次组合) -- 年视图 → 点击任意月份返回月视图(触发 sharedElement 转场) - -#### CalendarPager 预加载 - -`beyondViewportPageCount` 已设为 0,避免翻页时预加载相邻月页导致的帧丢失。 -如需恢复预加载,注意 `compose:lazy:prefetch` 可能产生 400-700ms 卡顿。 - ## Baseline Profile ```bash diff --git a/core/src/main/kotlin/plus/rua/project/ui/BottomCard.kt b/core/src/main/kotlin/plus/rua/project/ui/BottomCard.kt index 3363d26..426348e 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/BottomCard.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/BottomCard.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.contentDescription @@ -84,6 +85,7 @@ fun BottomCard( Surface( modifier = modifier .fillMaxWidth() + .testTag("bottom_card") .pointerInput(isCollapsed) { if (isCollapsed) { // 折叠状态:下拉恢复到月视图 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 e50d8b9..685b034 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -61,6 +61,9 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color @@ -177,6 +180,7 @@ fun CalendarMonthView( .fillMaxSize() .background(MaterialTheme.colorScheme.background) .statusBarsPadding() + .semantics { testTagsAsResourceId = true } .onSizeChanged { size -> screenWidthPx = size.width } @@ -377,7 +381,8 @@ fun CalendarMonthView( onClick = { isMenuExpanded = !isMenuExpanded }, modifier = Modifier .align(Alignment.BottomStart) - .padding(start = 24.dp, bottom = 32.dp), + .padding(start = 24.dp, bottom = 32.dp) + .testTag("fab_menu"), shape = CircleShape, containerColor = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer 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 5d9d6f6..21168c5 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/CalendarPager.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/CalendarPager.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import plus.rua.project.util.logd import androidx.compose.ui.draw.alpha import kotlinx.coroutines.flow.drop @@ -91,7 +92,7 @@ fun CalendarPager( state = pagerState, beyondViewportPageCount = 0, flingBehavior = PagerDefaults.flingBehavior(state = pagerState), - modifier = modifier + modifier = modifier.testTag("calendar_pager") ) { page -> val pageOffset = abs(currentPageOffsetFraction) val isCurrentPage = page == currentPage diff --git a/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt b/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt index 9ce9815..5412f1a 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/DateCheckerScreen.kt @@ -45,6 +45,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.text.input.ImeAction @@ -97,6 +100,7 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) { var datePickerTarget by remember { mutableStateOf(null) } Scaffold( + modifier = modifier.semantics { testTagsAsResourceId = true }, topBar = { TopAppBar( title = { Text("日期检查器") }, @@ -113,6 +117,7 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) { rows = rows + ExpiryRow(nextId, null) nextId++ }, + modifier = Modifier.testTag("date_checker_fab"), shape = CircleShape, containerColor = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer @@ -120,7 +125,6 @@ fun DateCheckerScreen(onBack: () -> Unit, modifier: Modifier = Modifier) { PlusIcon(color = MaterialTheme.colorScheme.onPrimaryContainer) } }, - modifier = modifier ) { innerPadding -> Column( modifier = Modifier @@ -261,7 +265,10 @@ private fun ProductionDateField( imeAction = ImeAction.Done ), trailingIcon = { - IconButton(onClick = onShowDatePicker) { + IconButton( + onClick = onShowDatePicker, + modifier = Modifier.testTag("date_picker_button") + ) { CalendarIcon(color = MaterialTheme.colorScheme.onSurfaceVariant) } }, diff --git a/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt b/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt index e64b9d1..ee38936 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt @@ -243,7 +243,11 @@ private fun DayCellImpl( .fillMaxSize() .semantics { @Suppress("DEPRECATION") - contentDescription = "${date.year}年${date.monthNumber}月${date.day}日" + contentDescription = if (isToday) { + "今天 ${date.year}年${date.monthNumber}月${date.day}日" + } else { + "${date.year}年${date.monthNumber}月${date.day}日" + } } .clip(CircleShape) .drawBehind { diff --git a/core/src/main/kotlin/plus/rua/project/ui/ToolsScreen.kt b/core/src/main/kotlin/plus/rua/project/ui/ToolsScreen.kt index 8335906..b54cc56 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/ToolsScreen.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/ToolsScreen.kt @@ -22,6 +22,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.unit.dp /** @@ -39,6 +42,7 @@ fun ToolsScreen( modifier: Modifier = Modifier ) { Scaffold( + modifier = modifier.semantics { testTagsAsResourceId = true }, topBar = { TopAppBar( title = { Text("工具") }, @@ -66,7 +70,6 @@ fun ToolsScreen( } ) }, - modifier = modifier ) { innerPadding -> Column( modifier = Modifier @@ -76,7 +79,8 @@ fun ToolsScreen( ) { ToolItem( title = "日期检查器", - onClick = onNavigateToDateChecker + onClick = onNavigateToDateChecker, + modifier = Modifier.testTag("tool_date_checker") ) } } 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 e9765b8..b3c773c 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/YearGridView.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/YearGridView.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.ui.platform.testTag import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -155,7 +156,9 @@ fun YearGridView( } Column( - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .testTag("year_grid"), horizontalAlignment = Alignment.CenterHorizontally ) { // 4×3 月历网格 diff --git a/macrobenchmark/src/main/java/plus/rua/project/baseline/BaselineProfileGenerator.kt b/macrobenchmark/src/main/java/plus/rua/project/baseline/BaselineProfileGenerator.kt index 432fe0d..dbd3138 100644 --- a/macrobenchmark/src/main/java/plus/rua/project/baseline/BaselineProfileGenerator.kt +++ b/macrobenchmark/src/main/java/plus/rua/project/baseline/BaselineProfileGenerator.kt @@ -91,25 +91,80 @@ class BaselineProfileGenerator { device.waitForIdle() } - // 8. 上下滑动触发月视图↔周视图切换(覆盖 BottomCard 拖拽 + collapse 动画) - val calendarArea = device.findObject(By.res("plus.rua.project:id/calendar_pager")) - ?: device.findObject(By.textContains("2026")) - if (calendarArea != null) { - calendarArea.swipe(Direction.UP, 0.5f) + // 8. 拖拽 BottomCard 触发月视图↔周视图折叠/展开 + val bottomCard = device.findObject(By.res("plus.rua.project:id/bottom_card")) + if (bottomCard != null) { + val bounds = bottomCard.visibleBounds + val centerX = bounds.centerX() + val centerY = bounds.centerY() + val dragDistance = (bounds.height() * 0.4).toInt() + // 向上拖拽 → 折叠到周视图 + device.drag(centerX, centerY, centerX, centerY - dragDistance, 20) device.waitForIdle() - calendarArea.swipe(Direction.DOWN, 0.5f) + // 向下拖拽 → 展开到月视图 + device.drag(centerX, centerY - dragDistance, centerX, centerY, 20) device.waitForIdle() } - // 9. 左右滑动切换月份(覆盖 CalendarPager 翻页) - if (calendarArea != null) { - calendarArea.swipe(Direction.LEFT, 0.5f) + // 9. 展开 FAB 并进入工具页面 + val fabMenu = device.findObject(By.res("plus.rua.project:id/fab_menu")) + if (fabMenu != null) { + fabMenu.click() device.waitForIdle() - calendarArea.swipe(Direction.RIGHT, 0.5f) + } + val toolsButton = device.findObject(By.text("工具")) + if (toolsButton != null) { + toolsButton.click() device.waitForIdle() } - // 10. 进入关于页面(覆盖 AboutScreen + AnimatedGif) + // 10. 进入日期检查器(覆盖 DateCheckerScreen) + val dateCheckerEntry = device.findObject(By.res("plus.rua.project:id/tool_date_checker")) + if (dateCheckerEntry != null) { + dateCheckerEntry.click() + device.waitForIdle() + } + + // 11. 点击日历图标打开 DatePickerDialog(覆盖 DatePicker) + val datePickerBtn = device.findObject(By.res("plus.rua.project:id/date_picker_button")) + if (datePickerBtn != null) { + datePickerBtn.click() + device.waitForIdle() + } + + // 12. 等待 DatePickerDialog 并点击确定 + device.wait(Until.findObject(By.text("确定")), 2000) + val confirmBtn = device.findObject(By.text("确定")) + if (confirmBtn != null) { + confirmBtn.click() + device.waitForIdle() + } + + // 13. 点击 FAB 添加新行(覆盖 FAB + LazyColumn items 重组) + val dateCheckerFab = device.findObject(By.res("plus.rua.project:id/date_checker_fab")) + if (dateCheckerFab != null) { + dateCheckerFab.click() + device.waitForIdle() + } + + // 14. 返回工具页 + device.pressBack() + device.waitForIdle() + + // 15. 返回主界面 + device.pressBack() + device.waitForIdle() + + // 16. 左右滑动切换月份(覆盖 CalendarPager 翻页) + val calendarPager = device.findObject(By.res("plus.rua.project:id/calendar_pager")) + if (calendarPager != null) { + calendarPager.swipe(Direction.LEFT, 0.5f) + device.waitForIdle() + calendarPager.swipe(Direction.RIGHT, 0.5f) + device.waitForIdle() + } + + // 17. 进入关于页面(覆盖 AboutScreen + AnimatedGif) val aboutButton = device.findObject(By.text("关于")) if (aboutButton != null) { aboutButton.click()