在 Wear OS 中创建首个 Tile

1. 简介

Three tiles: fitness, messages, calendar.

Wear OS Tile 提供便捷方式让用户获取所需信息并执行操作。只需从表盘轻扫一下,用户即可查看最新天气预报或启动计时器。

Tile 作为系统界面的一部分运行,而非在自己的应用容器中运行。我们使用 Service 来描述 Tile 的布局和内容。系统界面随后会在需要时渲染 Tile。

您将完成的操作

656045bed9c45083.png

您将为消息应用构建一个 Tile,用于显示最近的对话。用户可从此界面跳转到三项常见任务:

  • 打开对话
  • 撰写新消息

您将学到什么

在此 Codelab 中,您将学习如何编写自己的 Wear OS Tile,包括如何:

  • 创建 TileService
  • 在设备上测试 Tile
  • 在 Android Studio 中预览 Tile 界面
  • 开发 Tile 界面
  • 添加图片
  • 处理交互

前提条件

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. 创建基本 Tile

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

HelloWorldTileService

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

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

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

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

start 模块打开 HelloWorldTileService.kt。您将进行的所有更改都将在此模块中。如果您想查看此 Codelab 的结果,还有一个 finished 模块可供参考。

HelloWorldTileService 扩展了 Horologist Tiles 库中与 Kotlin 协程兼容的封装类 SuspendingTileServiceHorologist 是 Google 提供的一组库,旨在为 Wear OS 开发者补充 Jetpack 中尚未提供但开发者常用的功能。

SuspendingTileService 提供了两个 suspend 函数,它们是 TileService 中函数的协程等效项:

  • suspend resourcesRequest(requestParams: ResourcesRequest): Resources
  • suspend tileRequest(requestParams: TileRequest): Tile

要详细了解协程,请查阅Android 上的 Kotlin 协程文档。

HelloWorldTileService 尚未完成。我们需要在清单中注册该服务,并且还需要为 tileLayout 提供实现。

注册 Tile 服务

一旦 Tile 服务在清单中注册,它将显示在可供用户添加的 Tile 列表中。

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

首次加载 Tile 或加载 Tile 出错时,会使用图标和标签(作为占位符)。末尾的元数据定义了一个预览图片,该图片在用户添加 Tile 时显示在轮播界面中。

为 Tile 定义布局

HelloWorldTileService 有一个名为 tileLayout 的函数,其函数体为 TODO()。现在,我们将其替换为定义 Tile 布局的实现。

start/src/main/java/com/example/wear/tiles/hello/HelloWorldTileService.kt

fun tileLayout(
    context: Context,
    deviceConfiguration: DeviceParametersBuilders.DeviceParameters,
    message: String,
) =
    materialScope(
        context = context,
        deviceConfiguration = deviceConfiguration,
        allowDynamicTheme = false,
    ) {
        primaryLayout(mainSlot = { text(message.layoutString) })
    }

您的第一个 Wear OS Tile 就这样创建完成了!现在我们来安装这个 Tile 并看看它的样子。

4. 在设备上测试您的 Tile

在运行配置下拉菜单中选择 start 模块后,您可以将应用(即 start 模块)安装到您的设备或模拟器上,然后像用户一样手动安装 Tile。

然而,Android Studio 提供了一个快捷方式:通过点击边槽中的“运行服务”图标 (▷),然后选择“运行 ‘HelloWorldTileService’”,它将在连接的设备上安装并启动 Tile。

ded9f9355abd02f3.png

选择“运行 ‘HelloWorldTileService’”以在连接的设备上构建并运行您的 Tile。它应如下图所示。

693c130912097be6.png

显示屏顶部出现的“挥手”图标由系统提供。要更改它,请修改清单中 Tile 的 <service> 元素的 android:icon 属性。

为方便起见,此过程还会创建一个“HelloWorldTileService”的“运行配置”,以供将来使用。

b3335148771abbeb.png

5. 添加预览函数

我们可以在 Android Studio 中预览 Tile 界面。这缩短了开发界面时的反馈循环,提高了开发速度。

HelloWorldTileService.kt 文件的末尾为 HelloWorldTileService 添加 Tile 预览。

start/src/main/java/com/example/wear/tiles/hello/HelloWorldTileService.kt

@Preview(device = WearDevices.SMALL_ROUND, name = "Small Round")
@Preview(device = WearDevices.LARGE_ROUND, name = "Large Round")
internal fun helloLayoutPreview(context: Context): TilePreviewData {
    return TilePreviewData {
        TilePreviewHelper.singleTimelineEntryTileBuilder(
            helloLayout(context, it.deviceConfiguration, "Hello, preview tile!")
        )
            .build()
    }
}

使用“拆分”编辑器模式查看 Tile 预览

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

请注意,此处使用 @Composable 注解—尽管 Tile 使用与可组合函数相同的预览界面,但 Tile 不使用 Compose,并且不可组合。

6. 构建消息 Tile

cf18db0f604b1999.png

我们将要构建的消息 Tile 更符合实际应用中的 Tile。与 HelloWorld 示例不同,此 Tile 演示了 Material 3 Expressive 组件,显示图片,并处理交互以打开应用。

MessagingTileService

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

7. 添加界面组件

ProtoLayout 库提供了预构建的组件和布局,让您可以创建符合 Wear OS 最新 Material 3 Expressive 设计的 Tile。

将 Tiles Material 依赖项添加到您的 build.gradle 文件中

start/build.gradle

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

将布局代码添加到 tileLayout() 函数中,作为 materialScope() 函数的主体。这将创建一个包含两行(每行两个按钮)和一个边缘按钮的布局。

找到行“TODO() // Add primaryLayout()”,并将其替换为以下代码。

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

primaryLayout(
    mainSlot = {
        // This layout code assumes "contacts" contains at least 4 elements, for sample code
        // that can handle an arbitrary number of contacts, and also shows different numbers
        // of contacts based on the physical screen size, see
        // <https://github.com/android/wear-os-samples/tree/main/WearTilesKotlin>.
        Column.Builder()
            .apply {
                setWidth(expand())
                setHeight(expand())
                addContent(
                    buttonGroup {
                        buttonGroupItem { contactButton(contacts[0]) }
                        buttonGroupItem { contactButton(contacts[1]) }
                    }
                )
                addContent(DEFAULT_SPACER_BETWEEN_BUTTON_GROUPS)
                addContent(
                    buttonGroup {
                        buttonGroupItem { contactButton(contacts[2]) }
                        buttonGroupItem { contactButton(contacts[3]) }
                    }
                )
            }
            .build()
    },
    bottomSlot = {
        textEdgeButton(
            onClick = clickable(), // TODO: Launch new conversation activity
            labelContent = { text("New".layoutString) },
        )
    },
)

同一文件中的 contactButton() 函数用于创建各个联系人按钮。如果联系人有相关图片,则该图片会显示在按钮上。否则,会使用联系人的姓名首字母缩写。

此时您可能会注意到,尽管总体布局正确,但图片缺失。

809bdb9d1213c376.png

如果您将 Tile 部署到设备上,也会看到同样的情况

4671bb2eafdcc528.png

在下一步中,我们将修复缺失的图片。

8. 添加图片

从宏观层面看,Tile 由两部分组成:布局(通过字符串 ID 引用资源)和资源本身(可以是图片)。

目前,我们的代码提供了布局,但没有提供资源本身。要修复预览,我们需要提供图片“资源”。为此,请找到“TODO: Add onTileResourceRequest”并将以下代码作为额外的命名参数添加到 TilePreviewData() 中。

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

// Additional named argument to TilePreviewData
onTileResourceRequest = { resourcesRequest ->
    Resources.Builder()
        .setVersion(resourcesRequest.version)
        .apply {
            contacts.forEach {
                if (it.avatarSource is AvatarSource.Resource) {
                    addIdToImageMapping(
                        it.imageResourceId(),
                        it.avatarSource.resourceId
                    )
                }
            }
        }
        .build()
}

图片现在应显示在预览中

e77d746268f293f2.png

但是,如果 Tile 部署到设备上,图片将缺失。要解决此问题,请将 Service.kt 中的 resourcesRequest() 函数替换为以下内容:

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

override suspend fun resourcesRequest(
    requestParams: ResourcesRequest
): Resources {
    // resourceIds is a list of the ids we need to provide images for. If we're passed an empty
    // list, set resourceIds to all resources.
    val resourceIds =
        requestParams.resourceIds.ifEmpty {
            contacts.map { it.imageResourceId() }
        }

    // resourceMap maps (tile) resource ids to (Android) resource ids.
    val resourceMap =
        contacts
            .mapNotNull {
                when (it.avatarSource) {
                    is AvatarSource.Resource ->
                        it.imageResourceId() to
                            it.avatarSource.resourceId
                    else -> null
                }
            }
            .toMap()
            .filterKeys {
                it in resourceIds
            } // filter to only the resources we need

    // Add images in the resourceMap to the Resources object, and return the result.
    return Resources.Builder()
        .setVersion(requestParams.version)
        .apply {
            resourceMap.forEach { (id, imageResource) ->
                addIdToImageMapping(id, imageResource)
            }
        }
        .build()
}

现在,将 Tile 部署到设备时也会显示图片

cf18db0f604b1999.png

在下一步中,我们将处理各个元素的点击事件。

9. 处理交互

使用 Tile 最有用的功能之一是为关键的用户旅程提供快捷方式。这与仅打开应用的启动器不同 - 在这里,我们有空间提供进入应用中特定屏幕的上下文快捷方式。

到目前为止,我们一直对芯片和每个按钮使用无参数 clickable() 提供的虚拟操作。这对于非交互式预览来说没问题,但现在我们来看看如何为元素添加操作。

LaunchAction

LaunchAction 可用于启动 Activity。我们来修改 Layout,以便点击“新建”按钮可启动“新对话”用户旅程。

找到行“TODO: Launch new conversation activity”并将 clickable() 替换为

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

clickable(
    id = "new_button",
    action =
        launchAction(
            ComponentName(
                "com.example.wear.tiles",
                "com.example.wear.tiles.messaging.MainActivity",
            ),
            mapOf(
                MainActivity.EXTRA_JOURNEY to
                    ActionBuilders.stringExtra(
                        MainActivity.EXTRA_JOURNEY_NEW
                    )
            ),
        ),
)

重新部署 Tile。现在,点击“新建”将不再是无操作,而是启动 MainActivity 并开始“新对话”用户旅程:

a08c28b4a142fb8f.png

同样,修改 Layout,以便点击联系人按钮可与特定用户开始对话。

找到行“Launch open conversation activity”并将 clickable() 替换为

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

clickable(
    id = contact.id.toString(),
    action =
        launchAction(
            ComponentName(
                "com.example.wear.tiles",
                "com.example.wear.tiles.messaging.MainActivity",
            ),
            mapOf(
                MainActivity.EXTRA_JOURNEY to
                    ActionBuilders.stringExtra(
                        MainActivity
                            .EXTRA_JOURNEY_CONVERSATION
                    ),
                MainActivity.EXTRA_CONVERSATION_CONTACT to
                    ActionBuilders.stringExtra(
                        contact.name
                    ),
            ),
        ),
)

重新部署 Tile。现在,点击某个联系人将不再是无操作,而是与他们开始对话

b684a1ced0b226f9.png

10. 恭喜

恭喜!您已学会如何为 Wear OS 构建 Tile!

接下来?

如需了解更多信息,请查看 GitHub 上的 Golden Tiles 实现Wear OS Tile 指南设计准则