Jetpack Compose 用LazyVerticalGrid来实现MultiType Grid

3,479 阅读3分钟

大家都知道用RecyclerView GridLayoutManager 可以实现上图效果,也可以直接用 MultiType来实现。

注意:下面都是基于compose 1.1.1版本,1.2.0版本的LazyVerticalGrid已经不是试验性质的api了,api也有了变化, 大家可以见官网

在Jetpack Compose 中我们可以使用LazyVerticalGrid来实现:

LazyVerticalGrid(cells = GridCells.Fixed(2),
    contentPadding = PaddingValues(10.dp),
    verticalArrangement = Arrangement.spacedBy(10.dp),
    horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
    item(span = { GridItemSpan(3) }) { TopInfo() }

    itemsIndexed(imageList) { index, item ->
        key(index) {
            GridItemContent(item)
        }
    }
}
image.png

到这里感觉还行哈,接下来再加多一个类型,这次是一行3个:

LazyVerticalGrid(cells = GridCells.Fixed(6),
    contentPadding = PaddingValues(10.dp),
    verticalArrangement = Arrangement.spacedBy(10.dp),
    horizontalArrangement = Arrangement.spacedBy(10.dp)) {
    //top
    item(span = { GridItemSpan(6) }) { TopInfo() }
    //一行2个
    itemsIndexed(items = list1,
        spans = { _, _ ->
            GridItemSpan(3)
        },
        itemContent = { index, item ->
            GridItemContent(item.second)
        })
    //一行3个
    itemsIndexed(items = list2,
        spans = { _, _ ->
            GridItemSpan(2)
        },
        itemContent = { index, item ->
            GridItemContent(item.second)
        })
}
image.png

神马情况,图中红圈这两个item怎么跑到上面一行去了!! 那就换一种方式:

LazyVerticalGrid(cells = GridCells.Fixed(6),
    contentPadding = PaddingValues(10.dp),
    verticalArrangement = Arrangement.spacedBy(10.dp),
    horizontalArrangement = Arrangement.spacedBy(10.dp)) {

    itemsIndexed(items = list,
        spans = { index, item ->
            if (index == 0) {
                GridItemSpan(6)
            } else {
                if (item.first == 1) {
                    GridItemSpan(3)
                } else {
                    GridItemSpan(2)
                }
            }
        },
        itemContent = { index, item ->
            if (index == 0) {
                TopInfo()
            } else {
                GridItemContent(item.second)
            }
        })

结果发现还是一样的,并没有用, 这里的Jetpack Compose 是1.1.1版本的,这应该是LazyVerticalGrid的bug吧!目前LazyVerticalGrid还是实验性的。 (貌似compose 1.2.0-rc3依然有这个问题,在compose 1.2.0 LazyVerticalGrid已经不是试验性的API了,另外还多了一个LazyHorizontalGrid)

注意:实验性 API 将来可能会发生变化,也可能会被完全移除。

那如果想要做到顶图的效果要怎么办呢? 可以在这个类型的最后一行填补空的可组合项,让这一类型的最后一行把整行都填充就行了。 最后上代码:

const val COLUMN_COUNT = 6

enum class ViewType {
    banner, image, imageOther
}

fun getSpanCount(viewType: ViewType) = when (viewType) {
    ViewType.banner -> COLUMN_COUNT//一行只有一个item
    ViewType.image -> COLUMN_COUNT / 2//一行两个item
    else -> COLUMN_COUNT / 3//一行3个item
}

data class GridItem(val viewType: ViewType, val thumb: String) {
    //判断是否是占位item
    val isEmpty: Boolean
        get() = thumb.isBlank()
}

这里有3种类型,顶部的banner,一行占2个item的图片类型,一行占3个item的图片类型,在GridItem中,thumb为空就认为它是占位item,这里就要对list的数据进行处理了:

/**
 * 给该类型的最后行添加占位item
 */
fun getGridListByViewType(viewType: ViewType, list: List<String>): List<GridItem> {
    var columnCount = COLUMN_COUNT / getSpanCount(viewType)//一行有多少个item
    var lastRowColumnCount = list.size % columnCount//这个类型的最后一行有多少个item
    return if (lastRowColumnCount > 0)//给最后一行添加占位item
        list.map { GridItem(viewType, it) } +
                List(columnCount - lastRowColumnCount) { GridItem(viewType, "") }
    else
        list.map { GridItem(viewType, it) }
}

测试数据:

//添加banner
gridList.add(GridItem(ViewType.banner,"xxx"))

val imageUrlList = listOf("xxx","xxx","...")
//添加ViewType.image的items
gridList.addAll(getGridListByViewType(ViewType.image, imageUrlList))

val otherImageList: List<String> =listOf("xxx","xxx","...")

//添加ViewType.imageOther的items
gridList.addAll(getGridListByViewType(ViewType.imageOther, otherImageList))

接着是可组合项:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun GridContent() {
    val viewModel: MultiTypeViewModel = viewModel()
    val gridList = viewModel.gridList

    LazyVerticalGrid(cells = GridCells.Fixed(COLUMN_COUNT),
        contentPadding = PaddingValues(15.dp),
        verticalArrangement = Arrangement.spacedBy(15.dp),
        horizontalArrangement = Arrangement.spacedBy(15.dp)
    ){

        itemsIndexed(items = gridList,
            spans = { _, item ->
                GridItemSpan(getSpanCount(item.viewType))
            },
            itemContent = { index, item ->
                key(index) {
                    when (item.viewType) {
                        ViewType.banner -> {
                            Banner(item.thumb)
                        }
                        else -> {
                            GridItemContent(item)
                        }
                    }
                }
            })
    }
}
@Composable
fun GridItemContent(item: GridItem) {
    if (item.isEmpty) { //该类型最后一行占位用的
        Box() {
            //Text(text = "empty")
        }
    } else {
        Card() {
            ConstraintLayout(constraintSet = imageConstraint()) {
                //coil图片库的compose用法
                AsyncImage(
                    model = item.thumb,
                    contentDescription = null,
                    contentScale = ContentScale.Crop,
                    modifier = Modifier.layoutId("image")
                )
            }
        }
    }
}

/**
 * image是正方形的约束
 */
fun imageConstraint(): ConstraintSet {
    return ConstraintSet {
        val image = createRefFor("image")
        constrain(image) {
            width = Dimension.percent(1f)
            height = Dimension.ratio("1:1")
        }
    }
}
image.png

OK 搞定!!

Compose 1.2.0 版本已经修复了下面的问题了,如果大家是用的Compose 1.2.0及以上就不用往下看了,下面的是Compose 1.1.1的bug

这里记录一下:

  • contentPadding: 可以达到RecyclerView的clipToPadding="false"效果
  • verticalArrangement = Arrangement.spacedBy(SPACING.dp): 可以设置item纵向的间隔
  • horizontalArrangement = Arrangement.spacedBy(SPACING.dp):可以设置item横向的间隔

额 还没搞定,结果发现它又有个bug,滑动到最下面发现有一行空白。

image.png

后来发现把verticalArrangement = Arrangement.spacedBy(10.dp) 这个参数去掉就好了,但是去掉这个item上下直接的间距就没有了,没办法最后只能去掉它,在item里面的可组合项里设置padding吧!

const val SPACING = 15

LazyVerticalGrid(cells = GridCells.Fixed(COLUMN_COUNT),
    contentPadding = PaddingValues(top = SPACING.dp, start = SPACING.dp, end = SPACING.dp),
    horizontalArrangement = Arrangement.spacedBy(SPACING.dp)
){ 
    xxx
}
        
@Composable
fun GridItemContent(item: GridItem) {
    if (item.isEmpty) { //该类型最后一行占位用的
        Box(modifier = Modifier.padding(bottom = SPACING.dp)) {
            
        }
    } else {
        Card(modifier = Modifier.padding(bottom = SPACING.dp)) {
           xxx
        }
    }
}
untitled.gif