使用 DeepSeek V4.1 Flash 对 chhongzh/osu-mc 项目的UI/UX,做 1:1 复刻。在 HarmonyOS 系统上体验这个丝滑Q弹的esp32 UI。
当前访问频次受限,请登录后继续访问
osu!mc for HarmonyOS(osu-mc-hmos)
把 ESP32-S3 + 128×64 单色 OLED 上的 osu!mc(一个界面仿 osu!(lazer) 的单片机小项目) 1:1 重建为 HarmonyOS ArkTS 应用(模拟器 / 真机形态)。
- 范围:只复刻界面、全部动画与交互;Wi-Fi / HTTP / System Info 用假数据填充。
- 画法:屏幕不用原生 ArkUI 组件重写,而是软件光栅化——自建 1-bit 帧缓冲 → Canvas 逐像素 → 最近邻放大。
- 目标:常量、时序、边界行为与原项目一致(比特级一致)。
复刻已经完成。这是一套可复用的 osu!mc UI/UX 运行时:以后如果要做真实业务, 只需把这套 UI 接到真实的服务实现上(见下方「把它当成业务底座」)。
目录
文档导航
本文件只提供整个项目的缩略图;具体内容都在下面的专属文档里,按需进入。
| 文档 | 面向谁 | 内容(职责) |
|---|---|---|
README.md(本文件) |
人(项目入门) | 项目是什么、长什么样、按键语义、外壳概览、构建运行、目录结构——只做缩略图与导航 |
AGENTS.md |
改代码的 AI/agent | 精简工作指引:五条设计思路、实现坑、事件/主循环、常量表、里程碑状态——细节一律指向下方文档 |
docs/harmonyos-port-plan.md |
所有人(复刻权威) | 全部复刻细节:逐文件映射、逐类要点、常量、页面内容、验证方法、有意偏离、业务接入 |
docs/arkui-app-guide.md |
改外壳/设置的人 | 与复刻无关:Navigation 与系统路由表、应用设置页、MVVM 数据分层(为 SQLite 预留)、按键振动、沉浸式与主题资源 |
tools/exportfont/README.md |
需重新生成字体的人 | 点阵导出工具的用法与位流格式 |
tools/fixglyph/README.md |
需修正/扩展难辨字形的人 | 字形可读性修正工具的用法与偏差说明 |
一句话分工:细节看
harmonyos-port-plan.md,动外壳/设置看arkui-app-guide.md, 动代码前看AGENTS.md,人类入门看本文件。
它长什么样
- 屏幕:
128×64、1 bit/像素、固定黑底白字(OLED 语义),不走 ArkUI 主题。 由自建帧缓冲逐像素绘出,再按窗口可用的最大整数倍最近邻放大(128×64 → 例如 6× 即 768×384 px)。 放大倍数的上限可在设置页用滑块调整(默认为最大值,即自动取最大可用整数倍)。 只能是整数倍:非整数倍会让最近邻放大把源像素摊成大小不一的方块,肉眼看到的就是帧错位。 - 外壳:应用标题与 7 个按键是 ArkUI 原生控件,标签走
resources+$r, 浅色 / 深色(base/dark)两套资源;按键按下会振动一次(强度可在设置页调)。 - 屏内页面(1:1 复刻):Launcher(
osu!mc+ 四项)、Settings(Wi‑Fi / Screen off / About)、 Lorem Ipsum(搜索 → 歌曲 → 难度 → Options + 阅读器)、HTTP Client、System Info、三层软键盘。 每一页的具体内容与动感清单见方案文档 §19。
屏幕下方的 7 个按键
这 7 个按钮是 ArkUI 原生按钮,用来模拟真机的 6 个按键 + 1 个复位键外壳;
它们的映射关系与原项目 cmd/osu-mc/osu-mc.go 的按键定义完全一致。
排布(屏幕正下方,自上而下):
[ RST ] [ SET ] ← RST 在左 / SET 在右,两端对齐
[ UP ]
[ LEFT ] [ MID ] [ RIGHT ] ← 十字键
[ DOWN ]
- 屏幕按窗口可用的最大整数倍放大(
chooseScale();回退 6×、上限 10×);设置页的 「屏幕倍数」滑块是上限(默认 = 最大),窗口放不下时自动缩小,横竖屏都不会溢出。 - 横竖屏自适应:应用跟随系统传感器旋转(
module.json5里orientation: "auto_rotation", 需系统未锁定旋转)。竖屏就是上面这张"上下排";横屏改成左右排——左栏是标题 + 副标题 + 屏幕,右栏是按键,两栏各占一半宽度并垂直居中。旋转时放大倍数与排布都会重算。 RST是复位键,用警示红(文字 / 边框 / 背景三色资源key_danger_fg/key_danger_border/key_danger_bg),浅色与深色各一套;其余 6 个键仍是中性色(key_bg/key_fg)。
先说结论:MID 是确认、SET 是返回、RST 是回主页。 逐个说明如下。
- UP(
KeyUp,可重复)- 在列表里把选择上移一行。
- 到顶后继续按住,会让内容 + 游标反向偏移 6px(peek),松开弹回。
- DOWN(
KeyDown,可重复)- 把选择下移一行;与 UP 对称的边界行为。
- LEFT(
KeyLeft,可重复)- 在横向行(
Row,如 Options 的 Mods / 倍速行)里向左移动 / 滚动。 - 在行首按左键会向上冒泡到
Viewer返回上一页;由于「按下者独占后续 Repeat」, 按住左键不会连弹多层页面。
- 在横向行(
- RIGHT(
KeyRight,可重复)- 在横向行里向右移动 / 滚动。
- MID(
KeyEnter,不重复)= 确认- 执行当前选中项:打开页面、按下软键盘的按键、勾选
Checkbox/Chip、提交输入框(OK)等。 - 关键点:确认动作在松开时才执行(
Fires() = Repeat || (Release && !Repeated)), 按下去只给「游标倾斜 / 挤压」的反馈。
- 执行当前选中项:打开页面、按下软键盘的按键、勾选
- SET(
KeyBack,不重复)= 返回- 按住时当前页向右移动 8px 做 peek(根页 4px),松开才真正 pop 上一页。
- RST(无键码,单独处理)= 回主页
- 松开时调用
PopToRoot(),一次弹到栈底直接回主页(不是退一层)。 - 关屏时的唤醒由任意键触发,被唤醒的那一次按键(含 RST)会被吞掉, 所以唤醒不会顺带确认或返回。
- 松开时调用
应用外壳:导航 / 设置页 / 沉浸式
这一层是通用 App 外壳,与「128×64 屏幕的 1:1 复刻」无关(屏幕渲染一行没改)。 完整设计见
docs/arkui-app-guide.md。
-
导航:首页是 Navigation 根页(
HdsNavigation+NavigationMode.Stack), 标题栏不带标题文字,只保留右侧设置菜单(横屏时不挤占内容宽度)。 子页面走系统路由表(resources/base/profile/router_map.json+RouteName), 用pushPathByName跳转——新增页面不必改首页。 -
设置页(路由
Settings):七张卡片——外观(深浅色)、标题栏模糊、沉浸光感、 按键振动(关闭 / 弱 / 中 / 强,默认「弱」)、屏幕旋转、屏幕倍数(原生Slider,整数 2–10)、 关于(版本号取自bundleManager,不硬编码)。 卡片底色是浅灰#E8EAED/ 深灰#2A2A2A(settings_card_bg),分段控件用原生CapsuleSegmentButtonV2(状态管理 V2)、保持系统默认配色(未照搬参考项目的蓝灰底与自定义配色)。 -
状态管理统一为 V2(
@ComponentV2/@Local/@Param/@Event/@Monitor/@ObservedV2/@Trace):没有 V1 的@State/@StorageLink/AppStorage。 -
数据分层(MVVM,为 SQLite 预留):
View(SettingsPage · @Local 快照) └─→ ViewModel(SettingsViewModel · 编排读写 + 发布到 AppUiState + 系统生效) └─→ Repository(SettingsRepository · 业务语义,字段 ↔ 通用 KV 键) └─→ KeyValueStore(通用 KV 接口) ├── PreferencesKeyValueStore(现在:首选项文件 osu_mc_store) └── RdbKeyValueStore(以后:SQLite,只需改一行装配)持久化落
Preferences,运行时快照放AppUiState单例(@ObservedV2+@Trace, 首页@ComponentV2直接读字段即响应式刷新,设置页改动回到首页即时生效)。 以后换 SQLite 只需新增一个KeyValueStore实现, 并在唯一装配点createSettingsRepository()换一行。 -
沉浸式(组件级,不做窗口级全屏):页面背景用
expandSafeArea延伸到状态栏 / 导航条;标题栏支持滚动动态模糊(bindToScrollable)与沉浸光感; 首页 7 个按键用.systemMaterial(...)拿系统沉浸材质。状态栏文字颜色跟随深浅色。 -
按键振动:按下 7 个按键时给一次短振(模拟实体按键按下去的手感),强度在设置页调 (关闭 / 弱 / 中 / 强,默认「弱」)。用系统预置振动效果(
VibratePreset):设备支持高清振动时用intensity分档,否则用soft/hard/sharp三档;需要ohos.permission.VIBRATE(已声明)。实现见docs/arkui-app-guide.md§5.2。 -
平台版本:
targetSdkVersion=compatibleSdkVersion=26.0.0—— 沉浸光感所用的uiMaterial/systemMaterial自 API 26 起提供。
容易误解 / 语义不明确的地方
MID为什么是「确认」、SET为什么是「返回」?MID/SET是硬件按键的丝印名(原项目里KBD_MID_PIN = GPIO3、KBD_SET_PIN = GPIO46), 原作者沿用了板子上的叫法,再把它映射成KeyEnter/KeyBack。MID(middle,方向盘 / 摇杆的中间键)当确认键其实很自然;真正拗口的是把SET(设置键)当「返回」 ——因为板子上没有单独的 Back 键。- 这不是移植引入的问题,原项目就是这样;为保持 1:1,这里照搬,不做「重新设计」。
RST是「复位」,但不是重启芯片。- 硬件上它是 GPIO9 上的一个普通按键(不是 ESP32 的芯片复位 / EN 脚),固件像其他键一样
去抖轮询它,且键码为
0(不产生任何 UI 键事件)。 - 功能上的「复位」= 把界面复位回主页,对应
PopToRoot(),不会重启芯片。 - 原项目 README 把它和 6 个键并列描述为「六个按键加一个复位键」。
- 硬件上它是 GPIO9 上的一个普通按键(不是 ESP32 的芯片复位 / EN 脚),固件像其他键一样
去抖轮询它,且键码为
- 按下 ≠ 执行。
- 事件是三态
Press/Repeat/Release;按下只给反馈(游标倾斜 / 挤压、页面 peek), 确认、返回、打开键盘都在松开时执行。
- 事件是三态
- 屏幕是「软件光栅化」,按钮才是原生控件。
- 上方 128×64 单色画面由自建帧缓冲逐像素画出(黑底白字,1 bit/像素,不走 ArkUI 主题), 再按整数倍最近邻放大到画布上;
- 下方 7 个按钮是 ArkUI 原生按钮,仅作外壳。
- 显示名与模块名不一致。
- 界面上显示一律是
osu!mc;路径 / 模块名用osu-mc(!不能出现在路径里)。
- 界面上显示一律是
- 屏幕上的感叹号
!是刻意改过的。- 原固件字体把
!画成一条没有点的实心竖线,与l/|几乎难分;移植版用tools/fixglyph把它改成「竖线 + 点」。这是全项目唯一一处刻意偏离固件的像素 (见方案 §7.4)。
- 原固件字体把
- Wi-Fi / HTTP / System Info 是假数据。
application/MockServices.ets提供MockWiFi/MockHTTP/MockPower/MockSystem, 不做任何硬件访问;其中 System Info 的Loop/Draw/Busy会像真机一样每秒刷新。
- 光标可以停在文本行上。
Text也实现Focusable,所以在 Wi-Fi 页光标可以停在状态行 / IP 行(与原项目一致), 而不只是「可操作项」。
- 左右键的重复只到达「接管按下」的那个元素。
- 因此按住 LEFT 只会返回一层,不会一路弹回根页。
- 界面文案(如
MID/SET/RST)是对硬件的忠实还原,不是 UI 术语。- 若想改成更「符合语义」的名字(例如
OK/BACK),属于重新设计,与 1:1 目标冲突。
- 若想改成更「符合语义」的名字(例如
架构与技术要点
渲染 = 软件光栅化。 原项目不依赖任何平台 UI 框架:它自己把一切画到一个 1-bit Surface
(只有 SetPixel / InvertRect),字体是内嵌点阵(tinyfont notosans12pt),动画是纯数学。
移植版照此重建,因此可以做到比特级一致:
FrameBuffer(128×64,1 bit)
→ Canvas(surface + clip + origin,值类型;仅五种原语:置位像素/填充矩形/反色矩形/反色圆角矩形/位图画字形)
→ ScreenView(整数倍最近邻放大:128×64 离屏画布 → drawImage(关闭图像平滑)到 ArkUI Canvas)
动画 = 一个全局 Manager 推进所有 Value。 屏幕上每个可见数值(位置、尺寸、滚动、拉扯、
游标倾斜、页面进出……)都是 Value{Current, Target, Speed, Damping, Velocity}:
只改 Target,让弹簧去追;两种运动——指数平滑(帧率无关、永不过冲)与阻尼弹簧
(半隐式欧拉、固定子步长 1/240,带惯性与回弹)。
UI 模型 = 万物皆 ListItem。 没有布局容器:一页就是一个 List,每行是实现 ListItem
的组件;List / Row 本身也是 ListItem,可以互相嵌套。布局按 Target 算、绘制按
Current 算;一个效果只有一个动画源。
事件 = 三态 + 路由。 Press / Repeat / Release;Router 沿 root → focused() 先捕获
后冒泡;按下者独占该键的后续事件;确认 / 返回 / 开键盘都在松开时执行。
主循环 = displaySync 驱动。 每帧 dt = min(真实时间差, 0.1)(必须用真实时间,弹簧才
帧率无关);viewer.update(dt) 设置所有目标 → Default.update(dt) 推进动画 → 只有
发生变化时才重绘并上传。dt、时序、常量都与原固件逐一对齐。
逐文件映射、逐类要点、全部常量、有意偏离,都在
docs/harmonyos-port-plan.md。
开发约定与注意事项
这是翻译,不是创作。 常量、时序、边界行为必须与原项目一致;不要「重新设计」。
- 不要用原生 ArkUI 组件重写屏幕。
Column/Text/Swiper/animateTo只能做到 「神似」;屏幕必须走软件光栅化。外壳(标题、按键)以及以后新增的、与屏幕无关的 ArkUI 部分, 才是原生组件的地盘。 - 外壳与新增 ArkUI 部分:一律「鸿蒙原生组件 + 状态管理 V2」。
- 状态用 V2:
@ComponentV2/@Local/@Param/@Event/@Monitor,跨页共享用@ObservedV2+@Trace单例;不要再用@State/@StorageLink/AppStorage。 - 界面用原生组件,样式 / 动画 / 材质 / 无障碍保持系统默认行为,颜色与文案走
$r。 - 非必要不自造自定义控件:原生能表达的就不自己画;确需封装也只能建立在原生组件之上。
- 状态用 V2:
- 不要直接写
Current(会跳变)。只改Target。例外只有三种:子元素被父元素「携带」、Snap()/Jump()、以及设定动画的起点(页面进场、新插入项从尺寸 0 展开)。 - ArkTS 约束(本项目大量使用,务必遵守):
- 不能用
any/unknown;不能解构;不能用var;函数表达式一律写成箭头函数; - 不能用
Function.call/apply/bind;不能索引签名obj[key];不能for..in遍历对象; - 可选能力不能用
instanceof判断(interface是纯编译期的)——统一改成Element基类上的「空实现方法 +isXxx()能力标志」(详见AGENTS.md§2.1)。
- 不能用
- 命名对应原项目:Go 导出名保持原样,方法用小写驼峰,字段用小写驼峰,
于是
Text.FocusRect()↔text.focusRect()一眼可查。 - 资源与主题:外壳的固定文案进
resources/base/element/string.json并用$r(...)引用, 新增文案要同步所有语言;颜色进color.json,并同时补resources/dark/element/color.json。 屏幕内容自绘,不涉及$r——这是刻意的,1:1 要求自己控制每个像素。 - 缩放只能是整数倍(最近邻放大);非整数倍会产生肉眼可见的「帧错位」。
- 动画性能:不要在动画过程中频繁改
width/height/padding/margin; 复杂子组件的动画可考虑renderGroup(true)(本项目主要靠帧缓冲自绘,此项影响有限)。
把它当成业务底座
复刻阶段所有硬件能力都是假的,但这套屏幕与 UI 结构是完整可复用的。接真实业务时:
- 唯一接缝是
Services(application/Services.ets的接口 + 数据结构)。App与五个页面 只依赖接口,因此只需新写RealWiFi implements WiFi等,替换Index.ets里装进Services的Mock*即可;不要改接口签名。 - 耗时操作走
Job<T>+ 异步(App.ets),UI 侧轮询结果,绝不在帧内阻塞。 - 业务 UI 继续用
List/Row/Text/TextView/Input这套ListItem组件搭, 不要在帧缓冲上混入 ArkUI 组件;只有外壳/新增业务外框才用 ArkUI +$r。 - 详细迁移建议见
docs/harmonyos-port-plan.md§20。
构建与运行
- 用 DevEco Studio(推荐): 拉取项目 → 配置签名 → 直接运行。
- 用命令行
devecocli:
# 进入到hdc命令所在位置 `\Huawei\DevEco Studio\sdk\default\openharmony\toolchains`
## 无线连接设备
hdc tconn <ip>:<port>
## 确认设备已正常连接
devecocli device list
# 进入到本工程目录内
## 检查 ArkTS 语法
devecocli check arkts
## 构建 + 安装 + 启动到已连接设备(--uninstall 先卸载同名旧应用)
devecocli run --device "<设备名>" --uninstall
- 采用 Stage 模型 + ArkTS,
bundleName=cn.forforio.osumchmos(见AppScope/app.json5)。 build-profile.json5的targetSdkVersion/compatibleSdkVersion均为26.0.0: 应用外壳的「沉浸光感」依赖 API 26 起的uiMaterial/.systemMaterial()(见docs/arkui-app-guide.md§7)。删掉相关代码可降低至 API 19 。删掉提示按钮的bindPopup避让功能,可以降低至 API 12 。(未测试)build-profile.json5使用devecocli signature generate --product default生成的调试签名; ⚠️ 该文件内含明文调试签名口令,公开仓库时请自行评估。- 字体点阵表由
tools/exportfont(普通 Go 程序,不需要 TinyGo)从 tinyfont v0.7.0 的notosans12pt导出,再由tools/fixglyph做一次可读性修正(把!改成带点的形状), 可复现。所以首次构建、运行应用不需要 Go 环境 —— 只有需要重新生成字体或生成「黄金帧」比对 语料这两件一次性的事才用得到 Go。
目录结构
entry/src/main/ets/
├── animation/ 动画 Value(指数平滑 / 阻尼弹簧)与全局 Manager
├── application/ 屏内页面(Launcher / 屏内 Settings / Lorem / Http / SysInfo / App)+ MockServices
├── font/ 内嵌点阵字模 Noto12Glyphs(tinyfont notosans12pt,0x20..0x7E)
├── input/ Buttons:按键去抖 + 重复加速
├── pages/ Index(7 键外壳 + Navigation 根页);SettingsPage(应用设置页)
├── screen/ FrameBuffer(1-bit 帧缓冲)+ ScreenView(最近邻放大显示)+
│ Shell(主循环 / 输入分发)+ ScalePolicy(放大倍数策略)
├── ui/ Canvas / Element / Geometry / Event / Router / Cursor / Nav /
│ Item / List / Row / Text / TextView / Input / Select / Checkbox /
│ Chip / Keyboard / Viewer / Font / UiConstants
│
│ —— 以下为「应用外壳层」(ArkUI,与屏幕复刻无关,见 docs/arkui-app-guide.md)——
├── common/ AppTheme / ContextUtil / MaterialUtil / RouteName / TitleBarUtil /
│ VibratorUtil(按键振动:预置效果 + 强度分档)
├── components/ SegmentedControl(通用分段控件)
└── preferences/ AppSettings / AppUiState(V2 运行时状态单例)/ KeyValueStore /
SettingsRepository / DisplayController / SettingsViewModel
tools/exportfont/ 从 tinyfont 导出点阵表的工具(独立 Go module)
tools/fixglyph/ 对导出的点阵表做可读性修正(如 `!`)的工具(独立 Go module)
docs/ 复刻方案(harmonyos-port-plan.md)+ 应用外壳指南(arkui-app-guide.md)
注意区分两个「Settings」:
entry/src/main/ets/application/Settings.ets是屏内 那个 1:1 复刻的设置页(Wi-Fi / Screen off / About);pages/SettingsPage.ets是 应用外壳的设置页(外观 / 标题栏模糊 / 沉浸光感 / 按键振动 / 屏幕旋转 / 屏幕倍数 / 关于)。
验证
- 1:1 对齐:与原项目导出的黄金帧逐行 ASCII 比对(用库里的
FrameBuffer.toString()), Launcher / Settings / About / Lorem / 难度列表 / Options / HTTP 完全一致;键盘页唯一差异是caretBlinkPeriod的闪烁相位(设备用真实 dt、黄金帧用固定 dt),属时间对齐问题,非缺陷。entry/src/test/另有 LocalUnit 纯逻辑测试(Animation.test.ets/Render.test.ets), 在 DevEco Studio 里右键运行;可复现的「黄金帧语料 + 离线 harness」尚未入库 (里程碑 M7 尚未工具化的部分,补法见方案 §14.1)。 - 真机核验:System Info 每秒刷新、关屏与唤醒(唤醒键被吞)、RST 回根页、
Wi‑Fi 扫描 / 密码键盘 / 连接。System Info 的数值与黄金帧不同属预期(移植版用
MockSystem计算,原项目测试用固定fakeSystem)。 - 应用外壳真机核验清单见
docs/arkui-app-guide.md§8 (HUAWEI Mate 60 Pro / API 26)。
各里程碑(M0–M8)的实现状态见
AGENTS.md§7 与docs/harmonyos-port-plan.md§15。
项目来源
本项目的 UI/UX 完全参考自 osu!mc(一个界面仿 osu!(lazer) 的单片机小项目),该项目由 [chhongzh] 以 MIT 许可证发布。本项目的代码是基于其进行的复刻与修改。
项目介绍
使用 DeepSeek V4.1 Flash 对 chhongzh/osu-mc 项目的UI/UX,做 1:1 复刻。在 HarmonyOS 系统上体验这个丝滑Q弹的esp32 UI。
https://github.com/chhongzh/osu-mc定制我的领域