在 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 資訊方塊指南設計指南