在 Wear OS 中建立第一個資訊方塊

1. 簡介

操作手錶的示範動畫:使用者將錶面滑動至第一個資訊方塊 (天氣預測),再滑動至計時器資訊方塊,接著返回

Wear OS 資訊方塊可讓使用者輕鬆存取必要資訊和動作,流暢地處理大小事務。只要滑動錶面,就能查詢最新天氣預報或啟動計時器。

資訊方塊是系統 UI 的一部分,並不是在專屬應用程式容器中執行。我們會使用服務來描述資訊方塊的版面配置和內容,系統 UI 則會視需要顯示資訊方塊。

執行步驟

35a459b77a2c9d52.png

您會建構訊息應用程式資訊方塊,用於顯示最近的對話。使用者可以從這個畫面直接跳到下列任一項常見操作:

  • 開啟對話
  • 搜尋對話
  • 撰寫新訊息

課程內容

在這個程式碼研究室中,您將瞭解如何編寫自己的 Wear OS 資訊方塊,包括如何:

  • 建立 TileService
  • 在裝置上測試資訊方塊
  • 在 Android Studio 中預覽資訊方塊 UI
  • 開發資訊方塊 UI
  • 新增圖片
  • 處理互動

必要條件

2. 開始設定

在這個步驟中,您會設定環境並下載範例專案。

軟硬體需求

  • Android Studio Dolphin (2021.3.1) 以上版本
  • Wear OS 裝置或模擬器

如果您不熟悉 Wear OS 的使用方式,建議先參考這篇簡要說明,再開始進行。文章內容包括如何設定及操作 Wear OS 模擬器。

下載程式碼

如果您已安裝 Git,只要執行下列指令即可複製這個存放區的程式碼。如要檢查 Git 是否已安裝完成,請在終端機或指令列中輸入「git –version」,並確認可正確執行。

git clone https://github.com/android/codelab-wear-tiles.git
cd wear-tiles

如果您沒有 Git,可以點選下方按鈕,以下載這個程式碼研究室的所有程式碼:

在 Android Studio 中開啟專案

在「Welcome to Android Studio」視窗中,選取 c01826594f360d94.png「Open an Existing Project」或依序點選「File」>「Open」,然後選取「Download Location」資料夾。

3. 建立基本資訊方塊

資訊方塊的進入點是資訊方塊服務。在這個步驟中,您必須註冊資訊方塊服務,並定義方塊的版面配置。

HelloWorldTileService

實作 TileService 的類別時需要指定兩個函式:

  • onResourcesRequest(requestParams: ResourcesRequest): ListenableFuture<Resources>
  • onTileRequest(requestParams: TileRequest): ListenableFuture<Tile>

第一個函式會將字串 ID 對應至圖片資源。請在這個函數中,提供要使用於資訊方塊中的圖片資源。

第二個則會傳回資訊方塊的說明,包括方塊版面配置。請在這裡定義資訊方塊的版面配置,以及資料的繫結方式。

start 模組開啟 HelloWorldTileService.kt。您所做的任何變更都會收錄在本模組中。如果您想查看這個程式碼研究室的結果,也可以使用 finished 模組。

HelloWorldTileService 可擴充 CoroutinesTileService,後者來自 Horologist Tiles 程式庫,是適合用於 Kotlin 協同程式的包裝函式。Horologist 是 Google 提供的一系列程式庫,旨在提供 Wear OS 開發人員常用,但 Jetpack 目前尚未提供的功能。

CoroutinesTileService 提供兩個暫停函式,是 TileService 中函式的協同程式版本:

  • suspend resourcesRequest(requestParams: ResourcesRequest): Resources
  • suspend tileRequest(requestParams: TileRequest): Tile

如要進一步瞭解協同程式,請參閱 Android 上的 Kotlin 協同程式說明文件。

HelloWorldTileService 尚未完成。我們需要在資訊清單中註冊服務,且需要為 tileLayout 提供實作方式。

註冊資訊方塊服務

您必須在資訊清單中註冊資訊方塊服務,讓系統知道服務存在。註冊完畢後,就會顯示在可用資訊方塊清單中,供使用者新增。

<application> 元素中新增 <service>

start/src/main/AndroidManifest.xml

<service
    android:name="com.example.wear.tiles.hello.HelloWorldTileService"
    android:icon="@drawable/ic_waving_hand_24"
    android:label="@string/hello_tile_label"
    android:description="@string/hello_tile_description"
    android:exported="true"
    android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER">

    <intent-filter>
        <action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />
    </intent-filter>

    <!-- The tile preview shown when configuring tiles on your phone -->
    <meta-data
        android:name="androidx.wear.tiles.PREVIEW"
        android:resource="@drawable/tile_hello" />
</service>

使用者首次載入資訊方塊或發生載入錯誤時,系統會把圖示和標籤放在預留位置。結尾的中繼資料會定義使用者新增資訊方塊時,在輪轉介面中顯示的預覽圖片。

定義資訊方塊的版面配置

HelloWorldTileService 具有名為 tileLayout 的函式,主體為 TODO()。現在,我們要實際替換程式碼,定義資訊方塊的版面配置並繫結資料:

start/src/main/java/com/example/wear/tiles/hello/HelloWorldTileService.kt

private fun tileLayout(): LayoutElement {
    val text = getString(R.string.hello_tile_body)
    return LayoutElementBuilders.Box.Builder()
        .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
        .setWidth(DimensionBuilders.expand())
        .setHeight(DimensionBuilders.expand())
        .addContent(
            LayoutElementBuilders.Text.Builder()
                .setText(text)
                .build()
        )
        .build()
}

我們會建立 Text 元素並在 Box 中設定該元素,以便執行一些基本對齊方式。

您已成功建立第一個 Wear OS 資訊方塊!立即安裝,看看實際呈現的樣子。

4. 在裝置上測試資訊方塊

只要在執行設定下拉式選單中選取開始模組,即可在裝置或模擬器上安裝應用程式 (start 模組),然後像普通使用者一樣手動安裝資訊方塊。

請改用 Android Studio Dolphin 推出的 Direct Surface Launch 功能,以便建立新的執行設定,直接從 Android Studio 啟動資訊方塊。請在頂端面板的下拉式選單中,選取「Edit Configurations...」。

Android Studio 面板頂端的執行設定下拉式選單。「Edit configurations」選項以醒目底色顯示。

按一下「新增設定」按鈕,然後選擇「Wear OS 資訊方塊」。新增描述性名稱,然後選取 Tiles_Code_Lab.start 模組和 HelloWorldTileService 資訊方塊。

按一下「OK」以結束程序。

在「Edit Configuration」選單中,設定名為 HelloTile 的 Wear OS 資訊方塊。

使用 Direct Surface Launch 功能,就能透過 Wear OS 模擬器或實體裝置快速測試資訊方塊。嘗試執行「HelloTile」。您看到的畫面應如下方螢幕截圖所示。

以黑底白字顯示「Time to create a tile!」的圓形手錶

5. 建構訊息資訊方塊

圓形手錶有 5 個圓形按鈕,上排 2 個,下排 3 個。第 1 個和第 3 個按鈕以紫色文字顯示姓名縮寫,第 2 個和第 4 個按鈕是個人資料相片,最後一個則是搜尋圖示。按鈕下方是紫色的小巧方塊,上有黑色文字「New」。

接下來要建構的訊息資訊方塊比較貼近日常使用情境。這個例子與 HelloWorld 不同,會從本機存放區載入資料、從網路擷取要顯示的圖片,並直接從資訊方塊處理互動以開啟應用程式。

MessagingTileService

MessagingTileService 擴充了我們先前看到的 CoroutinesTileService 類別。

本例與前例的主要差異在於,現在我們要觀測存放區中的資料,並從網路擷取圖片資料。

網路呼叫等執行時間較長的作業,比較適合使用 WorkManager 這類的服務,因為資訊方塊服務函式的逾時時間相對較短。在本程式碼研究室中,我們不會介紹 WorkManager。如果有興趣自行嘗試,歡迎查看這個程式碼研究室

MessagingTileRenderer

MessagingTileRenderer 會擴充 TileRenderer 類別 (Horologist Tiles 中的另一個抽象層),且完全同步 - 狀態會傳遞至轉譯器函式,方便您在測試和 Android Studio 預覽中使用。

在下一個步驟中,您將瞭解如何新增資訊方塊的 Android Studio 預覽。

6. 新增預覽函式

我們可以使用 Horologist Tile 的 TileLayoutPreview 和類似功能,在 Android Studio 中預覽資訊方塊 UI。這可縮短開發 UI 時的意見回饋循環,大幅加快疊代速度。

我們會使用 Jetpack Compose 的工具來查看這個預覽畫面,因此下方預覽函式中會顯示 @Composable 註解。進一步瞭解可組合預覽,但並不需要完成本程式碼研究室。

在檔案結尾為新增MessagingTileRenderer的可組合預覽。

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

@WearDevicePreview
@Composable
fun MessagingTileRendererPreview() {
    TileLayoutPreview(
        state = MessagingTileState(MessagingRepo.knownContacts),
        resourceState = emptyMap(),
        renderer = MessagingTileRenderer(LocalContext.current)
    )
}

請注意,可組合函式會使用 TileLayoutPreview;我們不能直接預覽並排版面配置。

您可以使用「分割」編輯器模式預覽資訊方塊:

Android Studio 的分割畫面檢視畫面,左側是預覽程式碼,右側是資訊方塊的圖片。

我們會在 MessagingTileState 中傳送人工資料,但目前沒有任何資源狀態,因此可以傳遞空白地圖。

在下一步中,我們會使用 Tiles Material 來更新版面配置。

7. 新增 Tiles Material

Tiles Material 提供預先建立的 Material 元件和版面配置,可讓您建立採用 Wear OS 最新 Material 設計的資訊方塊。

將 Tiles Material 依附元件新增至 build.gradle 檔案:

start/build.gradle

implementation "androidx.wear.tiles:tiles-material:$tilesVersion"

根據設計的複雜度,建議您在同一檔案中使用頂層函式來封裝 UI 的邏輯單元,以便與轉譯器並列版面配置程式碼。

將按鈕的程式碼新增至轉譯器檔案底部,以及預覽:

start/src/main/java/MessagingTileRenderer.kt

private fun searchLayout(
    context: Context,
    clickable: ModifiersBuilders.Clickable,
) = Button.Builder(context, clickable)
    .setContentDescription(context.getString(R.string.tile_messaging_search))
    .setIconContent(MessagingTileRenderer.ID_IC_SEARCH)
    .setButtonColors(ButtonColors.secondaryButtonColors(MessagingTileTheme.colors))
    .build()

@IconSizePreview
@Composable
private fun SearchButtonPreview() {
    LayoutElementPreview(
        searchLayout(
            context = LocalContext.current,
            clickable = emptyClickable
        )
    ) {
        addIdToImageMapping(
            MessagingTileRenderer.ID_IC_SEARCH,
            drawableResToImageResource(R.drawable.ic_search_24)
        )
    }
}

LayoutElementPreviewTileLayoutPreview 相似,但用於個別元件,例如按鈕、方塊或標籤。結尾的 lambda 能讓我們指定資源 ID 對應 (對應至圖片資源),因此將 ID_IC_SEARCH 對應至搜尋圖片資源。

使用「分割」編輯器模式時,我們可以看到搜尋按鈕的預覽畫面:

預覽的垂直堆疊組合、頂端資訊方塊,以及下方的搜尋圖示按鈕。

我們可以採取類似的做法來建立聯絡人版面配置:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

private fun contactLayout(
    context: Context,
    contact: Contact,
    clickable: ModifiersBuilders.Clickable,
) = Button.Builder(context, clickable)
    .setContentDescription(contact.name)
    .apply {
        if (contact.avatarUrl != null) {
            setImageContent(contact.imageResourceId())
        } else {
            setTextContent(contact.initials)
            setButtonColors(ButtonColors.secondaryButtonColors(MessagingTileTheme.colors))
        }
    }
    .build()

Tiles Material 不只包含元件。與其使用一系列的巢狀結構欄和列,我們可以使用 Tiles Material 中的版面配置快速呈現所需外觀。

在這裡,我們可以使用 PrimaryLayoutMultiButtonLayout 排列 4 位聯絡人和搜尋按鈕:使用以下版面配置更新 MessagingTileRenderer 中的 messagingTileLayout() 函式:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

private fun messagingTileLayout(
    context: Context,
    deviceParameters: DeviceParametersBuilders.DeviceParameters,
    state: MessagingTileState
) = PrimaryLayout.Builder(deviceParameters)
    .setContent(
        MultiButtonLayout.Builder()
            .apply {
                // In a PrimaryLayout with a compact chip at the bottom, we can fit 5 buttons.
                // We're only taking the first 4 contacts so that we can fit a Search button too.
                state.contacts.take(4).forEach { contact ->
                    addButtonContent(
                        contactLayout(
                            context = context,
                            contact = contact,
                            clickable = emptyClickable
                        )
                    )
                }
            }
            .addButtonContent(searchLayout(context, emptyClickable))
            .build()
    )
    .build()

在 2x3 金字塔中包含 5 個按鈕的資訊方塊預覽。第 2 個和第 3 個按鈕是藍色填滿的圓圈,表示缺少圖片。

MultiButtonLayout 最多可支援 7 個按鈕,並會以適當的間距為您放置這些按鈕。讓我們在 messagingTileLayout() 函式的 PrimaryLayout 建構工具中,將「新增」方塊新增至 PrimaryLayout

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

.setPrimaryChipContent(
    CompactChip.Builder(
        /* context = */ context,
        /* text = */ context.getString(R.string.tile_messaging_create_new),
        /* clickable = */ emptyClickable,
        /* deviceParameters = */ deviceParameters
    )
        .setChipColors(ChipColors.primaryChipColors(MessagingTileTheme.colors))
        .build()
)

資訊方塊預覽有 5 個按鈕,下方有寫著「new」的小巧方塊

在下一個步驟中,我們會修正缺少的圖片。

8. 新增圖片

在資訊方塊上顯示本機圖片非常簡單:運用 Horologist Tile 便利函式載入可繪項目,並轉換為圖片資源,藉此提供圖片與用於版面配置的字串 ID 之間的對應。SearchButtonPreview 提供範例:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

addIdToImageMapping(
    ID_IC_SEARCH,
    drawableResToImageResource(R.drawable.ic_search_24)
)

如果是訊息資訊方塊,我們也必須從網路載入圖片 (而不僅是本機資源),並且使用 Coil (基於 Kotlin 協同程式的圖片載入器)。

已為此編寫程式碼:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileService.kt

override suspend fun resourcesRequest(requestParams: ResourcesRequest): Resources {
    val avatars = imageLoader.fetchAvatarsFromNetwork(
        context = this@MessagingTileService,
        requestParams = requestParams,
        tileState = latestTileState()
    )
    return renderer.produceRequestedResources(avatars, requestParams)
}

由於資訊方塊轉譯器完全同步,因此資訊方塊服務會從網路擷取點陣圖。和先前一樣,根據圖片大小,更適合使用 WorkManager 事先擷取圖片,但在本程式碼研究室中,我們不會直接擷取這些圖片。

我們會將 avatars 對應 (ContactBitmap) 傳遞至轉譯器做為資源的「狀態」。現在,轉譯器可以將這些點陣圖轉換為資訊方塊的圖片資源。

該程式碼也已撰寫完成:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

override fun ResourceBuilders.Resources.Builder.produceRequestedResources(
    resourceState: Map<Contact, Bitmap>,
    deviceParameters: DeviceParametersBuilders.DeviceParameters,
    resourceIds: MutableList<String>
) {
    addIdToImageMapping(
        ID_IC_SEARCH,
        drawableResToImageResource(R.drawable.ic_search_24)
    )

    resourceState.forEach { (contact, bitmap) ->
        addIdToImageMapping(
            /* id = */ contact.imageResourceId(),
            /* image = */ bitmap.toImageResource()
        )
    }
}

因此,如果服務正在擷取點陣圖,而轉譯器會將這些點陣圖轉換為圖片資源,為什麼資訊方塊未顯示圖片?

其實是有的!如果您在裝置 (可連上網際網路) 執行資訊方塊時,應會發現圖片確實載入。此問題只出現在預覽中,因為我們仍在為 resourceState 傳遞 emptyMap()

以實際的資訊方塊來說,我們會從網路擷取點陣圖並對應至不同的聯絡人,但對於預覽和測試,我們就不需要連線至網路。

更新 MessagingTileRendererPreview(),以便為兩名聯絡人提供點陣圖:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

@WearDevicePreview
@Composable
fun MessagingTileRendererPreview() {
    val state = MessagingTileState(MessagingRepo.knownContacts)
    val context = LocalContext.current
    TileLayoutPreview(
        state = state,
        resourceState = mapOf(
            state.contacts[1] to (context.getDrawable(R.drawable.ali) as BitmapDrawable).bitmap,
            state.contacts[2] to (context.getDrawable(R.drawable.taylor) as BitmapDrawable).bitmap,
        ),
        renderer = MessagingTileRenderer(context)
    )
}

現在,如果我們重新整理預覽畫面,圖片應顯示如下:

含有 5 個按鈕的資訊方塊預覽,這次兩個按鈕的圖片顯示為藍色圓圈

在下一個步驟中,我們會處理每個元素的點擊動作。

9. 處理互動

資訊方塊最實用的功能之一,就是提供關鍵使用者旅程的捷徑。這與僅開啟應用程式的應用程式啟動器不同:此處有空間為應用程式的特定畫面提供內容捷徑。

到目前為止,我們已使用方塊和每個按鈕的 emptyClickable。這對於沒有互動的預覽而言沒有問題,但接下來將說明如何為元素新增動作。

「ActionBuilders」類別的兩個建構工具定義了可點擊屬性動作:LoadActionLaunchAction

載入動作

如果您想在使用者點選元素 (例如遞增計數器) 時,在資訊方塊服務中執行邏輯,則可使用 LoadAction

.setClickable(
    Clickable.Builder()
        .setId(ID_CLICK_INCREMENT_COUNTER)
        .setOnClick(ActionBuilders.LoadAction.Builder().build())
        .build()
    )
)

點擊後,服務會呼叫 onTileRequest (CoroutinesTileService 中的 tileRequest),因此建議您重新整理資訊方塊使用者介面:

override suspend fun tileRequest(requestParams: TileRequest): Tile {
    if (requestParams.state.lastClickableId == ID_CLICK_INCREMENT_COUNTER) {
        // increment counter
    }
    // return an updated tile
}

推出動作

LaunchAction 可用來啟動活動。在 MessagingTileRenderer 中,請更新搜尋按鈕的可點擊元素。

搜尋按鈕是由 MessagingTileRenderer 中的 searchLayout() 函式定義。它已經使用 Clickable 做為參數,但到目前為止,我們已傳遞 emptyClickable,也就是在點選按鈕時不執行任何操作。

讓我們更新 messagingTileLayout(),以便傳遞真實的點擊操作。新增 searchButtonClickable 參數並傳遞至 searchLayout()

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

private fun messagingTileLayout(
    context: Context,
    deviceParameters: DeviceParametersBuilders.DeviceParameters,
    state: MessagingTileState,
    searchButtonClickable: ModifiersBuilders.Clickable
...
    .addButtonContent(searchLayout(context, searchButtonClickable))

由於我們新增了參數 (searchButtonClickable),因此也必須更新 renderTile,也就是呼叫 messagingTileLayout 的位置。我們會使用 launchActivityClickable() 函式建立新的可點擊元素,並傳遞 openSearch() ActionBuilder 做為動作:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

override fun renderTile(
    state: MessagingTileState,
    deviceParameters: DeviceParametersBuilders.DeviceParameters
): LayoutElementBuilders.LayoutElement {
    return messagingTileLayout(
        context = context,
        deviceParameters = deviceParameters,
        state = state,
        searchButtonClickable = launchActivityClickable("search_button", openSearch())
    )
}

開啟 launchActivityClickable 查看這些函式 (已定義) 的運作方式:

start/src/main/java/com/example/wear/tiles/messaging/tile/ClickableActions.kt

internal fun launchActivityClickable(
    clickableId: String,
    androidActivity: ActionBuilders.AndroidActivity
) = ModifiersBuilders.Clickable.Builder()
    .setId(clickableId)
    .setOnClick(
        ActionBuilders.LaunchAction.Builder()
            .setAndroidActivity(androidActivity)
            .build()
    )
    .build()

這與 LoadAction 非常類似,主要差別在於我們呼叫了 setAndroidActivity。在同一個檔案中,我們提供多種 ActionBuilder.AndroidActivity 範例。

針對 openSearch,我們將其用於可點擊屬性,會呼叫 setMessagingActivity 並額外傳遞字串,以識別這是哪個按鈕點擊。

start/src/main/java/com/example/wear/tiles/messaging/tile/ClickableActions.kt

internal fun openSearch() = ActionBuilders.AndroidActivity.Builder()
    .setMessagingActivity()
    .addKeyToExtraMapping(
        MainActivity.EXTRA_JOURNEY,
        ActionBuilders.stringExtra(MainActivity.EXTRA_JOURNEY_SEARCH)
    )
    .build()

...

internal fun ActionBuilders.AndroidActivity.Builder.setMessagingActivity(): ActionBuilders.AndroidActivity.Builder {
    return setPackageName("com.example.wear.tiles")
        .setClassName("com.example.wear.tiles.messaging.MainActivity")
}

執行資訊方塊,然後按一下搜尋按鈕。這樣會開啟 MainActivity 並顯示文字,確認使用者點選了搜尋按鈕。

為其他項新增動作的方法類似。ClickableActions 包含您需要的函式。如需提示,請查看 finished 模組中的 MessagingTileRenderer

10. 恭喜

恭喜!您已瞭解如何建構適用於 Wear OS 的資訊方塊!

後續步驟

詳情請參閱 GitHub 上的 Golden Tiles 實作Wear OS Tiles 指南