在 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
  • 添加图像
  • 处理交互

先决条件

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模块)安装到你的设备或模拟器上,并像用户一样手动安装磁贴。

但是,对于开发,让我们使用“直接界面启动”(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. 添加预览函数

我们可以在 Android Studio 中使用 Jetpack Tiles 库 1.4 版本(目前处于 Alpha 阶段)中发布的磁贴预览功能来预览磁贴 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 可用于启动活动。在 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 磁贴指南设计指南