官方文档:https://developer.android.com/jetpack/compose/layouts/adaptive?hl=zh-cn
使用 Compose 布置整个应用时,应用级和屏幕级可组合项会占用分配给应用进行渲染的所有空间。在应用设计的这个层面上,可能有必要更改屏幕的整体布局以充分利用屏幕空间。
关键术语:
- 应用级可组合项:单个根可组合项,它会占用分配给应用的所有空间,并包含所有其他可组合项。
- 屏幕级可组合项:应用级可组合项中包含的一种可组合项,会占用分配给应用的所有空间。在应用中导航时,每个屏幕级可组合项通常代表一个特定目的地。
- 个别可组合项:所有其他可组合项。可以是各个元素、可重复使用的内容组,或者是在屏幕级可组合项中托管的可组合项。
原则:避免根据物理硬件值来确定布局。
1. 将原始尺寸转为窗口Size类
将尺寸分组到标准的大小存储分区中,这些是一些断点,目的是要灵活地针对大多数独特情形优化您的应用,又不至于实现起来太过困难。这些 Size 类参考的是应用的整个窗口,因此请使用这些类来确定影响整个屏幕的布局。您可以将这些 Size 类作为状态进行传递,也可以执行其他逻辑来创建派生状态以传递给嵌套可组合项。
@Composable
fun Activity.rememberWindowSizeClass(): WindowSize {
val configuration = LocalConfiguration.current
val windowMetrics = remember(configuration) {
//此方案依赖window库,非compose也可使用
WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this)
}
val windowDpSize = with(LocalDensity.current) {
windowMetrics.bounds.toComposeRect().size.toDpSize()
}
return when {
windowDpSize.width < 600.dp -> WindowSize.COMPACT
windowDpSize.height < 840.dp -> WindowSize.MEDIUM
else -> WindowSize.EXPANDED
}
}
@Composable
fun MyApp(windowSizeClass: WindowSizeClass) {
// Perform logic on the size class to decide whether to show
// the top app bar.
val showTopAppBar = windowSizeClass != WindowSizeClass.Compact
// MyScreen knows nothing about window sizes, and performs logic
// based on a Boolean flag.
MyScreen(
showTopAppBar = showTopAppBar,
/* ... */
)
}
2. 测量约束条件
如果可以利用额外的屏幕空间,您在大屏幕上向用户显示的内容可以比在小屏幕上多。当实现具有此行为的可组合项时,您可能想要提高效率,根据当前屏幕尺寸来加载数据。
使用 BoxWithConstraints
作为更强大的替代方案。这个可组合项提供的测量约束条件可用来根据可用空间调用不同的可组合项。但是,这样也会带来一些后果,因为 BoxWithConstraints
会将组合推迟到布局阶段(此时已知道这些约束条件),从而导致在布局期间执行更多工作。
BoxWithConstraints {
if (maxWidth < 400.dp) {
OnPane()
} else {
TwoPane()
}
}
//或者如下
@Composable
fun AdaptivePane(
showOnePane: Boolean,
/* ... */
) {
if (showOnePane) {
OnePane(/* ... */)
} else {
TwoPane(/* ... */)
}
}
3. 约束布局
ConstraintLayout
是一种布局,让您可以相对于屏幕上的其他可组合项来放置可组合项。它是一种实用的替代方案,可代替使用多个已嵌套的 Row、Column、Box 和其他自定义布局元素这种做法。在实现对齐要求比较复杂的较大布局时,ConstraintLayout
很有用。
详见:https://developer.android.com/jetpack/compose/layouts/constraintlayout?hl=zh-cn
@Composable
fun ConstraintLayoutContent() {
ConstraintLayout {
// Create references for the composables to constrain
val (button, text) = createRefs()
Button(
onClick = { /* Do something */ },
// Assign reference "button" to the Button composable
// and constrain it to the top of the ConstraintLayout
modifier = Modifier.constrainAs(button) {
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text("Button")
}
// Assign reference "text" to the Text composable
// and constrain it to the bottom of the Button composable
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 16.dp)
})
}
}
4. Demo
最终效果如下:https://live.csdn.net/v/embed/341555 (画面被压缩有点失真…)
以下图为例,在大屏幕上显示两部分,左侧是图片列表,右侧是图片详情页。
接下来,介绍其关键部分的实现步骤。
1. 根据屏幕大小来选择布局
//测量约束布局
@Composable
fun Homepage() {
BoxWithConstraints {
if (maxWidth < 400.dp || maxHeight < 400.dp) {
SideBarDrawer() //持有MainNavRoute
ResourceHolder.navigationViewModel.setTwoPane(false) //标志位
} else {
ResourceHolder.navigationViewModel.setTwoPane(true)
Row {
Column(modifier = Modifier.weight(1f)) {
SideBarDrawer()
}
Column(modifier = Modifier.weight(1f)) {
PlaceholderNavRoute()
}
}
}
}
}
2. 列表和详情页联动
此部分实际上是根据路由实现的,left
和right
两部分屏幕级可组合项都持有独立的NavHostController
,我们根据是否是twoPane
,判断当前路由应该有哪个controller
进行跳转。两个NavHostController
分别定义如下,可以看到右侧内容的路由只是左侧内容的一个子集,因为只有部分可组合项会在右侧展示。
//左侧的navHostController定义的路由
@Composable
fun MainNavRoute(curItem: String? = null) {
val navHostController = rememberNavController().also {
ResourceHolder.navHostController = it
}
val startDestination = curItem?.let {
getSideBarMenu().getOrDefault(curItem, NavRoute.PICTURE_LIST_PAGE.route)
} ?: NavRoute.PICTURE_LIST_PAGE.route
NavHost(navHostController, startDestination = startDestination) {
composable(NavRoute.PICTURE_LIST_PAGE.route) {
PictureListPage(ResourceHolder.pictureViewModel)
}
composable(NavRoute.PICTURE_DETAIL_PAGE.route) {
val position = it.arguments?.getString("position")?.toIntOrNull() ?: 0
PictureDetailPage(ResourceHolder.pictureViewModel, position)
}
}
}
//右侧的navHostController定义的路由
@Composable
fun PlaceholderNavRoute() {
val navHostController = rememberNavController().also {
ResourceHolder.navHostController2 = it
}
NavHost(navHostController, startDestination = NavRoute.EMPTY_PAGE.route) {
composable(NavRoute.PICTURE_DETAIL_PAGE.route) {
val position = it.arguments?.getString("position")?.toIntOrNull() ?: 0
PictureDetailPage(ResourceHolder.pictureViewModel, position)
}
composable(NavRoute.EMPTY_PAGE.route) {
EmptyPage()
}
}
}
那么,我们如何知道此时该由哪个navHostController
进行路由跳转呢?可能会需要如下代码,这段代码定义了应该何时使用main
或者placerholder
所持有的Controller
。
NavigationViewModel.kt
/**
* @param isMainRoute: false时才支持在右侧打开二级页面
*/
fun setRoute(isMainRoute: Boolean = false, route: String) {
safeRoute {
if (twoPane.value && isMainRoute.not()) {
ResourceHolder.placeholderNavController?.navigate(route)
} else {
ResourceHolder.mainNavController?.navigate(route)
}
}
}
通过以上步骤,我们就基本搭建了一个能够支持屏幕大小变化的应用~
3. 其他
如果需要感知全屏、分屏、小窗、屏幕旋转,我们可以定义如下函数:
@Composable
fun rememberFullScreen(
predicate: () -> Boolean = { //额外的判断条件
true
}
): Boolean {
val configuration = LocalConfiguration.current
return remember(configuration) {
configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && predicate.invoke()
}
}
在上述的方案中,我们分别定义了两个navHostController
,实际上还可以定义一个全局的Controller
,增加ListWithDetail
的路由即可,如下图所示。更推荐下面这种方案!!!但是成本不会降低很多。
公众号同步连载中,若您觉得还不错,欢迎关注~