玩转Jetpack Compose 响应式布局

944 阅读4分钟

我们主要研究一下使用BoxWithConstraints如何实现响应式布局。

image.png

Jetpack compose 已经在逐步的支持多个平台开发了,手机、电脑、手表和网页。所以用Jetpack compose开发的app可以适应不同平台的屏幕尺寸,以及屏幕的方向也就势在必行。

那么问题就来了,如何能够让一个composable自动全屏。如何能让一个屏幕里的几个composable分别占用屏幕可用的空间呢?BoxWithConstraints就是可以用起来了。据我的理解,它会根据可用空间自动响应。使用它来创建适用于不同屏幕和屏幕方向的composable是非常简单的。我们来几种不同的情况。

在横向列表中的使用

image.png

如同这个名字: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。

BoxWithConstraintsScopemaxWidthminWidthmaxHeightminHeightdp值。另外还有一个constraints属性包含了以上四个的像素值。有兴趣可以看看BoxWithConstraints的源码。

image.png

在横屏改变屏幕的方向时,BoxWithConstraintScopemaxWidth属性会更新,所有的缩略图条目都会显示出来。

有人可能会说,使用LocalConfiguration也可以达到相同的效果。但是,这样一来就只能用来处理填充全屏的情况。并不灵活!在开发一个composable的时候就需要处理它所可能遇到的大小约束,否则就是没有使用响应式的思维开发composable了。

假如,我们要在横屏或者更宽的屏幕的时候,分两列显示上面的例子呢?如图:

image.png

这样就不需要额外的更改。

在LazyRow里显示固定数量的条目

在前面的例子里,条目的大小是已知的,我们准备尽可能多的显示这些条目。在本例中,我们要显示提前预定义的数量的条目,而且每个条目的大小是根据条目当前数量,和当前可用的大小自适应的。如图:

1_-JY29_k-1uQaikLfOrrAzQ.gif

我们来看代码:

@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的值就可以。

1_2selNn6ST8T-cI0xPz_eEQ.gif

你应该知道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来计算要显示多少列的。

还可以根据容器大小设定文字大小,代码在这里

原文地址在这里