使用 Compose 构建 Wear OS 应用
适用于 Wear OS 的 Compose 已推出了开发者预览版,使用 Compose 构建 Wear OS 应用,不仅可以轻松遵循 Material You 指南,同时可以将 Compose 的优点发挥出来。开箱即用,帮助开发者使用更少的代码快速构建出更精美的 Wear OS 应用。本文将通过 Wear Compose 主要的可组合项 (Composable) 来帮助您更好地了解如何使用 Compose 来进行构建。
如果您更喜欢通过视频了解此内容,请在此处查看:
△ 使用 Compose 构建 Wear OS 应用
Bilibili 视频链接
https://www.bilibili.com/video/BV1AL411P73L/
△ 主应用界面和通知界面
移动应用往往需要针对多种不同的界面种类进行开发,通常情况下,承载应用的主界面由 Fragment、Activity 和 View 构成,而在 Compose 的世界中则是由可组合项构成,作为开发者您需要了解并适应这种变化。除此之外,您还要针对额外的通知界面进行开发,这意味着您需要在主应用界面之外提醒用户注意某些重要信息,或让他们在启动主应用后继续完成刚刚执行的操作,例如跟踪跑步路线或者播放音乐。如果您使用了 Widget,也可以借助此类界面向用户提供信息。
△ Wear OS 中不同的应用界面
叠加层 (Overlay) 与移动应用的主界面类似,之前由 Activity、View 和 Fragment 组成,现在由可组合项构成,非常适合流程较长或较为复杂的交互; 通知 (Notification) 界面同样符合移动应用开发准则; 复杂功能 (Complication) 可在表盘中提供信息便于用户直接查看,用户只需在表盘上轻点一下,Complication 即可打开相关联的应用,或执行独立操作,例如饮水记录功能,记录您一天用水杯喝水的次数; 图块 (Tile) 提供了更多展示内容的空间,用户可在表盘上通过任意方向滑动,快速访问信息、执行操作。
多种界面
https://developer.android.google.cn/training/wearables/user-interfaces叠加层 (Overlay)
https://developer.android.google.cn/training/wearables/overlays
本文我们将着重介绍 Overlay 界面,并快速演示几项 Wear 可组合项,了解它们的工作原理及其与移动平台的相似之处。
添加依赖项
在使用 Wear Compose 之前,我们需要先确保已有正确的依赖项,它同移动版 Compose 略有不同。在移动版上,主要使用的依赖项有 Material、Foundation、UI、Runtime 和 Compiler,您还可以选择使用 Navigation 和 Animation 依赖。但在 Wear 中,您可以使用一样的 UI 依赖项,Runtime、Compiler 和 Animation 也都是相同的。此外,其他一些方面也都是相同的,比如工具和一些 Compose 设计理念,比如使用双向数据流。
但还是有一些不同之处的,比如您需要使用 Wear Compose Material 替换 Material,单从技术上来说移动版 Material 也是可以直接用的,但它并没有针对 Wear 的一些特性进行优化,类似的我们还推荐您使用 Wear Navigation 来替换 Navigation。
虽然我们建议直接使用 Wear Compose Material,但您仍然可以使用 Material Ripple 和 Material Icons Extended 等。在添加正确的 Wear 依赖项后,您就可以着手进行开发了。
Wear OS Material 库介绍
Compose Wear OS Material 库
https://developer.android.google.cn/reference/kotlin/androidx/wear/compose/material/package-summary.htmlMaterial 主题
https://developer.android.google.cn/training/wearables/components/theme
Button
Button(
modifier = Modifier.size (ButtonDefaults.LargeButtonSize),
onClick = {... },
enabled = enabledState
) {
Icon(
painter = painterResource (id = R.drawable.ic_phone),
contentDescription = "phone",
modifier = Modifier
.size(24. dp)
.wrapContentSize(align = Alignment.Center),
)
}
△ Button 可组合项代码
Card
Card 可针对单一主题的内容和操作进行呈现,十分灵活。
如图左侧 Card 展示了一些图标和文字,中间界面只保留了文字,右侧使用了一张图片作为背景。
△ Card 用例
在 Wear OS 中,主要有 AppCard 和 TitleCard 两种 Card,TitleCard 更侧重文字展示,本文我们将着重介绍 AppCard。如下示例代码创建了一个 AppCard,并相继通过 Image、Text 和 Column 定制内容:
AppCard(
appImage = {
Image(painter = painterResource(id = R.drawable.ic_message), …)
},
appName = { Text ("Messages") },
time = { Text ("12m" ) },
title = { Text("Kim Green") },
onClick = { … },
body = {
Column(modifier = Modifier.fillMaxWidth()) {
Text("On my way!")
}
},
)
△ Card 代码效果
Chip
Chip(
modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = { … },
enabled = enabledState,
label = {
Text(
text = "1 minute Yoga",
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
icon = {
Icon(
painter = painterResource (id = R.drawable.ic_yoga),
contentDescription= "yoga icon",
modifier = Modifier
.size(24.dp)
.wrapContentSize(align = Alignment.Center),
)
},
)
△ Chip 可组合项代码
△ Chip 代码效果
ToggleChip
△ ToggleChip 用例
ToggleChip(
modifier = Modifier.height(32.dp)
checked = checkedState,
onCheckedChange = { … }
label = {
Text(
text = "Sound",
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
△ ToggleChip 代码
CurvedText 和 TimeText
https://developer.android.google.cn/training/wearables/components/curved-text
如下代码示例展示了如何创建 TimeText,并以 CurvedText 的方式进行展示:
var textBeforeTime by rememberSaveable { mutableStateOf("ETA 99 hours") }
// 首先创建在时间之前显示的前缀字符串
TimeText(
// 创建 TimeText 可组合项
leadingCurvedContent = {
BasicCurvedText(
text = textBeforeTime,
style = TimeTextDefaults.timeCurvedTextStyle()
)
},
// 指定 leadingCurvedContent,在时间文本前显示文字,以 CurvedText 的方式在曲面设备上展示。
leadingLinearContent = {
Text(
text = textBeforeTime,
style = TimeTextDefaults.timeTextStyle()
)
},
// 指定 leadingLinearContent,在时间文本前显示文字,常规显示,适用于非曲面设备。
)
△ TimeText 代码
通过上述代码,我们可以看到时间文本在圆形屏幕的显示效果如下:
ScalingLazyColumn
val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState()
ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(6.dp),
state = scalingLazyListState,
) {
items(messageList.size) { message ->
Card(...) {...}
}
item {
Card(...) {...}
}
}
△ ScalingLazyColumn 示例代码
SwipeToDismissBox
这是大家十分熟悉的 Box 组件被视为界面中的一个容器,可在移动端使用,但 Wear 中有专属版本 SwipeToDismissBox,可用于您的布局,顾名思义它的功能是滑动以关闭。在 Wear OS 中,主要的手势就是滑动,通过使用 SwipeToDismissBox 向右滑动,就相当于点击了 "返回" 按钮。
val state = rememberSwipeToDismissBoxState()
SwipeToDismissBox(
state = state,
){ isBackground ->
if (isBackground) {
Box(modifier = Modifier. fillMaxSize().background(MaterialTheme.colors.secondaryVariant))
} else {
Column(
modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.primary),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
){
Text ("Swipe to dismiss", color = MaterialTheme.colors.onPrimary)
}
}
}
在上述代码中,我们为 SwipeToDismissBox 设置了 state 属性,这一点和移动端不同。通过传递的 state 获取到 isBackground 回调值,它代表了此过程是否是滑动返回,您可以根据不同的状态展示不同的内容。下图是最终的呈现效果:
△ SwipeToDismissBox 代码效果
至此,我们介绍了一些 Wear OS 的可组合项,若您对移动端的可组合项开发有所了解,您可能会发现在 Wear OS 中开发基本是一样的,换句话说,您之前学习 Compose 时掌握的知识可以直接用于 Wear OS 开发。
使用 Scaffold
Scaffold 可让您实现具有基本 Material Design 布局结构的界面,它可为最常见的顶层 Material 组件 (例如 TopBar、BottomBar、FloatingActionButton 和 Drawer) 提供槽位。使用 Scaffold 时,您可以确保这些组件能够正确放置并协同工作。而在 Wear OS 中,它也有着专属的版本,除了同移动版相同的 content 组件之外,额外提供了以下三个主要组件:
TimeText: 可以将时间置于屏幕的顶部,我们已经介绍过它,具体请参考上文中关于 TimeText 的部分; Vignette: 可在屏幕周围为您提供漂亮的晕影效果,如上图中所示; PositionIndicator: 也称为滚动指示器,是屏幕右侧的指示符,用于根据您传入的状态对象类型显示当前指示符的位置。将它放置于 Scaffold 中是由于屏幕是弧形的,因此位置指示器需要位于表盘中央 (Scaffold),而不仅仅是在视口 (viewport) 中央。否则,指示器可能会被截断。
Scaffold 设计
△ Scaffold 设计层级
// positionIndicator 在 Content 之外,因此要将 state 提升到 Scaffold 之上的级别
val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState()
MaterialTheme {
Scaffold(
modifier = Modifier.fillMaxSize(),
timeText = {...},
vignette = {
Vignette (vignettePosition =
VignettePosition.TopAndBottom)
},
positionIndicator = {
// 通过查看 state 来判断是否处于滚动状态,若否,则不会进行展示
if (scalingLazyListState.isScrollInProgress) {
// PositionIndicator 需要用到 state,这也是我们从 LazyColumn 提升状态的主要原因
PositionIndicator(scalingLazyListState =
scalingLazyListState)
}
}
) {
// 设置 content
…
}
}
上述代码中,由于 positionIndicator 位于 content 之外,因此要将 state 提升到 Scaffold 之上的级别,来避免它在屏幕中被截断。而在滚动时,可以通过检查滚动状态,通过隐藏时间显示来为屏幕留出更多的空间,还可以根据状态来关闭或打开 vignette 效果。positionIndicator 支持多种滚动选项,本例中我们使用了 scalingLazyListState,还可以使用很多效果炫酷的其他选项,具体请参考相关文档。而关于 modifier 和 TimeText 想必不用过多介绍了,而 vignette 的设置其实也很简单。
Navigation
在本文一开始就提到您需要使用 Wear Navigation 依赖项来替换 Navigation,这里再次强调一下,从技术层面来说您仍可使用 Navigation,但是可能会遇到各种问题,所以还是建议您直接使用已针对 Wear 优化的 Wear Navigation。
Wear Navigation
https://developer.android.google.cn/training/wearables/design/navigation
MaterialTheme {
Scaffold(...){
val navController = rememberSwipeDismissableNavController()
SwipeDismissableNavHost(
navController = navController,
startDestination = Screen.MainScreen.route
) {
composable(route = Screen.MainScreen.route){
MyListScreen(...)
}
composable(route = Screen.DetailsScreen. route + "/{$ID}", ...) {
MyDetailScreen(...)
}
}
}
}
总结
如需了解更多详细信息,请参阅:
Wear OS 概览
https://developer.android.google.cn/training/wearables
文档指南: 在 Wear OS 上使用 Jetpack Compose
https://developer.android.google.cn/training/wearables/composeCodelab: 开始编写适用于 Wear OS 的应用
https://developer.android.google.cn/codelabs/compose-for-wear-osGitHub 示例
https://github.com/android/wear-os-samples#readme
推荐阅读