1. 简介
Wear OS 磁贴 提供了用户完成任务所需信息和操作的便捷访问方式。用户只需从表盘上轻轻滑动,即可查看最新的天气预报或启动计时器。
磁贴作为系统 UI 的一部分运行,而不是在自己的应用程序容器中运行。我们使用一个服务来描述磁贴的布局和内容。然后,系统 UI 会在需要时呈现磁贴。
你将要做什么
你将为一个消息应用程序构建一个磁贴,该磁贴显示最近的对话。通过此界面,用户可以跳转到三个常见任务
- 打开对话
- 搜索对话
- 撰写新消息
你将学到什么
在本 Codelab 中,你将学习如何编写自己的 Wear OS 磁贴,包括如何
- 创建
TileService
- 在设备上测试磁贴
- 在 Android Studio 中预览磁贴的 UI
- 开发磁贴的 UI
- 添加图像
- 处理交互
先决条件
- 对Kotlin 的基本了解
2. 设置环境
在此步骤中,你将设置你的环境并下载入门项目。
你需要什么
- Android Studio Koala 功能更新 | 2024.1.2 Canary 1 或更高版本
- Wear OS 设备或模拟器
如果你不熟悉使用 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”窗口中,选择 打开现有项目或文件 > 打开,然后选择文件夹[下载位置]。
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 启动我们的磁贴。从顶部面板的下拉列表中选择“编辑配置...”。
单击“添加新配置”按钮,然后选择“Wear OS 磁贴”。添加一个描述性名称,然后选择Tiles_Code_Lab.start
模块和HelloWorldTileService
磁贴。
按“确定”完成。
“直接界面启动”允许我们快速在 Wear OS 模拟器或物理设备上测试磁贴。尝试运行“HelloTile”。它应该如下面的屏幕截图所示。
5. 构建消息磁贴
我们即将构建的消息磁贴更像是现实世界中的磁贴。与 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,也不是可组合的。
使用“拆分”编辑器模式查看磁贴的预览。
在下一步中,我们将使用 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 中的布局来快速实现我们想要的外观。
在这里,我们可以使用 PrimaryLayout
和 MultiButtonLayout
来排列 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()
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()
)
在下一步中,我们将修复缺少的图像。
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
映射(Contact
到 Bitmap
)作为资源的“状态”传递给渲染器。现在,渲染器可以将这些位图转换为磁贴的图像资源。
此代码也已编写。
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
)
}
}
现在,如果我们刷新预览,图像应该会显示。
在下一步中,我们将处理每个元素的点击事件。
9. 处理交互
我们可以使用磁贴执行的最有用的操作之一是提供关键用户旅程的快捷方式。这与应用程序启动器不同,应用程序启动器只是打开应用程序——在这里,我们有空间提供到应用程序中特定屏幕的上下文快捷方式。
到目前为止,我们一直在为芯片和每个按钮使用 emptyClickable
。这对于非交互式的预览来说是可以的,但让我们看看如何为元素添加操作。
来自“ActionBuilders”类的两个构建器定义了可点击操作:LoadAction
和 LaunchAction
。
LoadAction
如果希望在用户点击元素时在磁贴服务中执行逻辑(例如,递增计数器),则可以使用 LoadAction
。
.setClickable(
Clickable.Builder()
.setId(ID_CLICK_INCREMENT_COUNTER)
.setOnClick(ActionBuilders.LoadAction.Builder().build())
.build()
)
)
当点击此按钮时,将在您的服务中调用 onTileRequest
(SuspendingTileService
中的 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()
以便它传递一个真实的点击操作。
- 添加一个新参数
searchButtonClickable
(类型为ModifiersBuilders.Clickable
)。 - 将其传递给现有的
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 磁贴指南和设计指南。