使用 Compose 构建 Wear OS 应用

使用 Compose 构建 Wear OS 应用

适用于 Wear OS 的 Compose 已推出了 开发者预览版,使用 Compose 构建 Wear OS 应用,不仅可以轻松遵循 Material You 指南,同时可以将 Compose 的优点发挥出来。开箱即用,帮助开发者使用更少的代码快速构建出更精美的 Wear OS 应用。本文将通过 Wear Compose 主要的可组合项 (Composable) 来帮助您更好地了解如何使用 Compose 来进行构建。

如果您更喜欢通过视频了解此内容,请在此处查看:

△ 使用 Compose 构建 Wear OS 应用

△ 主应用界面和通知界面

△ 主应用界面和通知界面

移动应用往往需要针对多种不同的界面种类进行开发,通常情况下,承载应用的主界面由 Fragment、Activity 和 View 构成,而在 Compose 的世界中则是由可组合项构成,作为开发者您需要了解并适应这种变化。除此之外,您还要针对额外的通知界面进行开发,这意味着您需要在主应用界面之外提醒用户注意某些重要信息,或让他们在启动主应用后继续完成刚刚执行的操作,例如跟踪跑步路线或者播放音乐。如果您使用了 Widget,也可以借助此类界面向用户提供信息。

△ Wear OS 中不同的应用界面

△ Wear OS 中不同的应用界面

Wear OS 拥有 多种界面,在打造完备的 Wear OS 应用体验时,需要您全部考虑:

  • 叠加层 (Overlay) 与移动应用的主界面类似,之前由 Activity、View 和 Fragment 组成,现在由可组合项构成,非常适合流程较长或较为复杂的交互;

  • 通知 (Notification) 界面同样符合移动应用开发准则;

  • 复杂功能 (Complication) 可在表盘中提供信息便于用户直接查看,用户只需在表盘上轻点一下,Complication 即可打开相关联的应用,或执行独立操作,例如饮水记录功能,记录您一天用水杯喝水的次数;

  • 图块 (Tile) 提供了更多展示内容的空间,用户可在表盘上通过任意方向滑动,快速访问信息、执行操作。

本文我们将着重介绍 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 Compose 文档页面 查看依赖项

Wear OS Material 库介绍

Compose Wear OS Material 库 提供了很多与移动平台上相同的可组合项,您可以替换 Material 主题,并且自定义颜色、字体等,不同的是它们都针对手表进行了优化。接下来我们就为您介绍一些常用的可组合项。

Button

Button 属于紧凑的界面元素,用户可通过点按 Button 执行操作或做出选择。

通过如下代码可轻松添加 Button,虽然样式与移动版不同,但代码一样。我们在代码里初始化了一个 Button 可组合项,然后声明了一些参数,它们被称为 Modifier,通过 Modifier 可以更改很多属性,比如这里的 onClick、same、enabled,若您还想为 Button 添加一个图标,那就需要用到包含 painter、contentDescription 和 modifier 的 Icon 可组合项:

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 可组合项代码

通过上述代码,我们可以创建精美小巧的 Button,显示效果如下:

△ Button 代码效果

△ Button 代码效果

Card

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 可组合项代码

通过上述代码,我们创建出了一个精美的 Card,显示效果如下:

△ Card 代码效果

△ Card 代码效果

如需获得不同外观的精美卡片显示效果,仅需对代码进行轻微调整即可。

Chip

Chip 旨在实现快捷的一键操作,对屏幕空间有限的 Wear 设备尤其有用,各种 Chip 变体也能让您尽情挥洒创意。

下面是实现 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 代码效果

△ Chip 代码效果

ToggleChip

ToggleChip 和 Chip 类似,区别是用户使用单选按钮、切换开关、复选框:

如下所示 ToggleChip 用例:

△ ToggleChip 用例

△ ToggleChip 用例

图左展示,只需轻点即可开关声音;图右则对 ToggleChip 进行了拆分,提供了两个不同的可点击区域,通过右侧按钮可将其关闭,点击左侧可以进入应用以便对闹钟进行编辑。其代码大同小异:

ToggleChip(
    modifier = Modifier.height(32.dp)
    checked = checkedState,
    onCheckedChange = { … }
    label = {
        Text(
            text = "Sound",
            maxLines = 1,
            overflow = TextOverflow.Ellipsis
        )
    }
)

△ ToggleChip 代码

△ ToggleChip 代码效果

△ ToggleChip 代码效果

CurvedText 和 TimeText

CurvedText 专门针对了圆形屏幕进行了优化,这对圆形设备来说非常重要,而 TimeText 是基于 CurvedText 所创建的可组合项,它为您处理时间方面的所有文字显示工作。

如下代码示例展示了如何创建 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 代码

通过上述代码,我们可以看到时间文本在圆形屏幕的显示效果如下:

△ TimeText 显示效果

△ TimeText 显示效果

ScalingLazyColumn

列表几乎是每个应用中都会用到的组件,它纵向展示了连续的界面元素。但由于 Wear OS 手表设备的屏幕顶部和底部空间都非常小,因此 Material Design 引入了新的 ScalingLazyColumn 来进行缩放和透明度的展示,这样有助于您在较小的空间内查看列表的内容。

△ ScalingLazyColumn 显示效果

上图展示了 ScalingLazyColumn 的效果,您可以看到随着列表内元素的滑入,当列表的某一行靠近中心位置时,会放大到完整尺寸,而随着该元素的滑出,会变得越来越小 (并且变得更透明) 直至完全消失,这种效果十分有利于内容的展示,内容更易于用户阅读。

ScalingLazyColumn 底层是由 LazyColumn 实现的,它只会对即将要在屏幕上呈现的内容进行处理,这样能够高效地处理大量数据,且能够以缩放和透明效果进行展示,因此它应该成为 Wear OS 的默认组件。

如上效果,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 示例代码

在上述代码中,我们为 SwipeToDismissBox 设置了 state 属性,这一点和移动端不同。通过传递的 state 获取到 isBackground 回调值,它代表了此过程是否是滑动返回,您可以根据不同的状态展示不同的内容。下图是最终的呈现效果:

△ SwipeToDismissBox 代码效果

△ SwipeToDismissBox 代码效果

至此,我们介绍了一些 Wear OS 的可组合项,若您对移动端的可组合项开发有所了解,您可能会发现在 Wear OS 中开发基本是一样的,换句话说,您之前学习 Compose 时掌握的知识可以直接用于 Wear OS 开发。

使用 Scaffold

Scaffold 可让您实现具有基本 Material Design 布局结构的界面,它可为最常见的顶层 Material 组件 (例如 TopBar、BottomBar、FloatingActionButton 和 Drawer) 提供槽位。使用 Scaffold 时,您可以确保这些组件能够正确放置并协同工作。而在 Wear OS 中,它也有着专属的版本,除了同移动版相同的 content 组件之外,额外提供了以下三个主要组件:

△ Wear Scaffold 中的三个主要组件

△ Wear Scaffold 中的三个主要组件

  1. TimeText: 可以将时间置于屏幕的顶部,我们已经介绍过它,具体请参考上文中关于 TimeText 的部分;

  2. Vignette: 可在屏幕周围为您提供漂亮的晕影效果,如上图中所示;

  3. PositionIndicator: 也称为滚动指示器,是屏幕右侧的指示符,用于根据您传入的状态对象类型显示当前指示符的位置。将它放置于 Scaffold 中是由于屏幕是弧形的,因此位置指示器需要位于表盘中央 (Scaffold),而不仅仅是在视口 (viewport) 中央。否则,指示器可能会被截断。

Scaffold 设计

△ Scaffold 设计层级

△ Scaffold 设计层级

在进行 Scaffold 的设计时,请参考上图中的层级顺序进行考虑,首先要做的是对 App 进行设置,其次是设置 MaterialTheme 来自定义一些应用的外观和风格,紧接着是考虑如何放置 Scaffold,最后才是对 Content 的定义。这个顺序同在移动端是一样的,先考虑设置 Theme,再到 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
        …
    }
}

△ Scaffold 示例代码

上述代码中,由于 positionIndicator 位于 content 之外,因此要将 state 提升到 Scaffold 之上的级别,来避免它在屏幕中被截断。而在滚动时,可以通过检查滚动状态,通过隐藏时间显示来为屏幕留出更多的空间,还可以根据状态来关闭或打开 vignette 效果。positionIndicator 支持多种滚动选项,本例中我们使用了 scalingLazyListState,还可以使用很多效果炫酷的其他选项,具体请参考相关文档。而关于 modifier 和 TimeText 想必不用过多介绍了,而 vignette 的设置其实也很简单。

在本文一开始就提到您需要使用 Wear Navigation 依赖项来替换 Navigation,这里再次强调一下,从技术层面来说您仍可使用 Navigation,但是可能会遇到各种问题,所以还是建议您直接使用已针对 Wear 优化的 Wear Navigation。

△ Navigation 设计

△ Navigation 设计

关于 Navigation 的设计,同 Scaffold 大致相同,采用了和移动版相同的设计,只是在 Scaffold 之下和 Content 之上增加了 SwipeDismissableNavHost,顾名思义该组件支持滑出操作,您可以直接使用与移动应用开发相同的知识来编写代码。

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

△ Navigation 示例代码

在上述代码中,MaterialTheme 和 Scaffold 与之前一样,但我们创建了一个 navController,并使用了 SwipeDismissable 版本的 rememberSwipeDismissableNavController,名称非常拗口,但是很容易理解它的功能。然后使用了 SwipeDismissableNavHost 将 startDestination 及其路径传递到控制器中,再设置主屏幕内容即可。您会发现代码基本上同移动端相同,非常便于理解。

总结

在 Wear OS 中,请确保使用合适的依赖项,替换 Material 并添加 Foundation 依赖,如果使用的是 Navigation 同样也要进行替换。另外,所有 Compose 构建方面的知识都可以直接应用于 Wear Compose 中,用移动端的开发经验助您快速构建精美的 Wear 界面。

如需了解更多详细信息,请参阅:

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!

版权声明

禁止一切形式的转载-禁止商用-禁止衍生 申请授权

脉脉不得语
脉脉不得语
Zhengzhou Website
Android Developer | https://androiddevtools.cn and https://androidweekly.io WebMaster | GDG Zhengzhou Funder & Ex Organizer | http://Toast.show(∞) Podcast Host

你已经成功订阅到 Android 开发技术周报
太棒了!接下来,完成检验以获得全部访问权限 Android 开发技术周报
欢迎回来!你已经成功登录了。
Unable to sign you in. Please try again.
成功!您的帐户已完全激活,您现在可以访问所有内容。
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.