在 Wear OS 中创建您的第一个功能块

1. 简介

手表动画演示:用户将表盘滑动到第一个天气预报功能块上,再滑动到一个计时器功能块,然后再返回

用户可通过 Wear OS 功能块轻松访问所需的信息和操作,顺利完成各种任务。只需在表盘上轻轻滑动一下,用户即可查看最新的天气预报或启动定时器。

功能块作为系统界面的一部分运行,而不是在其专属的应用容器中运行。我们使用 Service 来描述功能块的布局和内容。系统界面随后会根据需要渲染功能块。

实践内容

35a459b77a2c9d52.png

您将为一款即时通讯应用构建功能块,在其中显示近期对话。通过该功能块界面,用户可以跳转到以下三个常见任务:

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

学习内容

在此 Codelab 中,您将学习如何编写自己的 Wear OS 功能块,包括如何执行以下操作:

  • 创建 TileService
  • 在设备上测试功能块
  • 在 Android Studio 中预览功能块的界面
  • 为功能块开发界面
  • 添加图片
  • 处理互动

前提条件

  • 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 中打开项目

在“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 模块,供您查看此 Codelab 的成果。

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”(修改配置)已高亮显示。

点击“Add new configuration”(添加新配置)按钮,然后选择“Wear OS Tile”(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 个圆形按钮(2x3 金字塔形)的圆形手表。第 1 个和第 3 个按钮以紫色文字显示首字母缩写,第 2 个和第 4 个按钮显示个人资料照片,最后一个按钮显示搜索图标。按钮下方是紫色紧凑条状标签,其中显示黑色文本的“New”。

我们要构建的消息功能块更像是一个真实的功能块。与 HelloWorld 示例不同,此示例将从本地代码库加载数据、从网络获取要显示的图片,并直接从功能块处理互动以打开应用。

MessagingTileService

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

此示例与上一个示例的主要区别在于,我们现在从代码库观察数据,并从网络中提取图片数据。

MessagingTileRenderer

MessagingTileRenderer 扩展了 SingleTileLayoutRenderer 类(这是 Horologist Tiles 的另一种抽象化形式)。它是完全同步的:状态会传递到渲染函数,让用户能够更轻松地在测试和 Android Studio 预览中使用。

在下一步中,我们将介绍如何为功能块添加 Android Studio 预览。

6. 添加预览函数

在 Android Studio 中,我们可以使用 Jetpack Tiles 库 1.4 版(目前为 Alpha 版)中发布的功能块预览函数来预览功能块界面。这样可以缩短开发界面时的反馈环,从而加快开发速度。

在文件末尾,为 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 注释。尽管功能块使用与可组合函数相同的预览界面,但功能块不使用 Compose,也不是可组合项。

使用“拆分”编辑器模式查看功能块预览:

Android Studio 的分屏视图,左侧是预览代码,右侧是功能块图片。

在下一步中,我们将使用 Tiles Material 更新布局。

7. 添加 Tiles Material

Tiles Material 提供预构建的 Material 组件布局,因此您可以创建支持适用于 Wear OS 的最新 Material Design 的功能块。

将 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 的“primary”条状标签:

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. 添加图片

大体上讲,功能块由两部分组成:布局元素(通过字符串 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 提前获取图片。但在本 Codelab 中,我们将直接获取图片。

我们将 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()
    )
)

点击后,系统会在您的服务(SuspendingTileService 中的 tileRequest)中调用 onTileRequest,因此这是刷新功能块界面的绝佳机会:

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 中,更新搜索按钮的 Clickable。

搜索按钮由 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(我们在其中调用 messagingTileLayout),因为我们刚刚添加了一个新参数 (searchButtonClickable)。我们将使用 launchActivityClickable() 函数创建新的 Clickable,并传递 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 示例。

对于用于此 Clickable 的 openSearch,我们调用了 setMessagingActivity 并传递字符串 extra 来标识这是哪个按钮点击。

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 功能块指南设计准则