我们主要研究一下使用BoxWithConstraints
如何实现响应式布局。
Jetpack compose 已经在逐步的支持多个平台开发了,手机、电脑、手表和网页。所以用Jetpack compose开发的app可以适应不同平台的屏幕尺寸,以及屏幕的方向也就势在必行。
那么问题就来了,如何能够让一个composable自动全屏。如何能让一个屏幕里的几个composable分别占用屏幕可用的空间呢?BoxWithConstraints
就是可以用起来了。据我的理解,它会根据可用空间自动响应。使用它来创建适用于不同屏幕和屏幕方向的composable是非常简单的。我们来几种不同的情况。
在横向列表中的使用
如同这个名字:BoxWithConstraints
所表达的一样,它本质上就是一个Box
。所以它的子composable会以一个一个的上下叠加的方式显示。在这个例子里,第一个子composable是一个Column
。我们主要关注在column底部的Row
。先看代码:
@Composable
private fun Thumbnails(
thumbnails: List<String>,
modifier: Modifier = Modifier,
) {
BoxWithConstraints(modifier) {
val boxWithConstraintsScope = this
val padding = Theme.dimens.grid_2
val thumbnailSize = Theme.dimens.grid_6
val numberOfThumbnailsToShow = max(
0,
boxWithConstraintsScope.maxWidth.div(padding + thumbnailSize).toInt().minus(1)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(padding),
verticalAlignment = Alignment.CenterVertically,
) {
thumbnails
.take(numberOfThumbnailsToShow)
.forEach {
Image(
painter = rememberImagePainter(data = it),
contentDescription = null,
modifier = Modifier
.width(thumbnailSize)
.aspectRatio(1f),
)
}
val remaining = thumbnails.size - numberOfThumbnailsToShow
if (tagNumber > 0) {
Badge(badge = Badge.Info("+$remaining"))
}
}
}
}
这段代码干了啥呢
BoxWithContraints
的子composable都在BoxWithConstraintsScope
, 使用它可以获得布局的maxWidth
。- 从11到14行,
numberOfThumbnailsToShow
是根据最大值和每个条目的宽度算出来的。我们还有一个+{数字}
要显示呢。所以,如果条目个数正好够显示的时候,需要减少一个来显示那个+{数字}
。 - 从21到31行,遍历
numberOfThumbnailsToShow
,显示每个条目。这里用到了Image
composable。
BoxWithConstraintsScope
的maxWidth
、minWidth
、maxHeight
和minHeight
的dp值。另外还有一个constraints
属性包含了以上四个的像素值。有兴趣可以看看BoxWithConstraints
的源码。
在横屏改变屏幕的方向时,BoxWithConstraintScope
的maxWidth
属性会更新,所有的缩略图条目都会显示出来。
有人可能会说,使用LocalConfiguration
也可以达到相同的效果。但是,这样一来就只能用来处理填充全屏的情况。并不灵活!在开发一个composable的时候就需要处理它所可能遇到的大小约束,否则就是没有使用响应式的思维开发composable了。
假如,我们要在横屏或者更宽的屏幕的时候,分两列显示上面的例子呢?如图:
这样就不需要额外的更改。
在LazyRow里显示固定数量的条目
在前面的例子里,条目的大小是已知的,我们准备尽可能多的显示这些条目。在本例中,我们要显示提前预定义的数量的条目,而且每个条目的大小是根据条目当前数量,和当前可用的大小自适应的。如图:
我们来看代码:
@Composable
private fun PhotosRow(
images: List<BeautifulCreature>,
numPhotosVisible: Float,
modifier: Modifier = Modifier,
) {
BoxWithConstraints(modifier = modifier) {
// Arbitrarily chosen 20 as number of "units" to divide the available width
val numGrids = 20
// Using available space to calculate the space between items and itemWidth
val spaceBetweenItems = maxWidth.div(numGrids)
val itemWidth = (maxWidth - spaceBetweenItems).div(numPhotosVisible)
LazyRow {
items(images) {
PhotoCard(
onClick = { },
photo = it.photo,
contentDescription = it.name,
modifier = Modifier
.width(itemWidth)
.aspectRatio(1f)
)
if (it != images.last()) {
Spacer(modifier = Modifier.width(spaceBetweenItems))
}
}
}
}
}
@Composable
fun PhotoCard(
onClick: () -> Unit,
photo: String,
contentDescription: String,
modifier: Modifier = Modifier,
) {
Card(
onClick = onClick,
modifier = modifier
) {
Image(
painter = rememberImagePainter(data = photo),
contentDescription = contentDescription,
contentScale = ContentScale.Crop
)
}
}
9 - 12行是根据numPhotosVisible
计算每个条目的宽度itemWidth
和条目的间隔宽度spaceBetweenItems
的。
现在,假设我们要显示第三个条目的一部分,这样可以让用户知道还有更多条目可以显示。只需要修改numPhotosVisible
的值就可以。
你应该知道LazyVerticalGrid
可以根据可用空间支持动态个数的列的对吧。这听起来是不是很熟悉?
我们查看一下源码,LazyVerticalGrid
的这部分功能是怎么实现的:
@ExperimentalFoundationApi
@Composable
fun LazyVerticalGrid(
cells: GridCells,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
content: LazyGridScope.() -> Unit
) {
val scope = LazyGridScopeImpl()
scope.apply(content)
when (cells) {
is GridCells.Fixed ->
FixedLazyGrid(
nColumns = cells.count,
modifier = modifier,
state = state,
contentPadding = contentPadding,
scope = scope
)
is GridCells.Adaptive ->
BoxWithConstraints(
modifier = modifier
) {
val nColumns = maxOf((maxWidth / cells.minSize).toInt(), 1)
FixedLazyGrid(
nColumns = nColumns,
state = state,
contentPadding = contentPadding,
scope = scope
)
}
}
}
@ExperimentalFoundationApi
sealed class GridCells {
@ExperimentalFoundationApi
class Fixed(val count: Int) : GridCells()
/**
* Combines cells with adaptive number of rows or columns. It will try to position as many rows
* or columns as possible on the condition that every cell has at least [minSize] space and
* all extra space distributed evenly.
*
* For example, for the vertical [LazyVerticalGrid] Adaptive(20.dp) would mean that there will be as
* many columns as possible and every column will be at least 20.dp and all the columns will
* have equal width. If the screen is 88.dp wide then there will be 4 columns 22.dp each.
*/
@ExperimentalFoundationApi
class Adaptive(val minSize: Dp) : GridCells()
}
22 - 26行可以看到底层用的其实就是BoxWithConstraints
。然后通过minSize
来计算要显示多少列的。
还可以根据容器大小设定文字大小,代码在这里。
原文地址在这里。