Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Octoscript-Android

English | [简体中文](README.zh-CN.md)

## Shared Octoscript-Makepad runtime

`native-runtime.lock.json` selects one
Expand Down
111 changes: 111 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -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)。
4 changes: 3 additions & 1 deletion catalog/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down
137 changes: 137 additions & 0 deletions catalog/README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -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`)两种高度。