在 Wear OS 中创建你的第一个磁贴

1. 简介

animated watch, user swiping the watch face to the first tile which is a forecast, then to a timer tile, and back

Wear OS 磁贴 提供了用户完成任务所需信息和操作的便捷访问方式。用户只需从表盘上轻轻滑动,即可查看最新的天气预报或启动计时器。

磁贴作为系统 UI 的一部分运行,而不是在自己的应用程序容器中运行。我们使用 **服务** 来描述磁贴的布局和内容。系统 UI 随后会在需要时渲染磁贴。

你将要做什么

35a459b77a2c9d52.png

你将为一个消息应用程序构建一个磁贴,用于显示最近的对话。用户可以通过此界面直接进入三个常见任务

  • 打开对话
  • 搜索对话
  • 撰写新消息

你将学到什么

在本 Codelab 中,你将学习如何编写自己的 Wear OS 磁贴,包括如何

  • 创建 TileService
  • 在设备上测试磁贴
  • 在 Android Studio 中预览磁贴 UI
  • 开发磁贴的 UI
  • 添加图像
  • 处理交互

先决条件

  • 了解 Kotlin 的基本知识

2. 设置环境

在此步骤中,您将设置您的环境并下载启动项目。

你需要什么

如果您不熟悉使用 Wear OS,建议您在开始之前 阅读本快速指南。其中包含设置 Wear OS 模拟器的说明,并介绍了如何在系统中导航。

下载代码

如果您已安装 git,则只需运行以下命令即可从 此仓库 克隆代码。

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

如果您没有安装 git,可以点击以下按钮下载此 Codelab 的所有代码

在 Android Studio 中打开项目

在“欢迎使用 Android Studio”窗口中,选择 c01826594f360d94.png **打开现有项目** 或 **文件 > 打开**,然后选择文件夹 **[下载位置]**。

3. 创建基本磁贴

磁贴的入口点是磁贴服务。在此步骤中,您将注册磁贴服务并为磁贴定义布局。

HelloWorldTileService

实现 TileService 的类需要指定两种方法

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

第一个方法返回一个 Resources 对象,该对象将字符串 ID 映射到我们将在磁贴中使用的图像资源。

第二个方法返回磁贴的描述,包括其布局。在这里,我们定义磁贴的布局以及如何将数据绑定到它。

start 模块打开 HelloWorldTileService.kt。您将在此模块中进行所有更改。如果您想查看此 Codelab 的结果,还有一个 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 中引入的一项功能,以创建一个新的运行配置,以便直接从 Android Studio 启动我们的磁贴。从顶部面板的下拉列表中选择“编辑配置...”。

Run configuration dropdown from the top panel in Android Studio. Edit configurations is highlighted.

单击“添加新配置”按钮,然后选择“Wear OS 磁贴”。添加一个描述性名称,然后选择 Tiles_Code_Lab.start 模块和 HelloWorldTileService 磁贴。

按“确定”完成。

Edit Configuration menu with a Wear OS Tile called HelloTile being configured.

直接界面启动允许我们快速在 Wear OS 模拟器或物理设备上测试磁贴。尝试运行“HelloTile”。它应该如下面的屏幕截图所示。

Round watch showing 'Time to create a tile!' in white writing on a black background

5. 构建消息磁贴

Round watch showing 5 round buttons arranged in a 2x3 pyramid. The 1st and 3rd button show initials in a purple text, the 2nd and 4th show profile pictures, and the last button is a search icon. Below the buttons is a purple compact chip that reads 'New' in black text.

我们即将构建的消息磁贴更类似于现实世界的磁贴。与 HelloWorld 示例不同,此磁贴从本地存储库加载数据,从网络获取要显示的图像,并处理从磁贴直接打开应用程序的交互。

MessagingTileService

MessagingTileService 扩展了我们之前看到的 SuspendingTileService 类。

此示例与前一个示例的主要区别在于,我们现在正在观察存储库中的数据,并且还从网络获取图像数据。

MessagingTileRenderer

MessagingTileRenderer 扩展了 SingleTileLayoutRenderer 类(来自 Horologist Tiles 的另一个抽象)。它是完全同步的:状态传递给渲染器函数,这使得在测试和 Android Studio 预览中更容易使用。

在下一步中,我们将了解如何为磁贴添加 Android Studio 预览。

6. 添加预览功能

我们可以使用 Jetpack Tiles 库 1.4 版(目前处于 Alpha 阶段)中发布的磁贴预览功能在 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,也不是可组合的。

使用“拆分”编辑器模式查看磁贴的预览

split screen view of Android Studio with preview code on the left and an image of the tile on the right.

在下一步中,我们将使用 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() 函数中,将一个“新建”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 将图像与资源标识符关联。例如

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

如果希望在用户点击元素时在图块服务中执行逻辑(例如,递增计数器),则可以使用 LoadAction

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

当单击此操作时,将在您的服务中调用 onTileRequestSuspendingTileService 中的 tileRequest),因此这是一个刷新图块 UI 的好机会。

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

LaunchAction

LaunchAction 可用于启动 Activity。在 MessagingTileRenderer 中,让我们更新搜索按钮的可点击操作。

搜索按钮由 MessagingTileRenderer 中的 searchLayout() 函数定义。它已经将 Clickable 作为参数,但到目前为止,我们一直传递 emptyClickable,这是一种在单击按钮时不执行任何操作的空操作实现。

让我们更新 messagingTileLayout(),以便它传递一个真正的点击操作。

  1. 添加一个新参数 searchButtonClickable(类型为 ModifiersBuilders.Clickable)。
  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))

我们还需要更新 renderTile,因为我们刚刚添加了一个新参数(searchButtonClickable),这是我们调用 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 图块指南 以及 设计指南