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

1. 簡介

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

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

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

執行步驟

35a459b77a2c9d52.png

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

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

課程內容

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

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

必要條件

2. 開始設定

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

軟硬體需求

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

下載程式碼

如果您已安裝 Git,只要執行下列指令即可複製這個存放區的程式碼。

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

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

在 Android Studio 中開啟專案

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

3. 建立基本資訊方塊

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

HelloWorldTileService

實作 TileService 的類別需要指定兩個方法:

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

第一個方法會傳回 Resources 物件,在當中將字串 ID 對應至要在資訊方塊中使用的圖片資源。

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

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

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

SuspendingTileService 提供兩個暫停函式,是 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 模組,即可在裝置或模擬器上安裝應用程式 (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 擴充了我們先前看到的 SuspendingTileService 類別。

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

MessagingTileRenderer

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

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

6. 新增預覽函式

我們可以使用 Jetpack Tiles 程式庫 1.4 版 (目前為 Alpha 版) 發布的 Tile 預覽函式,在 Android Studio 中預覽資訊方塊 UI。這可縮短開發 UI 時的回饋循環,提升開發速度。

在檔案結尾為 MessagingTileRenderer 新增資訊方塊預覽。

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

@Preview(device = WearDevices.SMALL_ROUND)
@Preview(device = WearDevices.LARGE_ROUND)
fun messagingTileLayoutPreview(context: Context): TilePreviewData {
    return TilePreviewData { request ->
        MessagingTileRenderer(context).renderTimeline(
            MessagingTileState(knownContacts),
            request
        )
    }
}

請注意,這裡「沒有」提供 @Composable 註解;雖然 Tiles 使用與 Composable 函式相同的預覽 UI,但 Tiles 不會使用 Compose,也不具可組合性質。

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

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

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

7. 新增 Tiles Material

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

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

start/build.gradle

implementation "androidx.wear.protolayout:protolayout-material:$protoLayoutVersion"

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

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()

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

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)
    .setResponsiveContentInsetEnabled(true)
    .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()

96fee80361af2c0f.png

MultiButtonLayout 最多可支援 7 個按鈕,且會以適當間距排列這些按鈕。

messagingTileLayout() 函式中新增「New」CompactChip,做為 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()
    )

2041bdca8a46458b.png

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

8. 新增圖片

大致來說,Tiles 包含兩項要素:版面配置元素 (依字串 ID 參照資源),以及資源本身 (可以是圖片)。

要取用本機圖片很簡單:雖然無法直接使用 Android 可繪製資源,但可以使用 Horologist 提供的便利函式,輕易地將其轉換為所需格式。接著,使用 addIdToImageMapping 函式,將圖片與資源 ID 建立關聯。例如:

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: List<String>
) {
    addIdToImageMapping(
        ID_IC_SEARCH,
        drawableResToImageResource(R.drawable.ic_search_24)
    )

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

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

其實是有的!如果您在裝置 (可連上網際網路) 執行資訊方塊,應會發現圖片確實載入。這個問題只出現在預覽中,因為我們尚未將任何資源傳遞至 TilePreviewData()

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

我們需要進行兩項變更。首先,請建立 previewResources() 函式,傳回 Resources 物件:

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

private fun previewResources() = Resources.Builder()
    .addIdToImageMapping(ID_IC_SEARCH, drawableResToImageResource(R.drawable.ic_search_24))
    .addIdToImageMapping(knownContacts[1].imageResourceId(), drawableResToImageResource(R.drawable.ali))
    .addIdToImageMapping(knownContacts[2].imageResourceId(), drawableResToImageResource(R.drawable.taylor))
    .build()

接著,更新 messagingTileLayoutPreview() 以傳入資源:

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

@Preview(device = WearDevices.SMALL_ROUND)
@Preview(device = WearDevices.LARGE_ROUND)
fun messagingTileLayoutPreview(context: Context): TilePreviewData {
    return TilePreviewData({ previewResources() }) { request ->
        MessagingTileRenderer(context).renderTimeline(
            MessagingTileState(knownContacts),
            request
        )
    }
}

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

3142b42717407059.png

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

9. 處理互動

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

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

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

載入動作

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

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

點擊後,服務會呼叫 onTileRequest (SuspendingTileService 中的 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() 來傳遞實際的點擊動作。

  1. 新增類型為 ModifiersBuilders.ClickablesearchButtonClickable 參數。
  2. 將此參數傳遞至現有的 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")
}

執行資訊方塊 (請務必執行「messaging」資訊方塊,而非「hello」資訊方塊),然後按一下搜尋按鈕。這樣會開啟 MainActivity 並顯示文字,確認使用者點選了搜尋按鈕。

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

10. 恭喜

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

後續步驟

詳情請參閱 GitHub 上的 Golden Tiles 實作方式Wear OS 資訊方塊指南設計指南