[!免責聲明]
本文僅代表個人觀點,不代表任何官方立場!
最近閒逛知乎偶然發現還有知友在關心已被 MyScript 下架的 Math Pad:
myscript 制作的mathpad最近下架了吗?www.zhihu.com雖然 Math Pad 已然下架,但是 MyScript 發佈了 Interactive Ink SDK 讓開發者們開發定制自己的手寫識別應用:
Interactive ink | MyScriptwww.myscript.com本文將手把手教大家實現一個基於 MyScript Interactive Ink SDK 的、簡單的 Math Pad。
為簡便起見,本文將 Android 作為開發平台,Kotlin 作為編程語言,但是開發模式同樣適用於 Android(Java)、 iOS、Windows 和 Web。
最終預覽
本教程結束之後,我們將完美(哦,並不)復刻 MyScript Math Pad:
手寫并通過 Interactive Ink SDK 識別定積分:
然後導出 LaTeX 字符串并複製至剪貼板,粘貼至 WolframAlpha 中求解定積分。
MyScript Math Pad 原版和 Interactive Ink SDK 均支持圖像導出, 但為簡便起見,本教程最終復刻版對此 API 不作實現。
0 - 前期準備
要開始開發一個 Interactive Ink 應用,或者將 Interactive Ink SDK 集成到現有的應用中,我們需要具備以下條件:
MyScript 開發者賬號(免費)
Cross-platform handwriting recognition APIs | MyScript Developerdev.myscript.comInteractive Ink 開發許可(免費,由 MyScript 授權)
Getting started | MyScript Developerdeveloper.myscript.com註冊開發者賬號之後即可點選 Android 平台,然後在 Get your certificate 處點擊 Send email 即可通過開發者賬號註冊電郵收到 MyScript 授權的 Interactive Ink 開發許可,通常為一個名為 MyCertificate.java 的源碼文件。
項目腳手架(非官方,僅代表個人觀點)
Interactive Ink 應用還涉及到圖表、多語言文本、數學等識別配置*.conf
及資源檔*.res
,這些文件均可從 MyScript 開發者網站上手動下載到項目中:
但為簡便起見,我在 GitHub 上準備了方便腳手架,識別配置及資源檔已在 Gradle 中自動化為預構建依賴任務:
Interactive Ink Scaffold for Android (Kotlin) | King Or 的 GitHubgithub.com Interactive Ink Scaffold for Android (Java) | King Or 的 GitHubgithub.com我們將 Interactive Ink for Android (Kotlin) 腳手架項目 clone 到本地,並將從 MyScript 開發者網站申請到的開發許可 MyCertificate.java
覆蓋掉模塊 my-certificate
中的 MyCertificate.java
,我們就可以著手開發了。
1 - 從 Hello World 開始
作為程序猿,Hello World 程序是每個人通向神聖的必經之路,而且,作為寶貴的第一次,Hello World 程序必須是聖潔無雜質、實現最小化以及能帶來成就感的!
在 Android Studio 中打開項目,初始化之後項目中會出現如下模塊:
app
:這是我們將要實現 Hello World 的聖地;buildSrc
:包含下載識別配置及資源檔在內的預構建邏輯(不在本文討論範圍之內);my-certificate
:我們需要替換 MyScript 開發許可的地方;myscript-iink
:MyScript 官方提供的包含 EditorView 等 UI 實現的可重用模塊,詳見:
打開 MyApplication.kt
,我們可以看到 MyScript Interactive Ink 識別引擎在應用 onCreate 處被初始化:
/** MyApplication::onCreate */
// Create MyScript interactive ink engine.
// Please make sure that you have a valid active certificate.
// If not, please get one from MyScript Developer:
// - https://developer.myscript.com/getting-started
engine = Engine.create(MyCertificate.getBytes()).apply {
// configure MyScript interactive ink engine.
configuration?.let {
// configure the directories where to find *.conf.
it.setStringArray(
"configuration-manager.search-path",
arrayOf("zip://$packageCodePath!/assets/conf")
)
// configure a temporary directory.
it.setString("content-package.temp-folder", "${filesDir.path}${File.separator}tmp")
}
}
打開 activity_main.xml
,我們在根視圖中導入了 editor_view
:
<!-- activity_main.xml -->
<include layout="@layout/editor_view" />
在 MainActivity
創建時,我們先創建一個 Content Package
:
/** MainActivity */
lateinit var contentPackage: ContentPackage
/** MainActivity::onCreate */
val myPackageFile = File(filesDir, "my_iink_package.iink")
try {
// create a new iink content package.
contentPackage = engine.createPackage(myPackageFile)
} catch (e: Exception) {
e.printStackTrace()
}
Content Package
是一個存儲 Interactive Ink 的容器,在物理存儲中表現為一個後綴名為.iink
的文件。
接著創建一個 Content Part
,並指定其類型為文本Text
,以備用:
/** MainActivity */
lateinit var contentPart: ContentPart
/** MainActivity::onCreate */
// 創建內容類型為 Text 的 Content Part
contentPart = contentPackage.createPart("Text")
Content Part
對應著單獨的內容單元,例如:文本段落、數學公式、圖表模塊等。在創建Content Part
的時候需要指定內容類型,可用的類型包括:文本(=Text
)、數學(=Math
)、圖表(=Diagram
)、素描(=Drawing
)、 文檔(=Text Document
)和原始筆跡(=Raw Content
)。
接下來,我們需要初始化 Interactive Ink 編輯器視圖Editor View
:
/** MainActivity */
lateinit var editorView: EditorView
/** MainActivity::onCreate */
setContentView(R.layout.activity_main)
// 獲得編輯器視圖
editorView = findViewById(R.id.editor_view)
// 獲得識別引擎實例 engine
val engine = (application as? IInteractiveInkApplication)?.engine ?: return
// 編輯器視圖 editorView 綁定識別引擎實例 engine
editorView.setEngine(engine)
// 設定筆觸模式
editorView.inputMode = InputController.INPUT_MODE_AUTO
編輯器視圖 Editor View
有幾種筆觸模式設定:
- 自動(=
INPUT_MODE_AUTO
):顧名思義,即自動監測并區分筆寫(Pen)與觸摸(Touch); - 強制筆寫(=
INPUT_MODE_FORCE_PEN
):即所有觸控均識別為筆寫(手指也可繪製墨水); - 強制觸摸(=
INPUT_MODE_FORCE_TOUCH
):即所有觸控均識別為觸摸(筆觸也可滾動);
繼續初始化編輯器視圖Editor View
:
/** MainActivity::onCreate */
// 將 Content Part 綁定到編輯器
editorView.editor?.part = contentPart
// 可視化編輯器視圖
editorView.visibility = View.VISIBLE
構建運行 app
,并寫下我們神聖的 Hello World:
2 - 轉換:從筆跡墨水到電子文本
事實上,如果我們的裝置支持筆觸,並且編輯器視圖Editor View
的筆觸設定為自動(=INPUT_MODE_AUTO
),我們的 Interactive Ink 應用是可以通過雙擊將筆跡墨水轉換成電子文本的:
但是如果我們的裝置不支持筆觸,而編輯器視圖Editor View
的筆觸設定為強制筆寫(=INPUT_MODE_FORCE_PEN
)以用手指繪製筆跡,那麼我們則需要通過一個按鈕來觸發轉換。
在主視圖中創建選項菜單(Options Menu)并添加一個「轉換」選項,然後在按鈕觸發時向編輯器請求轉換:
/** MainActivity::onOptionsItemSelected(item: MenuItem) */
val editor = editorView.editor ?: return
// 等待編輯器完成所有工作
if (!editor.isIdle) editor.waitForIdle()
when(item.itemId) {
// 向編輯器請求轉換
R.id.menu_convert -> editor.convert() // 先往下看↓
}
不知道怎麼創建選項菜單(Options Menu)的請移步至此:
選項菜單 | Android Developersdeveloper.android.com我們發現,編輯器類Editor
中並沒有空參方法 convert()
,只有一個需要傳入內容塊Content Block
和轉換狀態Conversion State
的方法:
public final void convert(ContentBlock block, ConversionState targetState)
詳見:
Editor (MyScript Interactive Ink 1.3.0 API)developer.myscript.com沒錯,這是我在這裡耍的一個小小的奇技淫巧 —— Kotlin 擴展方法:
/** global scope */
// 給編輯器類(Editor)擴展一個轉換方法,以將全部內容轉換成電子排版形式
fun Editor.convert() =
getSupportedTargetConversionStates(null).firstOrNull()?.let { convert(null, it) }
回到正題,重新構建運行 app
,并再次寫下神聖的 Hello World,然後按「轉換」:
同理,編輯器還可以進行以下操作:
- 清除(clear)
- 撤銷(undo)
- 重做(redo)
這些留給你們自己去玩,玩夠了我們下面就開始干正事!
3 - 我要識別數學公式!
回歸正題,本文要復刻的是一個數學識別應用,所以,我們要先在創建 Content Part 的時候將指定內容類型改為數學(=Math
):
/** MainActivity */
lateinit var contentPart: ContentPart
/** MainActivity::onCreate */
// 創建內容類型為 Math 的 Content Part
contentPart = contentPackage.createPart("Math")
// 啟用渲染數學公式的特殊字體(內嵌於 myscript-iink 模塊中)
editorView.setTypefaces(FontUtils.loadFontsFromAssets(applicationContext.assets))
構建并運行 app,并寫下最偉大的算式:
Voilà,我們已經擁有了一個可以識別數學算式、公式等的應用!
4 - 內容導出
MyScript Interactive Ink SDK 支持多種格式的內容導出,詳見:
Import and export | MyScript Developerdeveloper.myscript.com我們將導出 LaTeX 格式的數學公式。
在選項菜單中添加「導出」按鈕,向編輯器請求導出 LaTeX 字符串并複製到安卓剪貼板:
/** MainActivity::onOptionsItemSelected(item: MenuItem) */
val editor = editorView.editor ?: return
// 等待編輯器完成所有工作
if (!editor.isIdle) editor.waitForIdle()
when(item.itemId) {
R.id.menu_export_latex -> {
// 向編輯器請求導出 LaTeX 字符串
val result = editor.export(null, MimeType.TATEX)
// 將導出結果複製到剪貼板
val service = (getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)
?: return@when
service.primaryClip = ClipData.newPlainText(type.name, result)
// 告知用戶導出結果
Toast.makeText(
this /* MainActivity */,
"String (LATEX) copied to clipboard:n$result",
Toast.LENGTH_LONG
).show()
}
}
構建運行 app
,并寫上本文開頭的示例公式:
其他格式的導出同理,此處不再贅述。
總結
至此,本文已不完全地將 MyScript Math Pad 復刻了出來(理論性重複的東西就留給你們自己玩啦),我也在 GitHub 上放出了本文開頭示例中的復刻版 Math Pad 源碼:
Interactive Math Pad (Android) | GitHubgithub.com還有 UWP 版:
Interactive Math Pad (UWP) | GitHubgithub.com謝謝大家!(此處應該有掌聲~)