最近好多人開始「vibe coding」——不寫規格、邊聊邊生、快速把想法變成一個能跑的 App。做閱讀器相關的小工具尤其熱門。我自己也用這種方式,做了一個展示用的手寫 App,跑在 BOOX 這類電子紙裝置上:可以手寫、辨識繁中英文,還會用「一筆一畫」的方式把 AI 回答寫回紙面。
為了不暴露專案,App 的名字這裡先不寫;你可以把它想成哈利波特裡那本會回話的魔法日記——你在紙面上寫字,它會用手寫回你話。
這篇想分享的,是我們實際用了哪些 SDK 與工具,以及在電子紙上踩到、最後學會的一些事。先講一句最重要的心得:
在一般 Android 平板上,畫筆跡就是
MotionEvent → Canvas → invalidate();但電子紙不是 60/120Hz 的 LCD。Android 這邊畫好了,不代表電子紙面板已經更新。 更新太快會塞車、太慢會卡頓、用錯波形會殘影、閃黑或缺筆。
注:文中會出現一些術語,我會在旁邊用括號補一句白話。想動手做的人可以照著 SDK 與程式碼走;只想看熱鬧的讀者,抓「電子紙為什麼難搞」的觀念就夠了。
先說結論:把它想成四種不同的工作
這個 App 最後採用的是一個分層架構:
- BOOX 原生筆跡層:處理低延遲書寫(筆尖跟得上手)。
- Android 向量筆跡層:保留每一筆,供辨識、淡出、局部清除,以及非 BOOX 裝置的備援。
- Google ML Kit Digital Ink Recognition:把手寫軌跡辨識成繁中、英文與混寫文字。
- BOOX EPD Device SDK:決定哪些區域用快速刷新、哪些用較乾淨的品質刷新。
- MNN 本機推論:在裝置上離線跑語言模型;題庫、規則與網路資料則作為可控的補充來源。
最重要的經驗,不是「挑一個最快的刷新模式」,而是:
把書寫、動畫、靜態文字、深度清除視為四種不同的工作,分別安排刷新區域、波形與節奏。
我們實際用了哪些 SDK 與工具
專案以 Java 17、compileSdk 35、minSdk 24、targetSdk 35 建置。核心清單如下:
| 類別 | 實際使用 | 版本/方式 | 用途 |
|---|---|---|---|
| Android | Android SDK | API 24–35 | Activity、View、Canvas、Path、MotionEvent、LocationManager、SharedPreferences、HttpURLConnection |
| 手寫辨識 | Google ML Kit Digital Ink Recognition | 19.0.0 |
繁中、英文、混寫手寫軌跡辨識 |
| BOOX 筆跡 | onyxsdk-pen |
1.5.4 |
TouchHelper、RawInputCallback、低延遲原生筆跡 |
| BOOX 裝置/電子紙 | onyxsdk-device |
1.3.4 |
EpdController、刷新模式、局部面板更新,以及可選的前光 API |
| BOOX 初始化 | ResManager、RxBaseAction |
隨 Onyx SDK | 初始化 BOOX SDK 所需資源與基礎動作 |
| 隱藏 API 相容 | HiddenApiBypass | 4.3 |
讓 BOOX 裝置 SDK 在新版 Android 上存取部分廠商/隱藏 API |
| 離線 AI | Alibaba MNN/MNN LLM native runtime | JNI .so + Java wrapper |
在裝置 CPU 上執行打包於 App 的本機語言模型 |
| 天氣 | Open-Meteo | HTTPS JSON,無專用 SDK | 以座標查即時天氣,不需 API Key |
| 網路查證 | MediaWiki API | HTTPS JSON,無專用 SDK | 普通 AI 模式下,處理明確要求上網查證的問題 |
| 字型 | LXGW WenKai TC、Kalam 等 | App 內嵌字型 | 中文閱讀字體、英文手寫感字體 |
Gradle 的核心依賴:
// 1) 先在 repositories 加入 Onyx 的來源(settings.gradle 或 project build.gradle)
maven { url 'https://jitpack.io' }
maven { url 'http://repo.boox.com/repository/maven-public/' }
// 2) 模組 build.gradle 的依賴
implementation 'com.google.mlkit:digital-ink-recognition:19.0.0'
implementation 'com.onyx.android.sdk:onyxsdk-pen:1.5.4'
implementation 'com.onyx.android.sdk:onyxsdk-device:1.3.4'
implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
Onyx 的 SDK 由上面兩個 Maven repository 取得(jitpack.io 與 repo.boox.com);MNN 不是一般 Maven 依賴,而是把 arm64 native library、Java JNI wrapper 與轉換後的模型檔一起包進專案。
各 SDK 的官方位置(想自己接,從這裡開始):
- Onyx BOOX SDK:GitHub 範例與文件 onyx-intl/OnyxAndroidDemo(含
Onyx-Pen-SDK.md、EpdController.md、FrontLightController.md、EPD-Update-Mode.md、Eink-Develop-Guide.md);模組有onyxsdk-base/onyxsdk-scribble/onyxsdk-pen/onyxsdk-device。 - Google ML Kit Digital Ink Recognition:developers.google.com/ml-kit/vision/digital-ink-recognition
- Alibaba MNN:github.com/alibaba/MNN
- HiddenApiBypass:github.com/LSPosed/AndroidHiddenApiBypass
- Open-Meteo:open-meteo.com
- MediaWiki API:mediawiki.org/wiki/API:Main_page
- 字型:LXGW WenKai github.com/lxgw/LxgwWenKai、Kalam(Google Fonts)
一個必須誠實的提醒:HiddenApiBypass 與 BOOX 廠商 API 都要當成「裝置專用整合」看待。 它們讓你在 BOOX 上拿到原生能力,但若要上架一般應用商店,得另外檢查隱藏 API、裝置相容性與商店政策——別把它們當成「所有 Android 閱讀器都保證支援」的通用介面。
哪些功能不需要 API Key?
- 離線題庫、MNN 離線 AI:不需要。
- ML Kit 手寫模型:下載後可離線辨識;但第一次下載語言模型時要連網。
- Open-Meteo 天氣、MediaWiki 查證:目前端點不需要 Key。
但「不需要 Key」不等於「可以永久無限用」。公開端點仍該設逾時、節流、快取與離線 fallback,也要留意日後服務條款或政策改變。
那其他家閱讀器呢?Supernote/Meebook/HyRead/Pubook 有 SDK 嗎
很多人會問:一定要 BOOX 嗎?我查了一輪公開資訊,簡單講——目前對開發者最友善、而且有「原廠公開 SDK」的,就是 BOOX(Onyx)。其他家大多是「開放 Android、可以側載 App,但沒有原廠的電子紙/筆 SDK」,電子紙優化得自己用標準 Android API 硬做。
| 品牌 | 原廠公開 SDK? | 能裝自己的 App 嗎 | 備註 |
|---|---|---|---|
| BOOX(Onyx) | ✅ 有(pen/device/scribble/base) | 可(開放 Android) | 有 TouchHelper、EpdController、前光 API 與完整文件,本專案就用這套 |
| Supernote(Ratta) | ❌ 沒有第三方裝置 SDK | 較封閉、不易自由側載 | 系統偏封閉;社群有非官方檔案工具(解析 .note),但那不是 App 開發 SDK |
| Meebook | ❌ 沒查到原廠 SDK | 多為開放 Android,可側載 | 能裝 APK,但低延遲手寫/刷新/筆得靠標準 Android API 自己處理 |
| HyRead Gaze | ❌ 沒查到原廠 SDK | 「開放系統」機型可側載;另有 HyRead 專屬封閉版 | 買前先確認是「開放 Android」版;封閉版裝不了自己的 App |
| Pubook(Pubu) | ❌ 沒查到原廠 SDK | 開放 Android(如 Pubook Pro 10.3),可側載 | 開放系統、能裝 APK,同樣沒有原廠 EPD/筆 SDK |
兩個誠實但書:一是「沒查到公開 SDK ≠ 一定沒有」,有些廠商可能有 NDA/合作性質的私有 SDK,只是不對外;二是「能側載 App」和「有好用的電子紙 SDK」是兩回事——沒有原廠 SDK 時,App 一樣能在開放 Android 機上跑,只是低延遲手寫、局部刷新、前光控制這些,多半只能用通用 Android 做法,效果不一定比得上原廠 API。
一句話結論:想玩「深度電子紙控制」,BOOX 目前最好上手;只想把一般 Android App 跑在電子紙上,開放系統的 Meebook/HyRead 開放版/Pubook 也都能側載來玩。
先分清楚:Wacom EMR、InkSense、Onyx Pen SDK 是三件事
很多人一開始會把這三個混成一團,但它們分屬不同層級:
- Wacom EMR:筆與面板之間的硬體感應技術(電磁式)。筆通常不用電池,靠面板去感應筆的位置、壓力,部分裝置還有傾斜。
- BOOX InkSense Plus:BOOX 特定機型採用的主動式筆方案,需要相容的主動筆。
- Onyx Pen SDK:App 真正呼叫的軟體 API,把裝置韌體提供的原生墨水能力,暴露成
TouchHelper與 raw drawing callback(原始筆點回呼)。
這個專案沒有直接整合 Wacom SDK,也沒有另一套獨立的 InkSense SDK。策略是這樣分工:
| 裝置/筆 | 硬體筆技術 | App 優先路徑 | 備援路徑 |
|---|---|---|---|
| BOOX Note Air 5C | EMR | BOOX TouchHelper raw drawing |
Android MotionEvent stylus |
| BOOX Note Max | EMR | BOOX TouchHelper raw drawing |
Android MotionEvent stylus |
| BOOX Palma 2 Pro | InkSense Plus 主動筆 | 仍先試 BOOX TouchHelper(實際能力看該機韌體) |
Android MotionEvent stylus |
| 一般 Android 裝置 | 視硬體而定 | 不使用 BOOX raw drawing | Android MotionEvent |
所以可攜性的關鍵,不是在程式裡判斷「這是 EMR 還是 InkSense」,而是:
- 製造商是 ONYX 時,嘗試建立 BOOX 原生筆跡層。
- 建立失敗、或不是 BOOX 時,自動退回標準 Android 輸入。
- 所有筆畫都轉成自己的共同資料格式,後面的辨識與特效不直接依賴筆的硬體種類。
好處是同一套上層邏輯能服務多台裝置;限制是不同韌體對 hover、橡皮擦、壓感、掌壓與原生 front buffer 的實作仍可能不同——不能因為 API 名字一樣,就假設體驗完全一致。
低延遲手寫:一個「雙軌」設計
1. BOOX 原生筆跡層
在 ONYX 裝置上,對書寫的 View 建立 TouchHelper:
touchHelper = TouchHelper.create(inkView, rawCallback());
touchHelper.setLimitRect(new Rect(0, 0, width, height), new ArrayList<>())
.setStrokeWidth(6f)
.openRawDrawing();
touchHelper.setRawDrawingEnabled(true);
RawInputCallback 會收到 down、move、up 的 TouchPoint。這條路徑由 BOOX 原生墨水層直接配合電子紙顯示,比只靠 Android View 重繪更接近筆尖(延遲更低)。
2. 同時保存一份 App 自己的筆跡
每一筆會同步存成兩種形式:
- Android
Path:供畫面重繪、淡出、局部擦除,以及非 BOOX 的 fallback。 - ML Kit
Ink.Point:保存 x、y、timestamp,交給手寫辨識模型。
這個「雙軌」很重要。BOOX raw layer 很快,但它不是 App 可以任意重播、做動畫的普通 Canvas;如果沒有另外留一份自己的 Path,後面要做「墨水消失」「局部清除」「旋轉後重繪」就會很難。
3. 標準 Android fallback
若 BOOX raw drawing 無法初始化,就走 InkView.onTouchEvent(),處理 TOOL_TYPE_STYLUS、TOOL_TYPE_ERASER、SOURCE_STYLUS 與 MotionEvent 的 historical samples(同一次事件裡夾帶的多個取樣點)。這讓 App 在其他 Android 裝置上仍可運作,也方便用測試工具模擬筆跡——只是通常無法完全複製 BOOX 原生那條低延遲 front buffer。
4. 壓感的現況
目前 raw drawing 的顯示寬度固定為 6px,辨識主要靠軌跡的幾何與時間;Android 路徑雖然能讀到 pressure/tilt,但還沒有把壓力對應成動態筆寬。
這是刻意的展示版取捨:固定粗度在電子紙上比較容易得到一致的辨識與淡出效果。 若未來要做更自然的書法筆觸,應先把壓感正規化,再測不同機型的回報範圍——不能假設所有裝置都是同一條 0–1 曲線。
掌壓防誤觸:硬體有幫忙,App 還是要做第二道防線
展示時最常出包的,其實不是辨識錯,而是使用者手腕一放上去,工具列、翻頁或功能鍵就被誤觸。所以 App 另外做了一套輸入政策:
- 每次偵測到筆後,啟動約 1.4 秒的 stylus palm guard(這段時間內把手掌類接觸擋掉)。
- 手指接觸的
touchMajor大於約 52dp,或 normalized size 大於 0.22,視為手掌、直接吞掉。 - 優先相信
MotionEvent.getToolType(),不要只信 source bit。 - BOOX raw drawing 啟用時,筆交給原生墨水層;手指只保留受控的翻頁、關書與手勢。
- 多指事件與手掌事件,不送進書寫 View。
這裡有個實機踩坑:Note Max 的一般手指事件,有時會帶著看似 stylus 的 source bit。 如果只用 SOURCE_STYLUS 判斷,手指會被誤認成筆——所以明確的 TOOL_TYPE_FINGER 必須優先。
掌壓防護也不該只靠「關掉手指」。展示 App 仍需要手指操作,比較好的做法是把「筆」「單指功能」「多指提示」「大面積接觸」拆成不同狀態機分開處理。
手寫辨識:繁中、英文、混寫,還有小朋友的注音
基礎模型
App 用 ML Kit Digital Ink Recognition 的 zh-Hani-TW 與 en-US 兩個模型。首次啟動會下載模型、並用一小段假筆跡 warm-up(暖機),之後可在裝置上離線辨識。
中英文混寫
整頁同時送進中、英文模型只是第一層。遇到「你好我是 Seal」或「Today 天氣如何」這種混寫,App 會找筆畫之間明顯的水平空隙,把左右區塊分開,再各自跑一次中、英文模型,最後用候選分數、字形與語境合併:
原始 strokes
├─ 整頁中文模型
├─ 整頁英文模型
└─ 若偵測到大型水平間距
├─ 左區:中文 + 英文
└─ 右區:中文 + 英文
↓
候選修復與語境合併
辨識範圍不能用整個螢幕
ML Kit 的 WritingArea(辨識範圍)會影響結果。如果小朋友只在大頁面角落寫幾個注音,卻把整個全螢幕當成 writing area,符號很容易被判成標點或碎片。所以本專案會依實際筆畫的 bounds 建一個較緊的辨識區域,再正規化筆畫。
注音模式要獨立
全注音、中文與中英混寫如果永遠同時競爭,候選會互相污染。因此改成讓「使用情境」決定等待時間與候選修復:
- 一般模式:約 2.8 秒沒有新筆畫就辨識。
- 兒童雙語模式:約 4.8 秒。
- 全注音模式:約 6 秒(小朋友通常一個符號一個符號慢慢寫)。
- 展示用 Promo 模式:約 2.2 秒,而且只跑中文模型,換取更快、更穩定的三題辨識。
亂塗、畫畫、寫錯字也不該立刻送 AI。App 會先判斷「密集來回的 scribble」或「停頓後的一橫」是不是塗掉的動作,刪掉相交的舊筆畫、局部刷新,然後等使用者重寫。
殘影控制的核心:局部刷新,不是一直全刷
「局部刷新」=只更新螢幕上真的有變動的那一小塊,而不是整頁重畫。這是電子紙不殘影的關鍵。
1. App 級的品質基線
BOOX 使用者可能在系統最佳化裡切到 Speed/Extreme。為避免 App 下次啟動時品質突然掉一截,App 會在啟動、回前景、重新取得焦點時,重新套用 app 範圍的 REGAL 基線並開啟 Regal(REGAL 是 BOOX 一種抑制殘影的局部刷新波形):
EpdController.applyAppScopeUpdate(
profileName, true, true,
UpdateMode.REGAL, Integer.MAX_VALUE);
EpdController.enableRegal();
這是應用範圍內的 best effort,不是繞過韌體的絕對鎖定。系統版本、裝置最佳化設定與廠商策略,仍可能影響最終面板行為。
2. 不同內容用不同 update mode
以下是本專案實測後的分工(不是宣稱所有 BOOX 機型都得照抄):
| 工作 | 使用模式 | 原因 |
|---|---|---|
| 即時手寫 | BOOX raw drawing;View 備援用 GU_FAST/GU |
以低延遲優先 |
| 回答逐筆浮現 | 過程用 GU 或 ANIMATION_QUALITY |
允許局部動態更新 |
| 翻頁/淡出 | ANIMATION_QUALITY;Note Max 淡出偏 REGAL |
避免把每個中間灰階都殘留在面板 |
| 回答完成 | REGAL/REGAL_PLUS |
做一次乾淨的局部 settle(收尾) |
| 深度清理 | 謹慎用 GC;Note Max 改用兩次局部 REGAL |
GC(整頁清除)可能造成明顯全螢幕閃爍 |
| 文字 View | 停用 A2 |
A2 雖快,但畫質與殘影不適合書頁文字 |
3. Dirty rectangle 是最重要的資料
每次刷新前,先算出實際變動範圍(dirty rect,「髒矩形」):
- 手寫區:所有 Path bounds 的聯集,再加約 36px padding。
- 回答動畫:只送出本幀新增筆畫的 dirty rect。
- 回答完成:只清理回答文字 content bounds 周圍。
- 塗掉重寫:只重繪「擦除區」與「仍存在筆畫」的聯集。
EpdController.refreshScreenRegion(
view, left, top, width, height, updateMode);
這比對整個 root view 呼叫刷新重要太多。大區域的品質刷新,很容易被韌體升級成一次明顯的全刷;小區域則比較能維持「紙面只有局部變化」的錯覺。
4. Android 的 frame 與 E Ink 面板的 frame 必須解耦
回答的手寫動畫在 Android 端大約每 44ms 更新一次 Path,但不會每 44ms 都要求面板更新。實際送往面板的節奏約為:
- Promo:145ms
- Note Air 5C/一般 BOOX:165ms
- Note Max:200ms
也就是 Android 可以先累積幾小段線,再一次刷新它們的 dirty rect。如果每次 invalidate() 都立刻要求電子紙刷新,更新會排隊,最後看到的反而是卡頓、缺字或中段沒正常顯示。
5. 要等面板真的完成
品質 settle 前,會用背景執行緒呼叫 EpdController.waitForUpdateFinished(),避免最後一個 REGAL 收尾插隊、蓋掉還沒畫完的 GU 幀;同時設 watchdog(看門狗計時器),因為部分韌體可能會漏掉「完成」訊號,不能讓回答永遠卡在等待。
手寫消失特效:不能照手機動畫的思維做
早期如果把字拆成粒子、透明度碎片或大量移動點,在 LCD 上可能很好看,在電子紙上卻會留下一堆中間狀態,像數位雜訊,而不是「墨水被紙吸走」。
現在的做法是:
- 先用 App 保存的完整向量 Path,覆蓋在 BOOX raw layer 下方。
- 等這個向量版本確實送到面板。
- 再關掉 raw drawing,避免原生墨水層突然消失、閃一幀空白。
- 前半段:讓完整字形以不透明灰階逐步靠近紙張白色。
- 後半段:才在已經很淡的線條上,加入位置固定的白色「紙纖維孔洞」。
- 最後只對筆畫 bounds 做
REGAL/REGAL_PLUS局部清除。
紙纖維的雜訊圖樣(hash)每一幀都固定,孔洞不會亂跑,所以視覺上比較像「紙慢慢吸墨」,而不是字被數位化拆散。
不同面板還要用不同節奏:
- Note Max(大尺寸黑白):5 個較慢步驟,每步約 420ms,過程以
REGAL為主。 - 彩色 BOOX:10 個步驟,每步約 260ms,用
ANIMATION_QUALITY呈現較連續的淡出,完成後再品質清理。
這不是因為 EMR 筆會製造殘影,而是「大尺寸黑白面板、彩色濾光層、刷新波形、動畫中間狀態」的組合不同。
AI 回答怎麼做出「一筆一畫寫上去」的效果
回答不是把 TextView 一個字一個字顯示。HandwrittenReplyView 的流程是:
- 用指定字型把 glyph(字形)rasterize 成 bitmap。
- 對黑色遮罩做 Zhang–Suen thinning(骨架化,把粗筆畫縮成一像素寬的骨架)。
- 從端點追蹤骨架,轉成一段段
Path。 - 按節奏逐段加入 retained path。
- 每次只把新增筆畫的 dirty rect 交給 BOOX 局部刷新。
於是視覺上像筆尖逐步寫出文字。但這裡要誠實標註:
這是「視覺上的手寫路徑模擬」,不是正確的國字教學筆順。
若產品要用於識字或筆順教育,就需要真正的中文字形筆順資料,不能只靠骨架追蹤。
Note Max 為什麼需要獨立的刷新設定
本專案目前做兩種刷新 profile:
NoteMax:大尺寸黑白面板,回答較慢、淡出以 REGAL 為主、每幾次回答安排一次局部品質清理。BooxColor:Note Air 5C/其他彩色 BOOX,書寫用GU_FAST、動畫用ANIMATION_QUALITY、完成後用REGAL_PLUSsettle。
Note Max 上若對大 parent view 用 GC,有機會被韌體表現成整頁閃黑。所以「深度清理」改成對目前可見的小範圍連續做兩次 REGAL,中間留一點間隔。
這也說明電子紙最佳化不該只問「哪個模式最好」,真正要問的是:
- 哪一塊畫面變了?
- 這一塊是動畫中間幀,還是最後結果?
- 使用者此刻更在意低延遲,還是乾淨文字?
- 面板這一分鐘累積了多少中間狀態?
前光控制:SDK 做得到,但這一版沒有接管
目前這個 App 沒有主動調前光。Note Air 5C 與 Palma 2 Pro 的前光仍由 BOOX 系統/使用者控制;Note Max 本身沒有前光。
這是刻意的產品選擇:前光是裝置層級的狀態,不是 App 裡的一個 View 屬性。 展示 App 若擅自改亮度或色溫、離開又沒復原,會影響使用者後續閱讀。
BOOX Device SDK 的範例顯示,可以先判斷前光類型:
switch (BrightnessController.getBrightnessType(context)) {
case FL:
// 單一前光
break;
case WARM_AND_COLD:
// 暖光、冷光分開控制
break;
case CTM:
// 亮度 + 色溫
break;
case NONE:
// 此裝置沒有前光,例如 Note Max
break;
}
取得對應的 BaseBrightnessProvider 後,可以用 getIndex()/getMaxIndex()、setIndex(...)、toggle()、isLightOn()(依 provider 版本能力而定)。常見 light type 包括 LIGHT_TYPE_FL、LIGHT_TYPE_CTM_WARM、LIGHT_TYPE_CTM_COLD、LIGHT_TYPE_CTM_BRIGHTNESS、LIGHT_TYPE_CTM_TEMPERATURE;也可以廣播 action.show.brightness.dialog,讓 BOOX 自己跳亮度設定介面。
若下一版要加入前光控制,建議的流程是:
- 先偵測
BrightnessType,沒有前光就隱藏功能。 - 進 App 時記住原本的 on/off、亮度與色溫。
- 只在使用者明確啟用時才調整,避免每次啟動就強改全機設定。
- App 離開或展示模式結束時,把原值復原。
- 對 Note Max 顯示「此裝置沒有前光」,不要去呼叫空的 provider。
離線 AI 與資料來源
App 內的語言模型用 MNN native runtime(阿里巴巴的端側推論框架),在 arm64 裝置上以 CPU、2 threads、low precision、low memory 執行。這讓離線 AI 功能不必依賴雲端 API Key,但小模型的知識、推理與速度都有限,所以實際採混合架構:
- 高命中、展場重要問題:離線題庫直接回答。
- 人格與世界觀:用結構化的 Knowledge Pack 與 Evidence Map 約束回答。
- 開放式生成:MNN 離線模型。
- 天氣:Open-Meteo。
- 明確的網路查證:MediaWiki。
- 逾時或模型卡住:watchdog + 可控 fallback,不能讓畫面永遠停在「思考中」。
補一個容易誤會的點:資料(例如幾本小說)不是每次回答都整本塞進 prompt,也不是把大量原文輸出給使用者;它們主要拿來離線整理譯名、事件、角色知道什麼、角色願意透露什麼,以及可追溯的證據摘要。
如果你也要 vibe coding 這種 App,幾個值得保留的方法
1. 每一台機器都當成不同的顯示器
不要只在模擬器驗證。至少記錄下:Build.MODEL、Android/韌體版本、用的 refresh profile、每次辨識耗時與最終選擇、AI 載入與生成時間、是否觸發 watchdog、局部刷新矩形與 update mode。
2. 先建立「可重播測試」,再調動畫
手寫問題如果只能請真人反覆寫,很難分辨是辨識、輸入還是刷新退步。把 stroke 的 x/y/time 存下來,就能在測試環境重播同一份筆跡,分別測:ML Kit 候選有沒有變、混寫切分對不對、塗除有沒有誤判、回答區有沒有缺幀、Note Max 長時間用會不會愈來愈慢。
3. 把「刷新」當成排程器,而不是工具函式
成熟一點的版本,可以把所有 EPD 更新集中到一個 coordinator:合併相近的 dirty rect、限制同時只有一個品質刷新、動畫結束後統一 settle、記錄每次面板等待時間。這比讓各個 View 到處直接呼叫 refreshScreenRegion() 好維護太多。
4. 不要把展示流程綁在最慢的元件上
冷啟動時,ML Kit 模型、MNN 模型與電子紙清理都可能要時間。使用者看到的「開書動畫」不是純裝飾,而是把初始化狀態變成一段可理解的過場——但仍要有上限與 watchdog,動畫不能拿來掩蓋真正的死鎖。
Android 的另一個好處:AI 可以用 ADB 自己測、APK 直接裝
做電子紙 App 時,Android(相對於封閉系統)有兩個對「vibe coding」特別友善的地方。
一、AI 可以透過 ADB 直接操作真機、自己測。 把閱讀器用 USB 接上(或開無線 ADB),AI 就能用指令安裝、啟動、點擊、輸入、截圖,形成「改程式 → 裝上真機 → 截圖看結果 → 再改」的自動迴圈,不用你每一輪都手動操作:
adb install -r app-debug.apk # 安裝/覆蓋安裝
adb shell am start -n 你的套件名/.MainActivity # 啟動 App
adb shell input tap 500 800 # 模擬點擊
adb shell input swipe 300 900 300 300 # 模擬滑動
adb exec-out screencap -p > shot.png # 截圖回存,讓 AI「看」結果
(想做更完整的 UI 自動化,可再搭配 uiautomator 或用 scrcpy 投影操作。)電子紙有個現實:截圖看到的畫面,和你眼睛看到的殘影/刷新品質不完全一樣,所以 ADB 自動測適合驗證「流程對不對、有沒有崩」,最後的刷新手感還是得真人盯著面板看。
二、做好的 APK 可以直接裝,不必上架商店。 開放式 Android 閱讀器(BOOX、Meebook、HyRead 開放版、Pubook 等)允許側載(sideload):把 .apk 丟上去、或用 adb install,就能直接跑自己的 App,不必送 Google Play 審核、也不必公開發佈。對「做給自己用」「展場 demo」「小圈子分享」來說,這個門檻低到幾乎沒有——這也是為什麼 vibe coding 一個電子紙 App,比你想像的更容易起步。
但別忘了前面那個誠實提醒:用到 BOOX 廠商 API/HiddenApiBypass 的部分是裝置專用的;自用、側載沒問題,但哪天要正式上架一般商店,隱藏 API、裝置相容性與商店政策都得再處理。
想自己動手?兩段可以直接用的 Prompt
如果你也想試,這裡給兩段可以直接丟給 AI(Claude/Codex 等)的起手 Prompt:一段先求「最小可跑」,一段再加「電子紙優化」。
Prompt 1:先做出一個能手寫的最小版
幫我做一個 Android(Java,minSdk 24、targetSdk 35)電子紙手寫 App 的最小可跑版本。需求:
1. 一個全螢幕手寫 View:在 BOOX(製造商為 ONYX)裝置上,用 Onyx 的 onyxsdk-pen(TouchHelper + RawInputCallback)做低延遲書寫;
若初始化失敗或非 BOOX,退回標準 Android MotionEvent(TOOL_TYPE_STYLUS)。
2. 每一筆同時保存兩份:Android Path(畫面用)與一組 (x, y, timestamp) 點列(之後給辨識用)。
3. 一顆「清除」按鈕。
請附上 build.gradle 要加入的 Maven repository 與依賴,並在關鍵處註解說明為什麼要「雙軌」保存筆跡。
Prompt 2:把辨識與電子紙刷新加上去
延續上一個手寫 App,加上:
1. 用 Google ML Kit Digital Ink Recognition,模型用 zh-Hani-TW 與 en-US 離線辨識;停筆約 2.5 秒後才辨識。
2. 電子紙刷新只刷「有變動的區域」:每次更新前計算 dirty rectangle(所有變動 Path bounds 的聯集加 padding),
用 onyxsdk-device 的 EpdController.refreshScreenRegion() 局部刷新;書寫用快速模式、收尾用 REGAL 做一次乾淨清除。
3. 把「Android 重繪節奏」和「送去電子紙面板的節奏」分開,不要每次 invalidate 都要求面板刷新。
請解釋你選的 update mode,以及為什麼不要一直對整頁做全刷。
小提醒:AI 生出來的第一版通常「能跑但不夠順」。真正的功夫,是拿到真機上一輪一輪調刷新節奏——也就是前面整篇在講的東西。
實作檢查清單(可直接照抄)
輸入
- BOOX raw drawing 失敗時可退回 Android MotionEvent。
- 所有筆畫同時保留 Path 與辨識點列。
- 明確區分 finger、stylus、eraser 與 palm。
- 不只依賴 source bit 判斷筆。
- 橡皮擦、塗掉與普通橫線有不同規則。
辨識
- 語言模型有下載、warm-up 與失敗提示。
- 繁中使用
zh-Hani-TW。 - 中英混寫不只跑單一整頁模型。
- 全注音/兒童模式有較長的 idle window。
- 亂塗、未寫完、低信心輸入不直接丟給 AI。
電子紙
- 所有動畫都計算 dirty rectangle。
- Android animation cadence 與 panel cadence 分離。
- 最後一幀後才做
REGAL/REGAL_PLUSsettle。 - 不對大 root view 頻繁
GC。 - 切前景、轉向、焦點恢復時,重新套用 refresh profile。
- 面板等待有 watchdog。
前光
- 先偵測裝置是否支援。
- 修改前先保存原狀態。
- 使用者明確操作才修改。
- 離開 App 時復原。
- Note Max 等無前光裝置不顯示無效控制。
結語
Vibe coding 可以很快做出「能跑」的 Android App,但電子紙會逼你面對另一層現實:畫面不是畫完就結束,筆跡也不是收到座標就完成。 真正的體驗,來自輸入技術、保留筆跡、辨識模型、面板波形、局部刷新與產品流程一起工作。
這個專案最後得到的,不是一個神奇的「最佳刷新模式」,而是一套協作原則:
筆尖要快、動畫要節制、文字要乾淨、清理要局部;硬體差異交給 adapter,使用者等待則交給有意義的故事與明確狀態。
這也是把一個原型,變成能在 Note Air 5C、Note Max、甚至不同筆技術裝置上都展示得起來的關鍵。如果你也在 vibe coding 自己的閱讀器 App,希望這份踩坑筆記能幫你少繞幾個彎。
參考資料與 SDK 連結
本文用到的 SDK/工具
- Onyx BOOX Android SDK 範例與文件(onyxsdk-base/scribble/pen/device):https://github.com/onyx-intl/OnyxAndroidDemo
- Onyx SDK Maven 座標(
com.onyx.android.sdk):https://mvnrepository.com/artifact/com.onyx.android.sdk- repositories:
https://jitpack.io、http://repo.boox.com/repository/maven-public/
- repositories:
- Google ML Kit|Digital Ink Recognition(手寫辨識):https://developers.google.com/ml-kit/vision/digital-ink-recognition
- Alibaba MNN(端側推論框架,離線 AI):https://github.com/alibaba/MNN
- LSPosed AndroidHiddenApiBypass(隱藏 API 相容):https://github.com/LSPosed/AndroidHiddenApiBypass
- Open-Meteo(免 API Key 天氣):https://open-meteo.com/
- MediaWiki API(網路查證):https://www.mediawiki.org/wiki/API:Main_page
- LXGW WenKai 字型:https://github.com/lxgw/LxgwWenKai;Kalam(Google Fonts):https://fonts.google.com/specimen/Kalam
其他閱讀器的 SDK/開放性參考
- 只有 BOOX(Onyx)有原廠公開 SDK(見上方連結)。
- Supernote、Meebook、HyRead、Pubook:以公開資訊查無原廠 EPD/筆 SDK;開放 Android 機型可側載 App(HyRead 有開放版與封閉版之分,購買前請確認)。「查無」不代表一定沒有,可能有 NDA/私有 SDK。
延伸閱讀
- Read with Pro:〈Pubu/Pubook 路線整理:從電子書平台到自家閱讀器〉:https://www.readwithpro.com/blog/pubu-pubook-platform-reader-investigation
