UI 组件定位与还原
三个流程:A 定位组件、B 1:1 还原、C 改现有 UI。
| 用户说 | 走 |
|---|---|
| "这效果用什么组件""有没有现成的""这是啥组件"(配图) | A |
| "按设计稿还原""1:1 对齐""还原度不够" | B |
| "把那个按钮改成…""圆角小一点""这个间距不对" | C |
铁律
- 数值不能凭手感。 每个 dp/sp 必须能说出从哪来:设计稿实测(A)、比例推算(B)、M3 默认(C)。三者之外没有第四种,C 档必须注明"默认值"。详见
references/design-rules.md。 - 端判断错了,后面全错。 宁问一次,别猜;问完记住,同一轮不再问。
- 不编 API。 索引表里没有的组件,先确认存在再报。版本敏感的 API(
SearchBar、CircularProgressIndicator)先确认 material3 版本。 - 没设计稿就没有还原,只有实现。 用户说"按设计稿还原"却拿不出图时,先要图,别用默认值假装还原。
- 禁止空词。 不写"更美观""更现代""有呼吸感"。要么给数值,要么不给建议。
- 别凭文件名猜脚本能力。 不确定支持哪些格式就跑
--help或看源码里的常量。scan_ui.py支持.kt/.dart/.swift,diff_ui.py和语言完全无关——它们不是 Android 专用的。
先定端
命中即停:
| 顺序 | 信号 | 动作 |
|---|---|---|
| 1 | 用户明说("安卓端""iOS""小程序") | 直接采信 |
| 2 | 贴了代码 | 看 import,比项目文件还准 |
| 3 | 有项目目录 | python scripts/detect_platform.py <路径> |
| 4 | 都没有 | 问一次,给编号选项 |
脚本三档置信度:high 直接用、medium 说一句"按 X 处理"、low 问一句。
必须知道的坑:
- 看到
AndroidManifest.xml不等于 Android——Flutter/RN 项目的android/是宿主壳,脚本已降权。 - KMP 项目 Compose 代码共享,平台 API 走
expect/actual,别按纯 Android 报。 - 中文术语「组件/页面/界面」不分端;「控件」偏 Android、「视图」偏 iOS 但都不可靠;「前端」在有些团队指 App 客户端。
各环节的端支持
| 环节 | Compose | Flutter | SwiftUI | Web / 小程序 |
|---|---|---|---|---|
| 判端 | ✅ | ✅ | ✅ | ✅ |
| 组件索引 | ✅ | ✅ | ✅ | 部分(12 条高频) |
| 代码模板 | compose-m3.md |
flutter-dart.md |
无专属,靠索引表 | 无专属,靠索引表 |
| UI 索引扫描 | .kt |
.dart |
.swift |
— |
| 截图 | adb | adb(Android 端) | 手动 | 手动 |
| 量化比对 | ✅ | ✅ | ✅ | ✅ |
这个表是为自己看的:判完端先来对一眼,别拿 Compose 的模板去答 Flutter 项目。SwiftUI / Web 没有专属模板文件,代码靠索引表 + 当场写。
A. 定位组件
A1. 拆成意图三元组
别直接拿词去搜。先拆三件事,形态相近但意图不同的组件靠这个区分:
| 维度 | 问什么 | 例子 |
|---|---|---|
| 交互意图 | 临时操作 / 常驻对照 / 选一项 | ModalBottomSheet(看完就关)vs BottomSheetScaffold(边看边操作) |
| 视觉形态 | 从哪出、占多少屏、有无遮罩 | 抽屉 / 面板 / 弹窗 / 内联 |
| 触发方式 | 点击 / 长按 / 滑动 / 滚动 | 手势组件 vs 状态驱动 |
A2. 检索
grep -n -i -E "抽屉|弹出" references/intent-index.md
搜碎片,不搜整句:把用户原话拆成「动词 + 名词」各搜一次。两次搜不到就是该端无内置 → 转 references/compose-m3.md 的手搓方案,给最小骨架,别硬套。
A3. 回答格式(五段,缺一不可)
组件:ModalBottomSheet(Compose Material3)
理由:底部滑出 + 遮罩 + 临时选择的标准解,自带拖拽关闭
[能编译的最小代码]
坑:
- 要半截高度必须 skipPartiallyExpanded = false
- 关闭带滑出动画要 sheetState.hide()
备选:需要常驻、可与主界面同时操作 → BottomSheetScaffold
代码模板按端选:Compose → references/compose-m3.md,Flutter → references/flutter-dart.md。SwiftUI / Web 没有专属模板,照索引表当场写。
取色取字一律走主题(Compose MaterialTheme.colorScheme,Flutter Theme.of(context).colorScheme),不硬编码 hex。尺寸间距标来源。
A4. 给图认组件
比 intent-index.md 末尾的形态识别表。拿不准就给「最可能的 + 两个相似的」,各附一句区分依据。
B. 1:1 还原
先量化,再动手。 肉眼判断"间距不对"改完还是不对。
B0. 体检(可选)
扫圆角/间距/硬编码颜色的分布,提前发现「怎么对都对不上」的根因。
# Compose
grep -rho "RoundedCornerShape([0-9]*\.dp)" --include="*.kt" . | sort | uniq -c | sort -rn
# Flutter
grep -rho "BorderRadius.circular([0-9.]*)" --include="*.dart" . | sort | uniq -c | sort -rn
注意:统计异常 ≠ 漂移。 宽 28、圆角 14 是半宽胶囊,改 12 会露直角。先逐处看用途,再决定动不动。
B1. 取基准
优先:MasterGo MCP > 设计稿原文件 > 用户贴图。
get_selection_node(projectDir, targetNodeId) → 图层宽高/颜色/字体(真实数值)
get_variables(projectDir) → 设计 token
get_screenshot(projectDir, targetNodeId, scale=2) → 落盘 PNG
拿到数值后、写代码前,先出「数值来源表」(模板见 references/design-rules.md),逐项标 A/B/C。这张表要交付给用户看。
B2. 取现状
python scripts/capture_android.py -o build/ui/current.png # Android
打印出的设备 dp 宽直接喂下一步。其他端手动截图到同路径。截图状态必须一致:同滚动位置、同数据、无键盘、无动画中间帧。
B3. 量化
python scripts/diff_ui.py design.png build/ui/current.png -o build/ui/diff \
--design-width 411 --device-width 411
零依赖,纯标准库解 PNG。输出全局差异、合并后的差异区域、差异点位、热力图、report.json。
判定:<2% 一致 / 2~8% 微调 / 8~20% 明显不符 / >20% 没对齐。
两轮法:粗网格(默认 24 列)抓系统性错误 → 细网格(--cols 72)抓 1~3dp 位移。粗网格块约 30-40px,小于块宽 1/3 的位移检不出来,第一轮干净不代表细节对。
B4. 归因
差异形态比数值有信息量,完整表见 references/restore-flow.md:
| 形态 | 大概原因 |
|---|---|
| 横向带 | 整体/整行位移、行内 padding |
| 纵向条 | 边距、圆角、分割线 |
| 块状 | 元素尺寸 / 色值 / 字重 |
| 大面积色差形状不变 | token 用错,或暗色模式没适配 |
| 文字块高度差几像素 | lineHeight,不是 fontSize |
先修系统性错误,80% 的差异来自 2~3 个系统性问题。
B5. 复测
改完重跑,看 mismatch_pct 是否收敛,目标 < 2%。
C. 改现有 UI
价值全在"快",别让用户等你去翻整个项目。
C1. 定位到行
文案是最强锚点:同一个 Button 一个文件里可能五个,但「保存」两个字通常只有一处。
python scripts/scan_ui.py <项目> --query "保存"
AddPasswordScreen.kt:50 AddPasswordScreen
文案: 取消@76 / 保存@89 / 标题 *@111
组件: Scaffold@71, TopAppBar@73, TextButton@76
没文案可锚就按组件类型或页面名 grep。
扫描器认 .kt / .dart / .swift 和 res/layout 下的 xml。Flutter 认 extends StatelessWidget|StatefulWidget,State 类的内容会算进对应的 Widget。
C2. 只读那一段
拿到行号后用 offset/limit 卡上下 20~30 行。一个 Screen 动辄四五百行,整文件读既费上下文又容易看花。
C3. 查影响面(改之前必做)
grep -rn "被改的组件名(" --include="*.kt" . # Compose
grep -rn "被改的组件名(" --include="*.dart" . # Flutter
一处引用直接改;多处引用改默认参数会波及全部,改成新增可选参数。
注意把定义行本身减掉:grep 会同时命中 class TaskCard / const TaskCard({...}) 这类声明,那不是调用点。
C4. 改哪个属性
见 references/design-rules.md 的定位表:「圆角太大」→ shape =;「太灰」→ 换 colorScheme token;「太挤」→ padding/spacedBy。
C5. 交代四件事
改了 AddPasswordScreen.kt:89
内容:Text("保存") 外层 TextButton → Button
范围:只此一处,未被其他文件调用
影响:在 TopAppBar 的 actions,宽度变大可能挤压标题
禁止只回一句"已经改好了"。
资源
| 文件 | 内容 | 何时读 |
|---|---|---|
references/intent-index.md |
口语 → 多端组件映射(165 条)+ 形态识别表 | 流程 A |
references/compose-m3.md |
Compose 参数骨架、版本坑、手搓方案 | 出 Compose 代码前 |
references/flutter-dart.md |
Flutter 骨架、特有坑、手搓方案 | 出 Flutter 代码前 |
references/design-rules.md |
数值来源三档、正反例、改 UI 定位表 | 出数值 / 流程 C |
references/restore-flow.md |
px→dp 换算、归因表、报告模板 | 流程 B |
scripts/detect_platform.py |
判端(目录 / 代码片段) | A 第一步 |
scripts/scan_ui.py |
UI 索引,按文案定位到行 | C1 |
scripts/diff_ui.py |
网格色差比对 + 热力图 | B3 |
scripts/capture_android.py |
adb 截图 + 设备 dp 宽 | B2 |