在 Wear OS 中创建您的第一个卡片

1. 简介

手表动画演示:用户将表盘滑动到第一个预测卡片上,再滑动到一个计时器卡片,然后再返回

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

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

实践内容

35a459b77a2c9d52.png

您将为一款即时通讯应用构建卡片,其中将显示近期对话。通过此 surface,用户可以直接执行以下三项常见任务中的一项:

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

学习内容

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

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

前提条件

  • Kotlin 有基本的了解

2. 准备工作

在此步骤中,您将设置环境并下载入门级项目

所需条件

  • Android Studio Dolphin (2021.3.1) 或更高版本
  • Wear OS 设备或模拟器(第一次使用?这里介绍了设置方法)

下载代码

如果您已安装 git,则只需运行以下命令即可克隆此代码库中的代码。如需检查是否已安装 git,请在终端或命令行中输入 git –version,并验证其是否正确执行。

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

如果您未安装 git,可以点击下方按钮下载此 Codelab 的全部代码:

在 Android Studio 中打开项目

在“Welcome to Android Studio”窗口中,选择 c01826594f360d94.png Open an Existing Project 或依次选择 File > Open,然后选择文件夹 [Download Location]

3.创建基本卡片

卡片的入口点是卡片服务。在此步骤中,您将注册一项卡片服务,并定义该卡片的布局。

HelloWorldTileService

实现 TileService 的类需要实现两个函数:

  • onResourcesRequest
  • onTileRequest

第一个函数会将字符串 ID 映射到图片资源,第二个函数会返回卡片的说明(包括其布局)。

打开 HelloWorldTileService.kt。此类扩展了 CoroutinesTileService,后者是一个来自 Horologist Tiles 库的对 Kotlin 协程友好的封装容器。该封装容器提供了两个挂起函数,这两个挂起函数是上述函数的协程版本:

  • resourcesRequest
  • tileRequest

如需详细了解协程,请参阅 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>

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

定义卡片布局

返回卡片服务,现在提供 tileLayout 函数的实现。我们需要在此处定义卡片的布局并绑定数据。

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 推出的 Direct Surface Launch 功能来创建一个新的运行配置,以便直接从 Android Studio 启动卡片。从顶部面板的下拉菜单中选择“Edit Configurations...”。

从 Android Studio 的顶部面板中运行配置下拉菜单。Edit configurations 已高亮显示。

点击“Add new configuration”按钮,然后选择“Wear OS Tile”。添加描述性名称,然后选择 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 扩展了我们之前看到的 CoroutinesTileService 类。

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

对于任何可能长时间运行的工作(例如网络调用),更适合使用 WorkManager 之类的工具,因为卡片服务函数的超时时间相对较短。在此 Codelab 中,我们不会介绍 WorkManager。若要亲自实践,请查看此 Codelab

MessagingTileRenderer

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

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

6. 添加预览函数

我们可以使用 Horologist Tile 中的 TileLayoutPreview(或类似方法)来预览 Android Studio 中的卡片界面。这样可以缩短开发界面时的反馈环,从而加快迭代速度。

在文件末尾为 MessagingTileRenderer 添加可组合预览

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

@WearSmallRoundDevicePreview
@Composable
fun MessagingTileRendererPreview() {
    TileLayoutPreview(
        state = MessagingTileState(MessagingRepo.knownContacts),
        resourceState = emptyMap(),
        renderer = MessagingTileRenderer(LocalContext.current)
    )
}

请注意,可组合函数使用 TileLayoutPreview;我们无法直接预览卡片布局。使用“拆分”编辑器模式,我们可以看到卡片的预览:

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

我们在 MessagingTileState 中传递人工数据,并且还没有任何资源状态,因此可以传递空映射。

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

7. 添加卡片材料

Tiles Material 提供预构建的 Material 组件和布局,让您可以创建支持适用于 Wear OS 的最新 Material Design 的卡片。

将 Tiles Material 依赖项添加到 build.gradle 文件中(请参阅此处的版本):

start/build.gradle

def tilesVersion = <latest-stable-version>
implementation "androidx.wear.tiles:tiles-material:$tilesVersion"

根据设计的复杂程度,将代码与渲染器共置极具实用性,因为您可以使用同一个文件中的顶级函数来封装界面的逻辑单元。

将按钮的代码添加到渲染程序文件的底部,并添加预览:

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

@IconSizePreview
@Composable
private fun SearchButtonPreview() {
    LayoutElementPreview(
        searchLayout(
            context = LocalContext.current,
            clickable = emptyClickable
        )
    ) {
        addIdToImageMapping(
            MessagingTileRenderer.ID_IC_SEARCH,
            drawableResToImageResource(R.drawable.ic_search_24)
        )
    }
}

LayoutElementPreview 类似于 TileLayoutPreview,但用于单个组件,例如按钮、条状标签或标签。借助末尾的尾随 lambda,我们可以指定资源 ID 映射(到图片资源),因此在此处,我们将 ID_IC_SEARCH 映射到搜索图片资源。

使用“拆分”编辑器模式,我们可以看到搜索按钮的预览:

一组垂直堆叠的预览,顶部是卡片,下方是搜索图标按钮。

我们也可以执行类似操作来构建联系人布局:

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 个联系人和搜索按钮:

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)
    .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()

包含 2x3 金字塔中的 5 个按钮的卡片预览。第 2 个和第 3 个按钮显示为蓝色实心圆圈,指示缺少图片。

MultiButtonLayout 最多支持 7 个按钮,这些按钮将为您留出适当的间距。接下来将“New”条状标签也添加到 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()
)

卡片预览:包含 5 个按钮,下方还有一个文本为“New”的紧凑条状标签

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

8. 添加图片

在卡片上显示本地图片是一项简单任务:使用 Horologist 卡片中的便捷函数加载可绘制对象并将其转换为图片资源,以便提供从布局中使用的字符串 ID 到图片的映射。

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

addIdToImageMapping(
    ID_IC_SEARCH,
    drawableResToImageResource(R.drawable.ic_search_24)
)

对于消息卡片,我们还需要从网络加载图片,为此,我们使用 Cil(一种基于 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(
    resourceResults: Map<Contact, Bitmap>,
    deviceParameters: DeviceParametersBuilders.DeviceParameters,
    resourceIds: MutableList<String>
) {
    addIdToImageMapping(
        ID_IC_SEARCH,
        drawableResToImageResource(R.drawable.ic_search_24)
    )

    resourceResults.forEach { (contact, bitmap) ->
        addIdToImageMapping(
            /* id = */ contact.imageResourceId(),
            /* image = */ bitmap.toImageResource()
        )
    }
}

因此,如果服务要提取位图,而渲染程序要将这些位图转换为图片资源,那么卡片为何未显示图片?

没错!如果您在具有互联网连接的设备上运行卡片,那么您应当看到图片确实进行了加载。问题仅存在于预览环节,因为我们仍在为 resourceState 传递 emptyMap()

对于实际卡片,我们将从网络中提取位图并将其映射到不同的联系人,但对于预览和测试,我们完全不需要连接到网络。更新预览,以便我们为两个联系人提供所需的位图:

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

resourceState = mapOf(
    state.contacts[1] to (context.getDrawable(R.drawable.ali) as BitmapDrawable).bitmap,
    state.contacts[2] to (context.getDrawable(R.drawable.taylor) as BitmapDrawable).bitmap,
)

现在,如果我们刷新预览,图片应如下所示:

卡片预览:包含 5 个按钮,这次在带有蓝色圆圈的两个按钮中显示了照片

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

9. 处理互动

我们可以在卡片上实现的最为实用的一项操作就是提供到关键用户历程的快捷方式。这与仅打开应用的应用启动器不同 - 在此处,我们有空间提供一些上下文快捷方式,让用户能够快捷访问应用中的特定屏幕。

到目前为止,我们针对条状标签和每个按钮均使用了 emptyClickable。对于不支持互动的预览,这种方式效果不错。现在,让我们来看看如何为这些元素添加操作。

“ActionBuilders”类中的两个构建器定义了可点击的操作:LoadActionLaunchAction

LoadAction

如果您希望在用户点击某个元素时在卡片服务中执行逻辑(例如,增加计数器),则可以使用 LoadAction

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

点击后,系统会在您的服务(CoroutinesTileService 中的 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。

searchLayout 函数已经接受 Clickable 作为参数,但我们目前从 messagingLayout 函数传递 emptyClickable。我们提升该参数,以便其从 renderTile 一直进行传递。

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())
    )
}

调用 searchLayout 时,请记得替换 emptyClickable

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

打开 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")
}

运行卡片并点击搜索按钮。系统应打开 MainActivity 并显示文本以确认已点击搜索按钮。

为其他卡片添加操作的方法也是类似的。ClickableActions 包含您需要的函数。如果您需要提示,请查看 finished 模块中的 MessagingTileRenderer

10. 恭喜

恭喜!您已了解如何构建适用于 Wear OS 的卡片!

后续操作

如需了解详情,请参阅 GitHub 上的 Golden Tiles 实现Wear OS 卡片指南