软件工程师
在本场讲座中,我们将从技术角度深入探究从全屏功能块到动态、部分高度 Wear 微件的演变。
我们将探讨统一的 Jetpack Glance API 和全新 Remote Compose 引擎的技术优势,并详细介绍它们如何与 Modern Android Development 无缝衔接。
最后,我们将分享最新的用户体验指南,并介绍实用的迁移策略,帮助您打造新一代一目了然的界面。
转写文稿
大家好!
我叫 Frank,负责 Wear OS 的一目了然体验。
手表是用户全天候的重要伙伴。今天,我们将带您了解如何利用全新的 Wear 微件体验。
我们将探讨如何将您最重要的功能直接提供给用户,并使用最新的 API 来实现这一目标。这些 API 旨在让像您这样的 Android 开发者能够以熟悉且直观的方式为 Wear OS 构建应用。
今天,我邀请了我的优秀同事兼邻桌好友 Yuri 与我一同登台,深入探讨我们是如何做到这一点的。
大家好,我是 Yuri Schimke。
我是 Wear OS 团队的一名工程师,主要负责 Remote Compose API。
今天,我将简要介绍 Remote Compose,并讨论从卡片迁移到 Wear 微件的过程。
接下来,我将把时间交还给 Frank。
为了让从卡片到微件的过渡尽可能顺畅,我们推出了两种标准容器尺寸:小号和大号。
这些格式旨在与您在移动设备上构建微件时已经习惯的 2x1 和 2x2 网格微件格式保持一致。
从 Wear 7 开始,在某些硬件制造商的设备上,您的 Wear 微件现在将与其他应用的微件一起显示在可垂直滚动的托盘中。
例如,以下是 Wear 微件在 Samsung Galaxy Watch 上的显示效果。
该平台架构旨在支持垂直滚动托盘,以便硬件制造商在未来采用这种模式。
您也无需担心会遗漏任何用户或现有用户。
Jetpack Glance Wear API 旨在支持向后兼容至 Wear 4。
在支持水平托盘(用于放置微件或卡片)的旧款设备上,系统会为您处理。
您只需在代码中声明要支持全屏模式,系统就会在这些设备上将大型微件显示为全屏传统卡片。
现在,Yuri 将带我们浏览代码,向大家展示如何将现有卡片迁移到 Wear 微件。
谢谢,Frank。
如果您要从头开始构建新的 Wear 微件,操作很简单。
您只需定义一个 GlanceWearWidgetService,并使用 Remote Compose 构建界面。
但如果您的应用已有正式版卡片,则需要迁移服务、界面和数据。
为了让这一过渡尽可能顺畅,我们提供了多种途径,确保现有用户能够获得无缝体验。
对于服务,您可以选择两种策略:双服务或单服务。
我们建议您采用双重服务,即保留现有的 TileService 以支持旧版 Wear OS 设备,同时为 Wear OS 7 及更高版本添加新的 GlanceWearWidgetService。
相比之下,如果使用单一服务,您只需创建一个新的单一 GlanceWearWidgetService,即可同时处理这两种协议,并在兼容模式下运行,以便在旧款设备上将微件转换为卡片。
无论采用哪种方式,您都可以继续覆盖用户。
它只会影响您今后维护的代码。
我们刚刚在 Android 开发者网站上发布了全面的文档,详细介绍了这两种模式的使用方法和时机。
欢迎查看,详细了解哪种策略最适合您的应用。
现在,我们来谈谈界面层。
迁移时,您的思维模式会发生转变。
以前,功能块使用 ProtoLayout,这是一个专为 Wear 功能块构建的库。
而 Wear 微件则基于 Remote Compose 构建。
Remote Compose 与 Jetpack Compose 界面共享完全相同的基础,使用相同的核心编译器和运行时。
其设计旨在让您感到熟悉,与 Compose 界面库(如 Foundation 和 Material 3)高度契合。
但您只需将 Box、Column 或 Text 替换为 RemoteBox、RemoteColumn 或 RemoteText。
在这种情况下,您会经常编写“remote”。
此外,为了加快您的工作流程,我们还提供了一套专为 widget 设计的即用型 Remote Compose 组件。
这意味着,您现有的 Wear Material 3 设计(包括您在 Figma 中的设计)可以轻松适配微件,让工程师和设计师都能轻松上手。
为了了解这些框架的契合程度,我们来看一个具体示例。
我们将在 Material 3 开关按钮的基础上构建自己的应用组件。
左侧是您在应用中编写的标准 Wear Compose 组件,右侧是 Remote Compose 版本。
// Wear Compose
@Composable
fun HistoryEraserButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
checked: Boolean = true,
onCheckedChange: (Boolean) -> Unit = {},
)// Wear RemoteCompose
@RemoteComposable
@Composable
fun RemoteHistoryEraserButton(
onClick: Action,
modifier: RemoteModifier = RemoteModifier,
checked: RemoteBoolean = true.rb,
onCheckedChange: Action = Action.Empty,
)请注意这些模式有多么相似。
不过,在编写 Remote Compose 代码时,您将使用 RemoteComposable 注解。
如果您使用修饰符和布尔值等熟悉的形参,它们具有等效的 Remote Compose 类型,例如 RemoteModifier 和 RemoteBoolean。
并且您使用的是操作,而不是 lambda。
您无需定义要在 lambda 中执行的代码,而是需要声明一个操作,例如启动待处理 intent、调用服务或更新值。
我们来看看组件的主体。
您会发现,甚至连主题都是一致的。
您可以像在标准 Compose 中一样,将 Material 3 配色方案应用于 Remote Compose 组件,从而使微件与应用的品牌风格完全一致。
// Wear Compose
val colors = MaterialTheme.colorScheme
SplitSwitchButton(
checked = checked,
onCheckedChange = onCheckedChange,
toggleContentDescription = "Arm Eraser",
onContainerClick = onClick,
modifier = modifier.fillMaxWidth(),
colors =
SwitchButtonDefaults.splitSwitchButtonColors(
checkedContainerColor = colors.errorContainer, …
),
label = { Text("History Eraser") },
secondaryLabel = { Text("Don't touch it!") },
)// Wear RemoteCompose
val colors = RemoteMaterialTheme.colorScheme
RemoteSplitSwitchButton(
checked = checked,
onCheckedChange = onCheckedChange,
toggleContentDescription = "Arm Eraser".rs,
onContainerClick = onClick,
modifier = modifier.fillMaxWidth(),
colors =
RemoteSplitSwitchButtonDefaults.splitSwitchButtonColors(
checkedContainerColor = colors.errorContainer, …
),
label = { RemoteText("History Eraser".rs) },
secondaryLabel = { RemoteText("Don't touch it!".rs) },
)最后,我们来看看结果。最终的界面正是您所期望的:一个简洁、响应迅速、带有动画效果的切换按钮。
很难分辨出区别。
为了帮助您设计、迭代和验证微件布局,而无需始终在设备或模拟器上运行,我们为 Wear 微件添加了 Compose 预览支持。
由于微件依赖于 OEM 特定的容器限制条件(例如大小、形状、内边距、圆角半径),因此我们基于标准预览注解进行构建,但会使用额外的参数提供程序来注入这些配置。
@Preview(name = "Squircle Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetSquirclePreview(
@PreviewParameter(SquircleAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)
@Preview(name = "Round Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetRoundPreview(
@PreviewParameter(RoundAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)该库提供了与实际 Wear OS 设备相匹配的预配置 PreviewParameter 提供程序实现。
借助这些预览注解,您可以轻松验证布局在各种不同硬件形状中的效果。
除了测试布局之外,您还可以使用这些预览生成设备端 widget 选择器所需的静态图片。
@Preview(name = "Widget Picker Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetRectangularPreview(
@PreviewParameter(RectangularAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)该库提供了一个矩形预览形状参数,用于模拟完整的未剪裁矩形容器。
如果您想了解此 API 在实际应用中的具体运作方式,可以查看我们更新后的 Horologist 媒体示例应用,其中包含一个展示此 API 的新微件。
我们将演示如何添加小型和大型媒体微件。
您可以在 GitHub 上查看该示例,对比旧版 ProtoLayout 代码和新的 Remote Compose 实现。
相信您一定会喜欢。
最后,如果不提及 AI,这次演讲就不完整。
为了加快迁移速度,我们在 GitHub 上发布了一项新的“卡片到微件迁移”技能。
您可以将其与您选择的智能体搭配使用,也可以使用 Android CLI 将其直接添加到 Android Studio。
android skills add wear-widgets
android skills add --all现在,我将把时间交给 François,由他来做总结。
// Wear Compose
@Composable
fun HistoryEraserButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
checked: Boolean = true,
onCheckedChange: (Boolean) -> Unit = {},
)// Wear RemoteCompose
@RemoteComposable
@Composable
fun RemoteHistoryEraserButton(
onClick: Action,
modifier: RemoteModifier = RemoteModifier,
checked: RemoteBoolean = true.rb,
onCheckedChange: Action = Action.Empty,
)// Wear Compose
val colors = MaterialTheme.colorScheme
SplitSwitchButton(
checked = checked,
onCheckedChange = onCheckedChange,
toggleContentDescription = "Arm Eraser",
onContainerClick = onClick,
modifier = modifier.fillMaxWidth(),
colors =
SwitchButtonDefaults.splitSwitchButtonColors(
checkedContainerColor = colors.errorContainer, …
),
label = { Text("History Eraser") },
secondaryLabel = { Text("Don't touch it!") },
)// Wear RemoteCompose
val colors = RemoteMaterialTheme.colorScheme
RemoteSplitSwitchButton(
checked = checked,
onCheckedChange = onCheckedChange,
toggleContentDescription = "Arm Eraser".rs,
onContainerClick = onClick,
modifier = modifier.fillMaxWidth(),
colors =
RemoteSplitSwitchButtonDefaults.splitSwitchButtonColors(
checkedContainerColor = colors.errorContainer, …
),
label = { RemoteText("History Eraser".rs) },
secondaryLabel = { RemoteText("Don't touch it!".rs) },
)@Preview(name = "Squircle Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetSquirclePreview(
@PreviewParameter(SquircleAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)
@Preview(name = "Round Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetRoundPreview(
@PreviewParameter(RoundAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)@Preview(name = "Widget Picker Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetRectangularPreview(
@PreviewParameter(RectangularAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)android skills add wear-widgets
android skills add --all