操作說明

隆重推出 Cahier:適用於大螢幕工作效率和創作的全新 Android GitHub 範例

11 分鐘閱讀
Chris Assigbe
開發人員關係工程師

Ink API 現已推出 Beta 版,可整合至應用程式。感謝開發人員提供寶貴意見,協助我們持續提升 API 的效能、穩定性和視覺品質,才能達成這項里程碑。

Google 應用程式 (例如 Google 文件Pixel 工作室Google 相簿Chrome PDFYouTube 特效工具) 和 Android 獨有的功能 (例如畫圈搜尋) 都會使用最新 API。

為慶祝這項里程碑,我們很高興宣布推出 Cahier。這款功能齊全的筆記應用程式範例已針對各種尺寸的 Android 裝置 (尤其是平板電腦和摺疊式手機) 進行最佳化。

什麼是 Cahier?

Cahier (法文的「筆記本」) 是一個範例應用程式,旨在說明如何建構應用程式,讓使用者結合文字、繪圖和圖片,記錄及整理想法。

這個範例可做為主要參考資料,協助您提升使用者在大螢幕上的工作效率和創造力。並展示建構這類體驗的最佳做法,協助開發人員更快瞭解及採用相關的強大 API 和技術。本文將逐步說明 Cahier 的核心功能、重要 API,以及架構決策,讓範例成為您自家應用程式的絕佳參考資料。

範例中展示的主要功能包括:

  • 多功能記事建立:說明如何實作彈性內容建立系統,在單一記事中支援多種格式,包括文字、手繪圖和圖片附件。
  • 創意筆跡工具:使用 Ink API 實作高效能、低延遲的繪圖體驗。這個範例實際展示如何整合各種筆刷、顏色挑選器、復原/重做功能和橡皮擦工具。
  • 透過拖曳整合流暢內容:示範如何使用拖曳功能處理傳入和傳出的內容。包括接受從其他應用程式拖曳的圖片,以及讓使用者將內容拖曳出應用程式,以便順暢分享。
  • 整理記事:將記事標示為我的最愛,方便快速存取。篩選檢視畫面,保持井然有序。
  • 離線優先架構:使用 Room 打造離線優先架構,確保所有資料都儲存在本機,且應用程式在沒有網路連線時仍可正常運作。
  • 強大的多視窗模式和多實體支援功能:展示如何支援多實體,讓應用程式在多個視窗中啟動,使用者可以並排處理不同記事,提升大螢幕上的工作效率和創造力。
  • 適用於所有螢幕的自動調整式 UI:使用者介面會使用 ListDetailPaneScaffoldNavigationSuiteScaffold,順暢地配合不同螢幕大小和螢幕方向調整,在手機、平板電腦和摺疊式裝置上提供最佳使用者體驗。
  • 深入整合系統:提供指南,說明如何回應系統範圍的「記事」意圖,讓應用程式成為 Android 14 以上版本的預設記事應用程式,並從各種系統進入點快速擷取內容。

專為大螢幕打造,提升工作效率和創造力

在初步發布時,我們將著重於幾項核心功能,讓 Cahier 成為生產力和創意用途的重要學習資源。

適應性基礎

Cahier 從一開始就設計為可自我調整,這個範例會使用 material3-adaptive 程式庫,特別是 ListDetailPaneScaffoldNavigationSuiteScaffold,完美配合各種螢幕大小和螢幕方向調整應用程式版面配置。這是現代 Android 應用程式的重要元素,而 Cahier 清楚示範了如何有效實作這項元素。

Cahier 自動調整式 UI,使用 Material 3 自動調整式程式庫建構。

Cahier 自動調整式 UI,使用 Material 3 自動調整式程式庫建構

展示重要 API 和整合功能

這個範例主要展示您可在自家應用程式中使用的強大生產力 API,包括:

深入瞭解重要 API

讓我們深入瞭解 Cahier 整合的兩大基礎 API,如何提供一流的筆記體驗。

使用 Ink API 打造自然的手寫體驗

觸控筆輸入可將大螢幕裝置變成數位筆記本和素描本。為協助您打造流暢自然的筆跡體驗,我們將 Ink API 設為範例的基石。Ink API 可輕鬆建立、算繪及操控精美筆觸,並提供同類最佳的低延遲體驗。

Ink API 採用模組化架構,因此您可以根據應用程式的特定堆疊和需求進行調整。API 模組包括:

  • 撰寫模組 (Compose檢視畫面):處理即時手寫筆輸入內容,以裝置可提供的最低延遲時間建立平滑筆觸。
    • DrawingSurface 中,Cahier 會使用新推出的 InProgressStrokes 可組合函式,處理觸控筆或觸控輸入內容。這個模組負責擷取指標事件,並以盡可能低的延遲時間算繪濕墨筆觸。
  • 筆劃模組:代表墨水輸入內容及其視覺呈現方式。使用者畫完線條後,onStrokesFinished 回呼會將最終/乾燥的 Stroke 物件提供給應用程式。這個代表完成墨水筆劃的不可變更物件,隨後會在 DrawingCanvasViewModel 中管理。
  • 算繪模組:有效顯示筆跡筆劃,可與 Jetpack ComposeAndroid 檢視區塊合併。
  • 筆刷模組 (Compose檢視畫面):提供宣告式方法,可定義筆觸的視覺樣式。自 Alpha03 版發布以來,最近的更新包括新的虛線筆刷,特別適用於套索選取等功能。DrawingCanvasViewModel 會保留 currentBrush 的狀態。DrawingCanvas 中的工具箱可讓使用者選取不同的筆刷系列 (例如 StockBrushes.pressurePen()StockBrushes.highlighter()),並變更顏色。ViewModel 會更新 Brush 物件,然後 InProgressStrokes 可組合項會將其用於新筆觸。
  • 幾何模組 (Composeviews):支援操控及分析筆劃,用於清除和選取等功能。
    • 工具箱中的橡皮擦工具和 DrawingCanvasViewModel 中的功能都依賴幾何模組。橡皮擦啟動時,會沿著使用者手勢的路徑建立 MutableParallelogram。橡皮擦接著會檢查形狀與現有筆劃的定界框是否相交,判斷要擦除哪些筆劃,讓橡皮擦使用起來直覺又精準。
  • 儲存空間模組:提供墨水資料的有效序列化和還原序列化功能,可大幅節省磁碟和網路大小。為儲存繪圖,Cahier 會將 Stroke 物件保留在 Room 資料庫中。在「Converters」中,範例會使用儲存空間模組的 encode 函式,將 StrokeInputBatch (原始點資料) 序列化為 ByteArray。位元組陣列和筆刷屬性會儲存為 JSON 字串。載入記事時,系統會使用 decode 函式重建筆觸。
orion.png

除了這些核心模組,最近的更新也擴充了 Ink API 的功能:

  • 開發人員可透過自訂 BrushFamily 物件的全新實驗性 API,建立獨特的創意筆刷類型,為「鉛筆」和「雷射筆」等工具提供更多可能性。

Cahier 運用自訂筆刷 (包括下方展示的獨特音樂筆刷),呈現進階的創作可能性。

使用 Ink API 的自訂筆刷建立彩虹雷射。

使用 Ink API 的自訂筆刷建立彩虹雷射

notes.png

使用 Ink API 的自訂筆刷建立的音樂筆刷

  • 原生 Jetpack Compose 互通性模組可直接在 Compose UI 中整合筆跡功能,簡化開發流程,提升效率。

相較於自訂實作,Ink API 具有多項優勢,是生產力和創意應用程式的理想選擇:

  • 使用方便:Ink API 會抽象化圖形和幾何的複雜性,讓您專注於 Cahier 的核心功能。
  • 效能:內建低延遲支援和最佳化算繪功能,確保提供流暢且反應迅速的筆跡體驗。
  • 彈性:模組化設計可讓您挑選所需元件,將 Ink API 完美整合至 Cahier 的架構。

許多 Google 應用程式已採用 Ink API,包括 Google 文件中的標記、畫圈搜尋,以及 Orion NotesPDF Scanner 等合作夥伴應用程式。

「我們在開發『以圈選搜尋』功能時,首先考慮的就是 Ink API。Ink API 說明文件十分詳盡,因此整合過程非常順利,我們只花了一週就完成第一個可運作的原型。Ink 支援自訂筆刷紋理和動畫,讓我們能快速反覆調整筆觸設計。」 - Google 軟體工程師 Jordan Komoda

透過記事角色成為預設記事應用程式

記事應用程式是核心功能,可提高使用者在大螢幕裝置上的工作效率。有了記事角色功能,使用者就能從螢幕鎖定畫面存取相容的應用程式,或在執行其他應用程式時存取。這項功能會識別並設定系統預設的記事應用程式,並授予啟動權限,以便擷取內容。

在 Cahier 中實作

實作附註角色需要完成幾個重要步驟,範例中都會說明:

  1. 資訊清單宣告:首先,應用程式必須宣告其處理記事意圖的功能。在 AndroidManifest.xml 中,Cahier 包含 android.intent.action.CREATE_NOTE 動作的 <intent-filter>。這會向系統發出信號,表示該應用程式是記事角色的潛在候選者。
  2. 檢查角色狀態SettingsViewModel 會使用 Android 的 RoleManager 判斷目前狀態。SettingsViewModel 會檢查裝置上是否有記事角色 (isRoleAvailable),以及 Cahier 目前是否持有該角色 (isRoleHeld)。這個狀態會透過 Kotlin Flow 向 UI 公開。
  3. 要求角色:如果角色可用但未持有,系統會在 Settings.kt 檔案中向使用者顯示「Button」。點選按鈕時,系統會呼叫 ViewModel 中的 requestNotesRole 函式。這項函式會建立意圖,開啟預設應用程式設定畫面,使用者可以在該畫面中選取 Cahier。這項程序是透過 rememberLauncherForActivityResult API 管理,負責啟動意圖及接收結果。
  4. 更新 UI:使用者從設定畫面返回後,ActivityResultLauncher 回呼會在 ViewModel 中觸發函式,以更新角色狀態,確保 UI 能準確反映應用程式是否已成為預設應用程式。

如要瞭解如何在應用程式中整合記事角色,請參閱「建立筆記應用程式指南」。

helloworld.png

在 Lenovo 平板電腦上,Cahier 會以浮動視窗開啟,做為預設的記事應用程式

重大進展:Lenovo 啟用記事角色

我們很高興宣布,Android 大螢幕生產力邁出重大一步:Lenovo 已在搭載 Android 15 以上版本的平板電腦上啟用 Notes Role 支援!本次更新後,您可更新筆記應用程式,讓相容的 Lenovo 裝置使用者將其設為預設應用程式,從螢幕鎖定順暢存取,並解鎖系統層級的內容擷取功能。

這項承諾來自於領先的原始設備製造商,顯示記事功能在 Android 裝置上提供真正整合式且高效的使用者體驗時,扮演的角色日益重要。

多實體、多視窗和電腦分割視窗

在大螢幕上提高工作效率的關鍵,在於有效管理資訊和工作流程。因此,Cahier 充分運用 Android 的進階視窗功能,提供可因應使用者需求調整的彈性工作區。應用程式支援:

  • 多視窗模式:在分割畫面或任意形式模式下,與其他應用程式並排執行的基本功能。這對於在 Cahier 中做筆記時參照網頁等工作至關重要。
  • 多實體:這項功能可讓您真正享受多工處理的便利性。使用者可以透過 Cahier 同時開啟多個獨立的應用程式視窗,試想一下,您可以在一個視窗中並排比較兩則不同的記事,或在另一個視窗中繪圖時參照某個文字記事。Cahier 會示範如何管理這些個別執行個體 (每個執行個體都有自己的狀態),將應用程式變成功能強大的多面向工具。
  • 電腦分割視窗:連上外接螢幕時,Android 電腦模式會將平板電腦或摺疊式裝置變成工作站。由於 Cahier 採用自動調整式 UI,並支援多實體,因此在這個環境中運作時表現出色。使用者可以開啟多個 Cahier 視窗、調整大小和位置,就像使用傳統桌機一樣,因此可以執行先前無法在行動裝置上進行的複雜工作流程。
cahier-desktop-windowing.webp

在 Pixel Tablet 上以電腦視窗模式執行 Cahier

以下說明我們如何在 Cahier 中導入這些功能:

如要啟用多實體功能,我們必須先在 AndroidManifest 中,將 PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI 屬性新增至 MainActivityMainActivity 宣告,向系統發出信號,表示應用程式支援多次啟動:

  <activity

    android:name="com.example.cahier.MainActivity"

    android:exported="true"

    android:label="@string/app_name"

    android:theme="@style/Theme.MyApplication"

    android:showWhenLocked="true"

    android:turnScreenOn="true"

    android:resizeableActivity="true"

    android:launchMode="singleInstancePerTask">


    <property

        android:name="android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI"

        android:value="true"/>

    ...

</activity>

接著,我們實作了啟動應用程式新例項的邏輯。在 CahierHomeScreen.kt 中,當使用者選擇在新視窗中開啟記事時,我們會建立含有特定旗標的新 Intent,指示系統如何處理新活動的啟動作業。FLAG_ACTIVITY_NEW_TASKFLAG_ACTIVITY_MULTIPLE_TASKFLAG_ACTIVITY_LAUNCH_ADJACENT 的組合可確保記事會在新視窗中開啟,與現有記事並列。

  fun openNewWindow(activity: Activity?, note: Note) {

    val intent = Intent(activity, MainActivity::class.java)

    intent.putExtra(AppArgs.NOTE_TYPE_KEY, note.type)

    intent.putExtra(AppArgs.NOTE_ID_KEY, note.id)

    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK or

        Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT


    activity?.startActivity(intent)

}

如要支援多視窗模式,我們需要將資訊清單的 <activity><application> 元素設為可調整大小,向系統發出信號,表示應用程式支援這項功能。

  <activity

    android:name="com.example.cahier.MainActivity"

    android:resizeableActivity="true"

    ...>

</activity>

UI 本身是以 Material 3 自動調整程式庫建構而成,因此在 Android 分割畫面模式等多視窗模式情境中,可順暢地自動調整。

為提升使用者體驗,我們新增了拖曳功能。請參閱下文,瞭解我們在 Cahier 中實作這項功能的做法。

拖曳

真正有助於提升生產力或創造力的應用程式不會獨立運作,而是與裝置生態系統的其餘部分順暢互動。拖曳是這類互動的基石,尤其是在大螢幕上,使用者經常會在多個應用程式視窗之間工作。Cahier 充分運用這項功能,實作直覺式的拖曳功能,方便使用者新增及分享內容。

  • 輕鬆匯入:使用者可以從其他應用程式 (例如網頁瀏覽器、相片庫或檔案管理員) 拖曳圖片,然後直接放到記事畫布上。為此,Cahier 會使用 dragAndDropTarget 修飾符定義放置區、檢查相容內容 (例如 image/*),以及處理傳入的 URI。
  • 輕鬆分享:分享 Cahier 內容與其他應用程式的內容一樣簡單。使用者可以長按文字記事中的圖片,或長按繪圖記事和圖片組合的整個畫布,然後拖曳到其他應用程式。

深入探索技術:從繪圖畫布拖曳

在繪圖畫布上實作拖曳手勢時,會遇到獨特的挑戰。在 DrawingSurface 中,處理即時繪圖輸入內容 (Ink API 的 InProgressStrokes) 的可組合函式,以及偵測長按手勢以啟動拖曳的 Box,都是同層級的可組合函式

根據預設,Jetpack Compose 指標輸入系統的設計方式是,只有一個同層級的可組合函式 (也就是在宣告順序中,與觸控位置重疊的第一個函式) 會接收事件。以 Cahier 為例,我們希望拖曳輸入處理邏輯有機會在 InProgressStrokes 可組合函式使用所有未使用的輸入內容繪圖並消耗該輸入內容之前,先執行並可能消耗輸入內容。如果我們未正確安排順序,Box 就不會偵測到長按手勢來開始拖曳,或 InProgressStrokes 不會收到繪圖輸入內容。

為解決這個問題,我們建立了自訂 pointerInputWithSiblingFallthrough 修飾符,並在可組合函式程式碼中,將 Box 放在 InProgressStrokes 前面,使用該修飾符。這項公用程式是標準 pointerInput 系統的精簡包裝函式,但有一項重大變更:它會覆寫 sharePointerInputWithSiblings() 函式,以傳回 true。這會告知 Compose 架構,即使指標事件已耗用,仍可傳遞至同層級的可組合函式。

  internal fun Modifier.pointerInputWithSiblingFallthrough(

    pointerInputEventHandler: PointerInputEventHandler

) = this then PointerInputSiblingFallthroughElement(pointerInputEventHandler)


private class PointerInputSiblingFallthroughModifierNode(

    pointerInputEventHandler: PointerInputEventHandler

) : PointerInputModifierNode, DelegatingNode() {


    var pointerInputEventHandler: PointerInputEventHandler

        get() = delegateNode.pointerInputEventHandler

        set(value) {

            delegateNode.pointerInputEventHandler = value

        }


    val delegateNode = delegate(

        SuspendingPointerInputModifierNode(pointerInputEventHandler)

    )


    override fun onPointerEvent(

        pointerEvent: PointerEvent,

        pass: PointerEventPass,

        bounds: IntSize

    ) {

        delegateNode.onPointerEvent(pointerEvent, pass, bounds)

    }


    override fun onCancelPointerInput() {

        delegateNode.onCancelPointerInput()

    }


    override fun sharePointerInputWithSiblings() = true

}


private data class PointerInputSiblingFallthroughElement(

    val pointerInputEventHandler: PointerInputEventHandler

) : ModifierNodeElement<PointerInputSiblingFallthroughModifierNode>() {


    override fun create() = PointerInputSiblingFallthroughModifierNode(pointerInputEventHandler)


    override fun update(node: PointerInputSiblingFallthroughModifierNode) {

        node.pointerInputEventHandler = pointerInputEventHandler

    }


    override fun InspectorInfo.inspectableProperties() {

        name = "pointerInputWithSiblingFallthrough"

        properties["pointerInputEventHandler"] = pointerInputEventHandler

    }

}

DrawingSurface 中使用方式如下:

  Box(

    modifier = Modifier

        .fillMaxSize()

        // Our custom modifier enables this gesture to coexist with the drawing input.

        .pointerInputWithSiblingFallthrough {

            detectDragGesturesAfterLongPress(

                onDragStart = { onStartDrag() },

                onDrag = { _, _ -> /* consume drag events */ },

                onDragEnd = { /* No action needed */ }

            )

        }

) 

// The Ink API's composable for live drawing sits here as a sibling.

InProgressStrokes(...)

完成這項設定後,系統就能同時正確偵測繪圖筆觸和長按拖曳手勢。啟動拖曳作業後,我們會使用 FileProvider 建立可共用的 content:// URI,並使用 view.startDragAndDrop() 將 URI 傳遞至系統的拖放框架。這個解決方案可確保提供穩健直覺的使用者體驗,並展示如何解決分層 UI 中複雜的手勢衝突。

採用現代架構建構

除了特定 API 之外,Cahier 也展示了建構優質自適應應用程式的重要架構模式。

展示層:Jetpack Compose 和適應性

簡報層完全是以 Jetpack Compose 建構而成,如前所述,Cahier 採用 material3-adaptive 程式庫,可適應不同 UI。狀態管理遵循嚴格的單向資料流 (UDF) 模式,ViewModel 執行個體則做為資料容器,用於保存記事資訊和 UI 狀態。

資料層:存放區和 Room

在資料層方面,Cahier 使用 NoteRepository 介面來提取所有資料作業。這項設計選擇可讓應用程式在本地資料來源 (Room) 和潛在的未來遠端後端之間,乾淨地進行交換。編輯記事等動作的資料流程很簡單:

  1. Jetpack Compose UI 會觸發 ViewModel 中的方法。
  2. ViewModel 會從 NoteRepository 擷取記事、處理邏輯,然後將更新後的記事傳回存放區。
  3. NoteRepository 會將更新儲存至 Room 資料庫。

全面支援輸入

如要成為真正的生產力強大工具,應用程式必須能完美處理各種輸入方式。Cahier 的設計符合大螢幕輸入規範,並支援:

  • 觸控筆:整合 Ink API、防止誤觸觸控筆、註冊記事角色、在文字欄位中輸入觸控筆,以及沉浸模式。
  • 鍵盤:支援最常見的鍵盤快速鍵和組合 (例如 Ctrl+按一下、Meta+按一下),並清楚標示鍵盤焦點。
  • 滑鼠和觸控板:支援按滑鼠右鍵和懸停狀態。

支援進階鍵盤、滑鼠和觸控板互動是進一步改善的重點。

現在就開始使用吧!

我們希望 Cahier 能成為您下一個優質應用程式的起點。我們將其建構為全面的開放原始碼資源,展示如何結合適應性 UI、Ink 和 Notes 角色等強大 API,以及現代化的適應性架構。

準備好開始使用了嗎?

  • 探索程式碼:前往 GitHub 存放區,探索 Cahier 程式碼集,並瞭解設計原則的實際應用。
  • 自行建構:以 Cahier 為基礎,打造自己的記事、文件標記或創意應用程式。
  • 貢獻:歡迎你一起來貢獻心力!協助我們讓 Cahier 成為 Android 開發人員社群更實用的資源。

請參閱官方開發人員指南,立即開始建構新一代的生產力和創造力應用程式。我們非常期待看到你的作品!

撰寫者:

繼續閱讀