diff --git a/README.md b/README.md index 30df683..4bdd7dc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Octoscript-Android +English | [简体中文](README.zh-CN.md) + ## Shared Octoscript-Makepad runtime `native-runtime.lock.json` selects one diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..ca41711 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,111 @@ +# Octoscript-Android + +[English](README.md) | 简体中文 + +## 共享的 Octoscript-Makepad 运行时 + +`native-runtime.lock.json` 选定一个 +[Octoscript-Makepad](https://github.com/OctoSense-org/Octoscript-Makepad) +发布版本。该版本的 `runtime.json` 负责锁定确切的 Makepad 和 Octoscript 版本, +AppCards、Mail 以及其他 OctoSense 应用共用同一份。 + +构建之前先运行 `python3 tools/setup-native.py`(需要 Python 3.9+)。框架仓库与本应用 +并列存放:`../octoscript-makepad`、`../makepad` 和 `../octoscript`。本地改动会被保留; +`--update` 只更新没有改动的 checkout。CI 会校验所选的发布版本,并拒绝重复的 Makepad 来源。 +用 `python3 tools/setup-native.py --check --cargo-manifest catalog/rust/Cargo.toml` +检查本地依赖图。现有的各平台渲染后端仍然属于各自的应用;框架负责的是共享的 VM 和 UI 源码。 + + +用 Rust 把 Octoscript DSL 渲染成 **Android 原生控件**——它是 +[Octoscript-OH](https://github.com/OctoSense-org/Octoscript-OH/blob/master/README.zh-CN.md) +在 Android 上的对应项目,后者面向 OpenHarmony 的 ArkUI 做同样的事。 + +``` +probe/ the feasibility probe — octoscript-render -> android.widget.*, framework widgets only +catalog/ the Material catalog — 42 screens of Octoscript DSL -> com.google.android.material.* +``` + +即:`probe/` 是可行性验证,只用框架自带控件;`catalog/` 是 Material 组件目录, +42 个 Octoscript DSL 页面渲染为 `com.google.android.material.*`。 + +两者都在真机上运行(OnePlus 6T,Android 11 / SDK 30)。 + +## 整体形态 + +``` +.octoscript ──► makepad-script VM ──► node tree ──► flat buffer ──► Java builder ──► Views + (via octoscript-render; (Rust) ONE JNI call (owns every View) + no makepad renderer) +``` + +**Java 持有每一个 `View`;Rust 只持有整数 id 和卡片状态。** Rust 里从不保存任何 +`jobject`,所以 ART 的 512 个局部引用上限导致的 abort,以及 `FindClass` +的 classloader 陷阱,从结构上就不可能触发。 + +进程里没有任何 makepad 渲染器——没有 `makepad-platform`,没有 `makepad-draw`, +没有 `makepad-widgets`,也没有 GL surface。唯一链接进来的 makepad 代码是 +`makepad-script`,也就是语言 VM,它自身的依赖只有 +`error_log`、`math`、`live_id`、`script-derive`、`smallvec`、`regex`、`html`。 + +## 为什么 Android 版不是 Octoscript-OH 的移植 + +OpenHarmony 提供 `arkui/native_node.h`——一套用于构建控件的 C NDK。 +**Android 没有对应物**:NDK 的 `android/` 目录下有 62 个头文件,没有一个是控件 API。 +每个 `android.widget.*` 对象都必须通过 JNI 进入 ART 来构造,而且与 ArkUI 不同, +托管对象之下没有原生层。因此 Octoscript-OH 那 2.5–3 倍的构建速度优势无法照搬过来; +这里的设计目标是*尽量减少跨边界调用*,而不是避开托管语言对象。 + +完整分析见 octos-one 的 `docs/`(`OCTOSCRIPT-ANDROID-NATIVE-WIDGETS.md`)。 + +## catalog/ + +复刻了 +[material-components-android](https://github.com/material-components/material-components-android) +的 catalog:**42 个页面**,每一个都用 Octoscript DSL 编写,并在真机上求值。 +其中包括真正的 `MaterialAlertDialogBuilder` / `Snackbar` / +`MaterialDatePicker` / `MaterialTimePicker` / `BottomSheetDialog` / +`SideSheetDialog` / `PopupMenu` / `DrawerLayout`,一个真正的 Carousel,以及实时运行的 +Material 动效(`MaterialContainerTransform`、`MaterialSharedAxis`、 +`MaterialFadeThrough`)。 + +它还包含 **octos-one 自己的 makepad 控件移植成的 Android View**—— +`WeatherIconView`、`NavMapView`、`GlassPanelView`——也就是此前被认为在 +`android.widget` 里没有对应物、因而放弃的那几个。 + +见 [`catalog/README.zh-CN.md`](catalog/README.zh-CN.md)。 + +## 构建 + +两者都需要 Android NDK 和 JDK。catalog 用 Gradle(正是 Gradle 才让 androidx 和 +Material 可用);probe 则直接用 `aapt2`/`javac`/`d8` 手工打出 APK。 + +```sh +# catalog +cd catalog/rust && cargo build --release --target aarch64-linux-android +cp target/aarch64-linux-android/release/liboctoscript_catalog.so ../app/src/main/jniLibs/arm64-v8a/ +cd .. && gradle assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk + +# probe +cd probe && ./build.sh +``` + +`catalog/rust` 还有一个在宿主机上运行、不需要设备的检查: + +```sh +cd catalog/rust && cargo run --release --example probe # evaluates all 42 routes +``` + +## 状态 + +- ✅ 可行性验证——框架控件、真正的输入法(IME)、完整的无障碍树 +- ✅ Material catalog——42 个页面,真机上 0 个占位、0 个异常 +- ✅ octos-one 控件移植——WeatherIcon(8 种天气状况)、MapView(3 种导航模式)、玻璃面板 +- ⏳ `UiNode` 增量/事件契约——构建已完成,增量更新尚未完成 +- ⏳ 向 `octoscript-render` 上游提交:`Native`/`Custom` 节点类型、用于协调(reconciliation)的 + `key` 属性,以及 `Serialize` derive + +## 许可证 + +Apache-2.0(见 [LICENSE](LICENSE) 和 [NOTICE](NOTICE))。取自 +material-components-android catalog 的 drawable 保留其原有的 Android Open Source Project +版权声明(同样是 Apache-2.0)。 diff --git a/catalog/README.md b/catalog/README.md index 6879f87..7585953 100644 --- a/catalog/README.md +++ b/catalog/README.md @@ -1,5 +1,7 @@ # Octoscript Catalog — the Material Components Android catalog, driven by Octoscript DSL +English | [简体中文](README.zh-CN.md) + A reproduction of [material-components-android](https://github.com/material-components/material-components-android)'s catalog app in which **every screen is authored in the Octoscript DSL, evaluated on device by the makepad-script VM, and rendered as real @@ -9,7 +11,7 @@ No makepad renderer. No GL surface. No `Octoscript` widget. The only makepad cod the process is the language VM. ``` -41 .splash screens +42 .octoscript screens │ ▼ makepad-script VM (via octoscript-render's re-export) ── Rust generic node tree (kind + attr bag + children) diff --git a/catalog/README.zh-CN.md b/catalog/README.zh-CN.md new file mode 100644 index 0000000..ba331cb --- /dev/null +++ b/catalog/README.zh-CN.md @@ -0,0 +1,137 @@ +# Octoscript Catalog——由 Octoscript DSL 驱动的 Material Components Android catalog + +[English](README.md) | 简体中文 + +复刻了 [material-components-android](https://github.com/material-components/material-components-android) +的 catalog 应用,其中**每个页面都用 Octoscript DSL 编写,在设备上由 makepad-script VM +求值,并渲染成真正的 `com.google.android.material.*` View。** + +没有 makepad 渲染器,没有 GL surface,也没有 `Octoscript` 控件。进程里唯一的 +makepad 代码是语言 VM。 + +``` +42 .octoscript screens + │ + ▼ makepad-script VM (via octoscript-render's re-export) ── Rust +generic node tree (kind + attr bag + children) + │ + ▼ flat binary buffer, one direct ByteBuffer + │ ── ONE JNI crossing per render ── + ▼ +Java builder → MaterialButton / Chip / TextInputLayout / Slider / … ── Java owns every View +``` + +## 构建 + +```sh +cd rust && cargo build --release --target aarch64-linux-android # needs the NDK env +cp target/aarch64-linux-android/release/liboctoscript_catalog.so ../app/src/main/jniLibs/arm64-v8a/ +cd .. && gradle assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk +``` + +通过深链接直接打开任意页面:`adb shell am start -n dev.octoscript.catalog/.MainActivity --es route button` + +不需要设备、在宿主机上检查所有路由:`cd rust && cargo run --release --example probe` + +## 覆盖范围——41 个页面 + +全部 41 个路由都已在真机上验证(OnePlus 6T,Android 11 / SDK 30,Material 1.13.0): +能渲染、没有占位、没有异常。每个页面 22–99 个节点。 + +`allcomponents` `adaptive` `badge` `bottomappbar` `bottomnav` `bottomsheet` +`button` `card` `carousel` `checkbox` `chip` `color` `datepicker` `dialog` +`divider` `dockedtoolbar` `elevation` `fab` `floatingtoolbar` `font` `imageview` +`listitem` `loadingindicator` `materialswitch` `menu` `musicplayer` +`navigationdrawer` `navigationrail` `preferences` `progressindicator` +`radiobutton` `search` `shapetheming` `sidesheet` `slider` `snackbar` `tabs` +`textfield` `timepicker` `topappbar` `transition` + +### 已在真机上验证的交互 + +| 流程 | 证据 | +|---|---| +| `MaterialAlertDialogBuilder`——alert / icon / single / multi / long / full-screen | 对话框以 M3 形状和遮罩渲染 | +| 带操作按钮的 `Snackbar` | 无障碍树中出现 "Message archived" + "Undo" | +| `MaterialDatePicker`——calendar / range / input | 完整日历、日期正确、Cancel/OK | +| `MaterialTimePicker`——12h / 24h / keyboard | "Select time" | +| `BottomSheetDialog`——modal / list / tall | 圆角底部面板 + 列表行 | +| `SideSheetDialog`——left / right / detached | "Side sheet" | +| `PopupMenu` | "Refresh" | +| `DrawerLayout` + `NavigationView` | Inbox / Starred / Settings | +| **状态往返** | 点标签页 → `Content for tab 0` → `Content for tab 1`;拖动滑块 → `Value: 50` → `Value: 97` | + +真正要紧的是状态往返:Java 控件事件写入 Rust 状态,Rust **在 VM 中重新对 DSL 求值**, +新的树再重建 View。决定屏幕上显示什么的是 DSL,而不是 Java。 + +## 发现的 VM 限制(来之不易的部分) + +`octoscript-render` 锁定的 makepad-script 版本 `e1c2164b` 有三种写法会悄无声息地 +生成错误的树,而不是报错。三者都是先表现为空白页面,再通过宿主机侧探测 +(`examples/probe.rs`)找出来的: + +| 写法 | 结果 | 替代做法 | +|---|---|---| +| 以顶层**函数调用**作为模块结果——`page([...])` | 根节点求值后没有 `t` 标签 | 以**字面量对象**结尾 | +| `let k = [ {…}, {…} ]` 然后 `c: k` | 数组到达时是**空的**——子节点被悄悄丢弃 | 把数组内联,或 `let k = []` + `k.push(…)` | +| 对普通对象取 `st.missing_key` | VM 硬错误,整个求值失败 | 用返回默认值的宿主函数——见 `S()` / `N()` | + +`S(key)` / `N(key, default)` 作为 VM 全局变量注入(`set_injected_global`), +与 Octoscript-OH 注入其网络辅助函数的方式完全相同。这也是状态读取不会失败的原因: +缺失的键返回 `""` 或默认值,而不会让求值中断。 + +**返回对象**的辅助函数没有问题——`section()`、`caption()`、`group()` 都是如此。 +只有上面三种写法不安全。 + +## 设计说明 + +- **Java 持有 View;Rust 持有 id 和状态。** Rust 里从不保存任何 `jobject`, + 所以 512 个局部引用上限导致的 abort 和 `FindClass` 的 classloader 陷阱 + 从结构上就不可能触发。 +- **每次渲染只跨一次 JNI。** 整棵树以扁平缓冲区的形式装在一个 direct `ByteBuffer` + 中传递;字符串放在一块附属数据区里,按 (offset, len) 寻址。 +- **通用属性包,而非固定结构体。** `octoscript-render` 的 `Attrs` 大约有 30 个固定字段; + 43 个 Material 组件需要的远不止这些,所以这里的遍历器携带 + `Vec<(String, Val)>`,并对照一份显式声明的约 56 个名称的词表。LiveId 键是 + 单向哈希,所以词表必须声明,无法反推。 +- **用 Gradle,而不是 cargo-makepad。** 正是 Gradle 才让 androidx 和 Material + 可用——`cargo_makepad/src/android/compile.rs` 中 `-classpath android.jar` + 的限制是那条构建路径本身的特性,而不是 Android 的限制。 +- **图标**是 catalog 自带的 112 个矢量 drawable,取自 MDC 仓库。 + 一张小型别名表把 DSL 中的名称映射到最接近的现有图标,用于图标集里没有完全匹配的情况。 + +## 动效、旋转、图标——全部接通 + +**Material 动效是真正在运行的**,通过 `androidx.transition.TransitionManager` +作用于 View 层级来驱动(不需要 fragment 回退栈): + +| 过渡 | 验证结果 | +|---|---| +| `MaterialContainerTransform` | 卡片 → 详情面:`Tap to expand` → `Expanded`,宿主高度随变换从 96dp 动画到 240dp | +| `MaterialSharedAxis` X / Y / Z | `Pane 1` → `Pane 2` | +| `MaterialFadeThrough` / `MaterialFade` | `Pane 2` → `Pane 3` | + +**旋转会重新渲染。** Activity 自己处理 `orientation|screenSize|…`, +把窗口尺寸等级重新写入 Rust 状态,并重新对 DSL 求值: +旋转时 `Compact (384dp)` → `Medium (803dp)`。自适应布局示例也随之重新布局。 + +**Carousel** 使用 `CarouselSnapHelper`;`fullscreen` 策略以页面大小的条目 +纵向翻页。 + +**图标都是真正的矢量图,没有近似替代。** 共 122 个 drawable:MDC +catalog 自带的一套,加上此处为其缺少的图标绘制的 Material Symbols(`share`、`shopping_cart`、 +`notifications`、`more_vert`、`shuffle`、`repeat`、`place`、`call`、`album`、`chat`、 +`bookmark`、`payments`)。别名表现在只保留真正的同义词(`mail`→`mail_outline`、`person`→ +`account_circle`,……),不再有替身。 + +## 靠看真机发现的 bug + +值得记录,因为它们都没有以错误的形式出现: + +- **`PaintDrawable` + `ShaderFactory` 从未绘制出来**,放在 `CENTER_CROP` 的 + `ImageView` 里时没有固有尺寸。carousel 画出的是空卡片。改用 + `GradientDrawable` 修复。 +- **`spacer` 在两个轴上都占了权重**,导致音乐播放器的时间行长到 + 682px,把播放控制按钮挤没了。现在 spacer 只沿父容器的方向占权重。 +- **父容器的 `addChildren` 覆盖了宿主自身的高度**,导致 + container-transform 的宿主一直是 220dp,折叠后的卡片悬在一片空白里。 + 现在由 DSL 声明折叠(`h`)和展开(`max`)两种高度。