小蟲茶肆
首发于小蟲茶肆
[Android] 手把手教你復刻 MyScript Math Pad

[Android] 手把手教你復刻 MyScript Math Pad

[!免責聲明]
本文僅代表個人觀點,不代表任何官方立場!

最近閒逛知乎偶然發現還有知友在關心已被 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:

復刻版 MyScript Math Pad 預覽https://www.zhihu.com/video/1101225667382870016

手寫并通過 Interactive Ink SDK 識別定積分: y=\int ^{1}_{0}xe^{x}dx

然後導出 LaTeX 字符串并複製至剪貼板,粘貼至 WolframAlpha 中求解定積分。

MyScript Math Pad 原版和 Interactive Ink SDK 均支持圖像導出, 但為簡便起見,本教程最終復刻版對此 API 不作實現。

0 - 前期準備

要開始開發一個 Interactive Ink 應用,或者將 Interactive Ink SDK 集成到現有的應用中,我們需要具備以下條件:

MyScript 開發者賬號(免費)

Cross-platform handwriting recognition APIs | MyScript Developerdev.myscript.com图标

Interactive Ink 開發許可(免費,由 MyScript 授權)

Getting started | MyScript Developerdeveloper.myscript.com图标

註冊開發者賬號之後即可點選 Android 平台,然後在 Get your certificate 處點擊 Send email 即可通過開發者賬號註冊電郵收到 MyScript 授權的 Interactive Ink 開發許可,通常為一個名為 MyCertificate.java 的源碼文件。

項目腳手架(非官方,僅代表個人觀點)

Interactive Ink 應用還涉及到圖表、多語言文本、數學等識別配置*.conf及資源檔*.res,這些文件均可從 MyScript 開發者網站上手動下載到項目中:

Recognition assets | MyScriptdeveloper.myscript.com

但為簡便起見,我在 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,我們就可以著手開發了。

無效的 MyCertificate.java

1 - 從 Hello World 開始

作為程序猿,Hello World 程序是每個人通向神聖的必經之路,而且,作為寶貴的第一次,Hello World 程序必須是聖潔無雜質、實現最小化以及能帶來成就感的!

在 Android Studio 中打開項目,初始化之後項目中會出現如下模塊:

項目結構
  • app:這是我們將要實現 Hello World 的聖地;
  • buildSrc:包含下載識別配置及資源檔在內的預構建邏輯(不在本文討論範圍之內);
  • my-certificate:我們需要替換 MyScript 開發許可的地方;
  • myscript-iink:MyScript 官方提供的包含 EditorView 等 UI 實現的可重用模塊,詳見:
Rendering | MyScript Developerdeveloper.myscript.com图标

打開 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 有幾種筆觸模式設定:

  1. 自動(=INPUT_MODE_AUTO):顧名思義,即自動監測并區分筆寫(Pen)與觸摸(Touch);
  2. 強制筆寫(=INPUT_MODE_FORCE_PEN):即所有觸控均識別為筆寫(手指也可繪製墨水);
  3. 強制觸摸(=INPUT_MODE_FORCE_TOUCH):即所有觸控均識別為觸摸(筆觸也可滾動);

繼續初始化編輯器視圖Editor View

/** MainActivity::onCreate */
// 將 Content Part 綁定到編輯器
editorView.editor?.part = contentPart
// 可視化編輯器視圖
editorView.visibility = View.VISIBLE

構建運行 app,并寫下我們神聖的 Hello World:

神聖的 Hello World!https://www.zhihu.com/video/1094012683250176000

2 - 轉換:從筆跡墨水到電子文本

事實上,如果我們的裝置支持筆觸,並且編輯器視圖Editor View的筆觸設定為自動(=INPUT_MODE_AUTO),我們的 Interactive Ink 應用是可以通過雙擊將筆跡墨水轉換成電子文本的:

雙擊筆跡轉換成電子文本https://www.zhihu.com/video/1094014854322524160

但是如果我們的裝置不支持筆觸,而編輯器視圖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,然後按「轉換」:

點按轉換https://www.zhihu.com/video/1094023256058789888

同理,編輯器還可以進行以下操作:

  • 清除(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,并寫下最偉大的算式:

一定要看到最後!https://www.zhihu.com/video/1094035919014809600

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,并寫上本文開頭的示例公式:y=\int ^{1}_{0}xe^{x}dx

導出并複製 LaTeX 字符串https://www.zhihu.com/video/1094043016175763456

其他格式的導出同理,此處不再贅述。


總結

至此,本文已不完全地將 MyScript Math Pad 復刻了出來(理論性重複的東西就留給你們自己玩啦),我也在 GitHub 上放出了本文開頭示例中的復刻版 Math Pad 源碼:

Interactive Math Pad | King Or 的 GitHubgithub.com图标

謝謝大家!(此處應該有掌聲~)

编辑于 2019-04-15

文章被以下专栏收录