在 Wear OS 中建立第一個圖塊

1. 簡介

動畫手錶,使用者將錶面滑動至第一個圖塊,也就是預測,然後滑動至計時器圖塊,然後返回

Wear OS 圖塊可讓使用者輕鬆存取處理各種事務所需的資訊和動作。只要從錶面向上滑動,使用者就能查詢最新天氣預報或啟動計時器。

圖塊是系統 UI 的一部分,而不是在其專屬應用程式容器中執行。我們使用 Service 來描述圖塊的版面配置和內容。系統 UI 會隨之視需要算繪圖塊。

執行步驟

35a459b77a2c9d52.png

你會建構訊息應用程式的圖塊,以顯示最近的對話。在該途徑中,使用者可以直接跳到下列 3 項常見工作中的 1 項:

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

課程內容

在本程式碼研究室中,您將瞭解如何編寫自己的 Wear OS 圖塊,包括如何:

  • 建立 TileService
  • 在裝置上測試圖塊
  • 在 Android Studio 中預覽圖塊的使用者介面
  • 為圖塊開發 UI
  • 新增圖片
  • 處理互動

必要條件

2. 開始設定

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

您需要準備的項目

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

如果您不熟悉 Wear OS 的使用方式,請先參閱此快速指南,再開始進行操作。包括設定 Wear OS 模擬器的操作說明,並說明如何瀏覽系統。

下載程式碼

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

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

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

在 Android Studio 中開啟專案

在「歡迎使用 Android Studio」視窗中,選取 c01826594f360d94.png「開啟現有專案」「檔案」>「開啟」,然後選取[下載位置]資料夾。

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 啟動圖塊。在頂端面板的下拉式選單中選取 [編輯設定...]。

在 Android Studio 頂端面板中執行設定下拉式選單。編輯設定將醒目顯示。

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

按一下 [確定] 以結束程序。

透過名為「HelloTile」的 Wear OS 圖塊,來編輯「設定」選單。

使用 Direct Surface Launch 功能,我們就能透過 Wear OS 模擬器或實體裝置快速測試圖塊。嘗試執行「HelloTile」。螢幕截圖應如以下螢幕截圖所示。

圓形錶面顯示「建立圖塊的時間!」黑底白字

5. 建構訊息圖塊

圓形手錶,以 2 x 3 金字塔排列的 5 個圓形按鈕。第 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 指南