Android 开发者 2024年11月18日
使用 Jetpack Compose 为 JetLagged 构建响应式仪表盘布局
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文介绍了如何在 Jetpack Compose 中构建灵活的仪表盘布局,使其能够自动适应不同屏幕尺寸,并提供流畅的用户体验。文章重点讲解了如何结合流式布局、WindowSizeClasses、可移动内容与 LookaheadScope,确保界面在屏幕切换时保持状态并实现自然过渡。通过使用 FlowRow 和 FlowColumn 实现响应式布局,利用 WindowSizeClasses 区分不同设备,并借助 movableContentOf 保持界面状态,最终通过 Modifier.animateBounds() 实现流畅的动画效果,从而构建出适应各种屏幕尺寸的仪表盘界面。

🤔**使用 FlowRow 和 FlowColumn 构建响应式布局:** 通过流式布局,可以轻松创建能够响应不同屏幕尺寸的布局,并在可用空间已满时自动对内容进行换行处理。例如,在 JetLagged 示例中,使用 FlowRow 并设置 maxItemsInEachRow 为 3,以便最大程度地利用可用空间,将卡片放置在一行或一列中。

📱**利用 WindowSizeClasses 区分不同设备:** WindowSizeClasses 可以帮助开发者在界面中建立断点,确定元素何时应该以不同的方式显示。例如,在屏幕尺寸较小时,将卡片保留在 FlowRow 中,而在屏幕尺寸较大时,则将卡片放置在 FlowColumn 中,实现布局的灵活切换。

🔄**借助 movableContentOf 保持界面状态:** 可移动内容允许保存可组合项的内容,并在布局层次结构中移动它,而不丢失状态。这使得在屏幕尺寸变化时,某些界面元素能够保持其内部状态,例如动画效果不会因为布局变化而重置。

⏱️**使用 Modifier.animateBounds() 实现流畅的动画:** 通过结合 LookaheadScope 和 Modifier.animateBounds(),可以实现布局切换时的流畅动画效果,使得界面过渡更加自然,提升用户体验。例如,在卡片切换布局时,使用 spring 动画规范,实现平滑的过渡效果。

原创 Android 2024-11-18 17:30 北京

与您分享如何使用 Compose 构建灵活的仪表盘布局,实现自动适应不同屏幕,带来流畅的用户体验。通过结合流式布局、WindowSizeClasses、可移动内容与 LookaheadScope,确保界面在切换时保持状态并实现自然过渡。

作者 / 开发者关系工程师 Rebecca Franks 


这篇文章是我们 "Adaptive Spotlight Week" 系列的内容之一。在该系列中,我们会提供文章、视频、示例代码等资源,以帮助您将应用适配到手机、可折叠设备、平板电脑、ChromeOS 甚至是车载系统中。您可以查阅更多关于 Adaptive Spotlight Week 的内容


? 查阅更多关于 Adaptive Spotlight Week 的内容

https://android-developers.googleblog.com/2024/10/adaptive-spotlight-week.html


我们了解到,在 Jetpack Compose 中创建自适应布局比以往任何时候都更加简便。作为一款声明式界面工具包,Jetpack Compose 非常适合设计和实现能够根据不同屏幕尺寸调整显示内容的布局。通过结合使用窗口大小类别 (Window Size Classes)、流式布局 (Flow layouts)、movableContentOfLookaheadScope,我们可以确保在 Jetpack Compose 中实现流畅的响应式布局。


? 创建自适应布局

https://developer.android.google.cn/jetpack/compose/layouts/adaptive

? 窗口大小类别

https://developer.android.google.cn/guide/topics/large-screens/support-different-screen-sizes#window_size_classes

? 流式布局

https://developer.android.google.cn/jetpack/compose/layouts/flow

? movableContentOf

https:/developer.android.google.cn/reference/kotlin/androidx/compose/runtime/package-summary#movableContentOf%28kotlin.Function4%29

? LookaheadScope

https://developer.android.google.cn/reference/kotlin/androidx/compose/ui/layout/LookaheadScope


在 2023 年 Google I/O 大会上发布了 JetLagged 示例之后,我们决定添加更多示例。具体来说,我们希望展示如何使用 Compose 创建一个美观的仪表盘式布局。本文将介绍我们如何实现这一目标。

△ Jetlagged 中的响应式设计,各个项目的位置会自动调整


? JetLagged

https://github.com/android/compose-samples/tree/main/JetLagged



借助 FlowRow 和 FlowColumn 

构建能够响应不同屏幕尺寸的布局


使用流式布局 (FlowRowFlowColumn) 可以更轻松地实现响应式、可重排布局,这些布局可以响应屏幕尺寸,并在行或列中的可用空间已满时,自动对内容进行换行处理。


? 流式布局

https://developer.android.google.cn/jetpack/compose/layouts/flow


在 JetLagged 的示例中,我们使用了 FlowRow,并将 maxItemsInEachRow 设置为 3。这可以确保我们最大程度地利用仪表盘的可用空间,并将每个独立的卡片放置在一行或一列中,合理利用空间。在移动设备上,我们通常每行放置 1 张卡片,只有当项目较小时,才会出现每行两张卡片的情况。


一些卡片使用了没有指定确切大小的修饰符 (Modifiers),因此这些卡片可以根据可用宽度进行扩展,例如使用 Modifier.widthIn(max = 400.dp),或者设定一个特定的大小,如 Modifier.width(200.dp)

FlowRow(    modifier = Modifier.fillMaxSize(),    horizontalArrangement = Arrangement.Center,    verticalArrangement = Arrangement.Center,    maxItemsInEachRow = 3) {    Box(modifier = Modifier.widthIn(max = 400.dp))    Box(modifier = Modifier.width(200.dp))    Box(modifier = Modifier.size(200.dp))    // etc }


我们还可以利用权重修饰符来分配行或列的剩余区域。您可以查阅项目权重的文档了解更多信息。


? 项目权重

https://developer.android.google.cn/jetpack/compose/layouts/flow#item-weights



使用 WindowSizeClasses

区分不同设备



WindowSizeClasses 对于在界面中建立断点非常有用,它可以确定元素何时应该以不同的方式显示。在 JetLagged 中,我们使用该类来确定应该将卡片包含在 Column 中,还是让它们连续流动排列。


? WindowSizeClasses 

https://developer.android.google.cn/guide/topics/large-screens/support-different-screen-sizes#window_size_classes


例如,如果 WindowWidthSizeClassCOMPACT,我们将项目保留在相同的 FlowRow 中;而如果布局大于紧凑型,则将项目放置在一个嵌套于 FlowRow 内的 FlowColumn 中:

  FlowRow(                modifier = Modifier.fillMaxSize(),                horizontalArrangement = Arrangement.Center,                verticalArrangement = Arrangement.Center,                maxItemsInEachRow = 3            ) {                JetLaggedSleepGraphCard(uiState.value.sleepGraphData)                if (windowSizeClass == WindowWidthSizeClass.COMPACT) {                    AverageTimeInBedCard()                    AverageTimeAsleepCard()                } else {                    FlowColumn {                        AverageTimeInBedCard()                        AverageTimeAsleepCard()                    }                }                if (windowSizeClass == WindowWidthSizeClass.COMPACT) {                    WellnessCard(uiState.value.wellnessData)                    HeartRateCard(uiState.value.heartRateData)                } else {                    FlowColumn {                        WellnessCard(uiState.value.wellnessData)                        HeartRateCard(uiState.value.heartRateData)                    }                }            }


根据上述逻辑,界面将在不同尺寸的设备上以如下方式呈现:

△ 不同尺寸设备上的不同界面



使用 movableContentOf
以在屏幕尺寸变化时
保持部分界面状态



借助可移动内容 (Movable content),您可以保存可组合项 (Composable) 的内容,以便在布局层次结构中移动它,而不丢失状态。它应该用于那些被视为相同内容,只是在屏幕位置不同的情况。


想象一下,您要搬家到另一个城市,打包了一个装有时钟的箱子。在新家打开箱子时,您会发现时钟仍然从您离开时的时间点继续走动。虽然该时间可能不是您新时区的正确时间,但它肯定是从您离开时的那个时间点继续走动的。箱子里的物体在其移动时并不会重置其内部状态。


如果我们能够在 Compose 中使用同样的概念来移动屏幕上的项目,而不丢失其内部状态,会发生什么呢?


请考虑以下场景:定义不同的 Tile 可组合项,这些项目会在 5,000 毫秒内显示 0 到 100 无限循环的动画。

@Composablefun Tile1() {    val repeatingAnimation = rememberInfiniteTransition()
val float = repeatingAnimation.animateFloat( initialValue = 0f, targetValue = 100f, animationSpec = infiniteRepeatable(repeatMode = RepeatMode.Reverse, animation = tween(5000)) ) Box(modifier = Modifier .size(100.dp) .background(purple, RoundedCornerShape(8.dp))){ Text("Tile 1 ${float.value.roundToInt()}", modifier = Modifier.align(Alignment.Center)) }}

然后我们使用 Column 布局在屏幕上展示这些项目。以下便是这些项目持续进行时的无限动画效果:

但如果我们想根据手机的不同屏幕方向 (或不同屏幕尺寸) 来重新排列 Tile,并且不希望动画值停止运行,该怎么办呢?我们可能会想到以下方法:

@Composablefun WithoutMovableContentDemo() {    val mode = remember {        mutableStateOf(Mode.Portrait)    }    if (mode.value == Mode.Landscape) {        Row {           Tile1()           Tile2()        }    } else {        Column {           Tile1()           Tile2()        }    }}

虽然这样的做法看起来相当标准,但在设备上运行时,我们会发现在这两种布局之间切换会导致动画重新启动。

此时是使用可移动内容的最佳时机,因为屏幕上的可组合项本质上是相同的,只是位置不同。那么我们该如何使用呢?我们只需要在 movableContentOf 块中定义 Tile,并使用 remember 来确保其状态在不同的组合中得以保存:

val tiles = remember {        movableContentOf {            Tile1()            Tile2()        } }


? movableContentOf 

https://developer.android.google.cn/reference/kotlin/androidx/compose/runtime/package-summary#movableContentOf%28kotlin.Function0%29

? remember

https://developer.android.google.cn/reference/kotlin/androidx/compose/runtime/package-summary#remember%28kotlin.Any,kotlin.Any,kotlin.Any,kotlin.Function0%29


现在,我们不是分别在 ColumnRow 中调用可组合项,而是改为调用 tiles()

@Composablefun MovableContentDemo() {    val mode = remember {        mutableStateOf(Mode.Portrait)    }    val tiles = remember {        movableContentOf {            Tile1()            Tile2()        }    }    Box(modifier = Modifier.fillMaxSize()) {        if (mode.value == Mode.Landscape) {            Row {                tiles()            }        } else {            Column {                tiles()            }        }
Button(onClick = { if (mode.value == Mode.Portrait) { mode.value = Mode.Landscape } else { mode.value = Mode.Portrait } }, modifier = Modifier.align(Alignment.BottomCenter)) { Text("Change layout") } }}

这样系统就会记住由这些可组合项生成的节点,并保留这些可组合项当前的内部状态。

我们现在可以看到,动画状态在不同的组合中保持一致。"箱子中的时钟" 现在在世界各地移动时,也会保持其状态。


利用这个概念,我们可以通过将卡片放置在 movableContentOf 中,以保持卡片上的动画气泡状态:

Language
val timeSleepSummaryCards = remember { movableContentOf { AverageTimeInBedCard() AverageTimeAsleepCard() } } LookaheadScope { FlowRow( modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center, maxItemsInEachRow = 3 ) { //.. if (windowSizeClass == WindowWidthSizeClass.Compact) { timeSleepSummaryCards() } else { FlowColumn { timeSleepSummaryCards() } } // } }

这使得卡片的状态得以保存,并且卡片不会被重新组合。这一点在观察卡片背景中的气泡时尤为明显:即使在屏幕尺寸变化时,气泡动画也会继续,而不会重新启动



使用 Modifier.animateBounds()

在不同窗口大小之间

实现流畅的动画效果


从上面的例子中,我们可以看到,虽然在布局大小 (或布局本身) 发生变化时状态得以保持,但切换布局时的变化有些不连贯。我们希望在两种状态切换时实现流畅的动画过渡。


compose-bom-alpha (2024.09.03) 中,我们新增了一个实验性的自定义修饰符 Modifier.animateBounds()animateBounds 修饰符需要配合 LookaheadScope 使用。


? compose-bom-alpha

https://developer.android.google.cn/develop/ui/compose/bom#what_if_i_want_to_try_out_alpha_or_beta_releases_of_compose_libraries

? Modifier.animateBounds() 

https://developer.android.google.cn/reference/kotlin/androidx/compose/animation/package-summary#%28androidx.compose.ui.Modifier%29.animateBounds%28androidx.compose.ui.layout.LookaheadScope,androidx.compose.ui.Modifier,androidx.compose.animation.BoundsTransform,kotlin.Boolean%29

? LookaheadScope

https://developer.android.google.cn/reference/kotlin/androidx/compose/ui/layout/LookaheadScope?hl=en


LookaheadScope 能够让 Compose 在布局变化时执行中间测量过程,并告知可组合项这些变化之间的中间状态。近期,您可能也注意到了 LookaheadScope 还可用于新的共享元素 API


? 共享元素 API

https://developer.android.google.cn/develop/ui/compose/animation/shared-elements


要使用 Modifier.animateBounds(),我们需要在顶层的 FlowRow 外包裹一个 LookaheadScope,然后将 animateBounds 修饰符应用于每个卡片。我们还可以通过指定 boundsTransform 参数到自定义的 spring 规范,从而定制动画的运行方式:

val boundsTransform = { _ : Rect, _: Rect ->   spring(       dampingRatio = Spring.DampingRatioNoBouncy,       stiffness = Spring.StiffnessMedium,       visibilityThreshold = Rect.VisibilityThreshold   )}

LookaheadScope { val animateBoundsModifier = Modifier.animateBounds( lookaheadScope = this@LookaheadScope, boundsTransform = boundsTransform) val timeSleepSummaryCards = remember { movableContentOf { AverageTimeInBedCard(animateBoundsModifier) AverageTimeAsleepCard(animateBoundsModifier) } } FlowRow( modifier = Modifier .fillMaxSize() .windowInsetsPadding(insets), horizontalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center, maxItemsInEachRow = 3 ) { JetLaggedSleepGraphCard(uiState.value.sleepGraphData, animateBoundsModifier.widthIn(max = 600.dp)) if (windowSizeClass == WindowWidthSizeClass.Compact) { timeSleepSummaryCards() } else { FlowColumn { timeSleepSummaryCards() } }

FlowColumn { WellnessCard( wellnessData = uiState.value.wellnessData, modifier = animateBoundsModifier .widthIn(max = 400.dp) .heightIn(min = 200.dp) ) HeartRateCard( modifier = animateBoundsModifier .widthIn(max = 400.dp, min = 200.dp), uiState.value.heartRateData ) } }}

将此逻辑应用到我们的布局中后,我们可以看到两个状态之间的转换更加流畅,不会出现不连贯的情况。

将此逻辑应用到整个仪表盘中,当调整布局大小时,您会感受到整个屏幕上的界面互动变得更加流畅自然。



总结



正如本文所述,通过使用 Compose,我们能够利用流式布局、WindowSizeClasses、可移动内容和 LookaheadScope 来构建一个响应式的仪表盘布局。这些概念同样可以应用于您自己的布局中,可能会有项目在布局中移动。


有关这些不同主题的更多信息,您可以查阅官方文档。有关 JetLagged 的详细更改,请查阅此拉取请求。也欢迎您持续关注 "Android 开发者" 微信公众号,及时了解更多开发技术和产品更新等资讯动态!


? 官方文档

https://developer.android.google.cn/jetpack/compose

? 此拉取请求

https://github.com/android/compose-samples/pull/1473



点击图片关注精彩活动





 点击屏末 阅读原文 | 即刻体验使用 Jetpack Compose 构建应用




阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Jetpack Compose 自适应布局 仪表盘 FlowRow WindowSizeClasses
相关文章