Jetpack Compose中的列表

news2024/9/20 10:41:31

Cloumn 和 Row

如果是普通的不是特别长的列表,可以直接使用 ColumnRow 组件,默认 ColumnRow 组件是不支持滚动的,如果需要支持滚动可以在 ColumnRow 组件上使用 Modifier.verticalScroll()Modifier.horizontalScroll() 修饰符

@Composable
fun ColumnList(list: List<String>) {
    Box {
        Column(Modifier.verticalScroll(rememberScrollState())) {
            list.forEach {
                Text(it)
                Divider()
            }
        }
    }
}

@Composable
fun RowList(list: List<String>) {
    Box {
        Row(Modifier.horizontalScroll(rememberScrollState())) {
            list.forEach {
                Text(it)
                Divider(Modifier.width(1.dp).fillMaxHeight(), thickness = 1.dp)
            }
        }
    }
}

Column 的 verticalArrangement 参数可以指定item在主轴不同的排列方式:

@Composable
fun ColumnExample() {
    Column(
        modifier = Modifier.border(1.dp, Color.Blue),
        // verticalArrangement = Arrangement.Top // 默认是Top
    ) {
        Text("默认效果", Modifier.background(Color.Green))
        Text("默认效果", Modifier.background(Color.Green))
        Text("默认效果", Modifier.background(Color.Green))
    }
}

@Composable
fun ColumnExample2() {
    Column(
        modifier = Modifier.border(1.dp, Color.Blue),
        verticalArrangement = Arrangement.Center
    ) {
        Text("Arrangement.Center", Modifier.background(Color.Green))
        Text("Arrangement.Center", Modifier.background(Color.Green))
        Text("Arrangement.Center", Modifier.background(Color.Green))
    }
}

@Composable
fun ColumnExample201() {
    Column(
        modifier = Modifier.border(1.dp, Color.Blue),
        verticalArrangement = Arrangement.SpaceAround
    ) {
        Text("Arrangement.SpaceAround", Modifier.background(Color.Green))
        Text("Arrangement.SpaceAround", Modifier.background(Color.Green))
        Text("Arrangement.SpaceAround", Modifier.background(Color.Green))
    }
}

@Composable
fun ColumnExample202() {
    Column(
        modifier = Modifier.border(1.dp, Color.Blue),
        verticalArrangement = Arrangement.SpaceBetween
    ) {
        Text("Arrangement.SpaceBetween", Modifier.background(Color.Green))
        Text("Arrangement.SpaceBetween", Modifier.background(Color.Green))
        Text("Arrangement.SpaceBetween", Modifier.background(Color.Green))
    }
}

@Composable
fun ColumnExample203() {
    Column(
        modifier = Modifier.border(1.dp, Color.Blue),
        verticalArrangement = Arrangement.SpaceEvenly
    ) {
        Text("Arrangement.SpaceEvenly", Modifier.background(Color.Green))
        Text("Arrangement.SpaceEvenly", Modifier.background(Color.Green))
        Text("Arrangement.SpaceEvenly", Modifier.background(Color.Green))
    }
}

@Composable
fun ColumnExample204() {
    Column(
        modifier = Modifier.border(1.dp, Color.Blue),
        verticalArrangement = Arrangement.spacedBy(10.dp)
    ) {
        Text("Arrangement.spacedBy(10.dp)", Modifier.background(Color.Green))
        Text("Arrangement.spacedBy(10.dp)", Modifier.background(Color.Green))
        Text("Arrangement.spacedBy(10.dp)", Modifier.background(Color.Green))
    }
}

@Composable
fun ColumnExample205() {
    Column(
        modifier = Modifier.border(1.dp, Color.Blue),
        verticalArrangement = Arrangement.Bottom
    ) {
        Text("Arrangement.Bottom", Modifier.background(Color.Green))
        Text("Arrangement.Bottom", Modifier.background(Color.Green))
        Text("Arrangement.Bottom", Modifier.background(Color.Green))
    }
}

在这里插入图片描述

下面的动图总结了不同垂直排列方式的效果:

在这里插入图片描述

同样,通过 horizontalAlignment 参数可以指定item在交叉轴(横轴)上的排列方式:

@Composable
fun ColumnExample3() {
    Column(
        modifier = Modifier.border(1.dp, Color.Blue),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("ffff", Modifier.background(Color.Green))
        Text("Alignment.CenterHorizontally", Modifier.background(Color.Green))
    }
}

@Composable
fun ColumnExample4() {
    Column(
        modifier = Modifier.border(1.dp, Color.Blue),
        horizontalAlignment = Alignment.End
    ) {
        Text("kkk", Modifier.background(Color.Green))
        Text("Alignment.End", Modifier.background(Color.Green))
    }
}

在这里插入图片描述

Row 组件也可以通过 horizontalArrangement 参数指定item在水平方向的排列方式:

@Composable
fun RowExample() {
    val modifier = Modifier
        .padding(5.dp)
        .clip(RoundedCornerShape(35))
        .background(Color.Green)
    val modifier2 = modifier
        .width(50.dp)
        .padding(5.dp)
    Column {
        Divider()
        Text("Equal Weight")
        Row {
            Box(modifier
                .weight(1f)
                .padding(5.dp)) {
                Text(text = "A", Modifier.align(Alignment.Center))
            }
            Box(modifier
                .weight(1f)
                .padding(5.dp)) {
                Text(text = "B", Modifier.align(Alignment.Center))
            }
            Box(modifier
                .weight(1f)
                .padding(5.dp)) {
                Text(text = "C", Modifier.align(Alignment.Center))
            }
        }
        Divider()
        Text("Arrangement.Start")
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.Start
        ) {
            Box(modifier2) {
                Text(text = "A", Modifier.align(Alignment.Center))
            }
            Box(modifier2) {
                Text(text = "B", Modifier.align(Alignment.Center))
            }
            Box(modifier2) {
                Text(text = "C", Modifier.align(Alignment.Center))
            }
        }
        Divider()
        Text("Arrangement.Center")
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.Center
        ) {
            Box(modifier2) {
                Text(text = "A", Modifier.align(Alignment.Center))
            }
            Box(modifier2) {
                Text(text = "B", Modifier.align(Alignment.Center))
            }
            Box(modifier2) {
                Text(text = "C", Modifier.align(Alignment.Center))
            }
        }
        Divider()
        Text("Arrangement.End")
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.End
        ) {
            Box(modifier2) {
                Text(text = "A", Modifier.align(Alignment.Center))
            }
            Box(modifier2) {
                Text(text = "B", Modifier.align(Alignment.Center))
            }
            Box(modifier2) {
                Text(text = "C", Modifier.align(Alignment.Center))
            }
        }
        Divider()
        Text("Arrangement.SpaceBetween")
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Box(modifier2) {
                Text(text = "A", Modifier.align(Alignment.Center))
            }
            Box(modifier2) {
                Text(text = "B", Modifier.align(Alignment.Center))
            }
            Box(modifier2) {
                Text(text = "C", Modifier.align(Alignment.Center))
            }
        }
        Divider()
        Text("Arrangement.SpaceAround")
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceAround
        ) {
            Box(modifier2) {
                Text(text = "A", Modifier.align(Alignment.Center))
            }
            Box(modifier2) {
                Text(text = "B", Modifier.align(Alignment.Center))
            }
            Box(modifier2) {
                Text(text = "C", Modifier.align(Alignment.Center))
            }
        }
        Divider()
        Text("Arrangement.SpaceEvenly")
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceEvenly
        ) {
            Box(modifier2) {
                Text(text = "A", Modifier.align(Alignment.Center))
            }
            Box(modifier2) {
                Text(text = "B", Modifier.align(Alignment.Center))
            }
            Box(modifier2) {
                Text(text = "C", Modifier.align(Alignment.Center))
            }
        }
        Divider()
        Text("Arrangement.spacedBy(20.dp)")
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.spacedBy(20.dp)
        ) {
            Box(modifier2) {
                Text(text = "A", Modifier.align(Alignment.Center))
            }
            Box(modifier2) {
                Text(text = "B", Modifier.align(Alignment.Center))
            }
            Box(modifier2) {
                Text(text = "C", Modifier.align(Alignment.Center))
            }
        }
        Divider()
    }
}

在这里插入图片描述

下面的动图总结了不同水平排列方式的效果:

在这里插入图片描述

LazyColumn 和 LazyRow

这两个是支持惰性加载的列表组件,只有屏幕显示的部分才会真正被加载,对标传统View中的RecyclerView

@Composable
fun LazyColumnList() {
    LazyColumn(
        Modifier
            .fillMaxSize()
            .background(Color.Gray),
        contentPadding = PaddingValues(35.dp), 
        verticalArrangement = Arrangement.spacedBy(10.dp)
    ) {
        items(50) { index ->
            CardContent("我是序号第 $index 的卡片")
        }
    }
}

在这里插入图片描述

在LazyList组件中是通过使用itemsitem这两个DSL来添加item组件的,通过item DSL可以很方便的指定 header 和 footer

@Composable
fun LazyColumnList2() {
    val list = listOf("A", "B", "C")
    LazyColumn(
        Modifier.fillMaxSize(),
        contentPadding = PaddingValues(35.dp),
        verticalArrangement = Arrangement.spacedBy(10.dp)
    ) {
        item {
            Text("Header 第一项")
        }
        items(5) { index ->
            Text("第 ${index + 2} 项")
        }
        itemsIndexed(list) { index, s ->
            Text("第 ${index + 7}$s")
        }
        item {
            Text("Footer 最后一项")
        }
    }
}

在这里插入图片描述

粘性 header

粘性header通过 stickyHeader 这个DSL来实现:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyColumnList5() {
    val sections = listOf("A", "B", "C", "D", "E", "F", "G")
    LazyColumn(
        /*reverseLayout = true, */
        contentPadding = PaddingValues(15.dp)
    ) {
        sections.forEach { section ->
            stickyHeader {
                Text(
                    "Section $section 粘性Header",
                    Modifier
                        .fillMaxWidth()
                        .background(Color.LightGray)
                        .padding(8.dp)
                )
            }
            items(10) {
                CardContent("Item $it from the section $section")
            }
        }
    }
}

在这里插入图片描述

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyRowExample() {
    LazyRow(
        Modifier
            .fillMaxWidth()
            .background(Color.Gray),
        contentPadding = PaddingValues(15.dp),
        horizontalArrangement = Arrangement.spacedBy(10.dp)
    ) {
        stickyHeader {
            Box(Modifier
                .height(150.dp)
                .background(Color.Red)
                .padding(8.dp)) {
                Text(text = "粘性Header", color = Color.White, fontSize = 20.sp)
            }
        }
        items(50) { index ->
            CardContent2(index)
        }
    }
}

在这里插入图片描述

如需实现具有多个标题的列表(例如“联系人列表”),可以执行以下操作,先通过groupBy进行分组:

// TODO: This ideally would be done in the ViewModel
val grouped = contacts.groupBy { it.firstName[0] }

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ContactsList(grouped: Map<Char, List<Contact>>) {
    LazyColumn {
        grouped.forEach { (initial, contactsForInitial) ->
            stickyHeader {
                CharacterHeader(initial)
            }

            items(contactsForInitial) { contact ->
                ContactListItem(contact)
            }
        }
    }
}

自定义排列方式

通常,延迟列表包含许多项,并且这些项所占空间大于滚动容器的大小。不过,如果列表中填充的项很少,那么在设计中,您可以对这些项在视口中的位置做出更具体的要求。

@Composable
fun TopWithFooterExample() {
    val list = listOf("A", "B", "C", "I am the Footer")
    LazyColumn(
        Modifier.fillMaxHeight(),
        contentPadding = PaddingValues(15.dp),
        verticalArrangement = TopWithFooter // 使用自定义垂直 Arrangement
    ) {
        itemsIndexed(list) { index, item ->
            Box(modifier = Modifier
                .background(Color.Blue)
                .padding(10.dp)) {
                Text("第 $index$item", fontSize = 20.sp, color = Color.White)
            }
        }
    }
}
object TopWithFooter : Arrangement.Vertical {
    // 重写 arrange 方法
    override fun Density.arrange(totalSize: Int, sizes: IntArray, outPositions: IntArray) {
        var y = 0
        // 首先,它会将列表项逐个放在相应位置。
        sizes.forEachIndexed { index, size ->
            outPositions[index] = y
            y += size
        }
        // 其次,如果所用总高度低于视口高度,则会将页脚放置在底部:
        if (y < totalSize) {
            val lastIndex = outPositions.lastIndex
            outPositions[lastIndex] = totalSize - sizes.last()
        }
    }
}

在这里插入图片描述

contentPadding 内容边距

使用LazyList时,如果使用普通的 Modifier.padding() 为其添加内边距,效果不会像我们期望的那样,而是会在滑动到边界位置时内容出现padding大小的截断:

@Composable
fun AlignYourBodyRow(
    modifier: Modifier = Modifier
) {
    LazyRow(
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        // 使用修饰符修改左右的padding会导致滑动的时候出现截断
        modifier = modifier.padding(horizontal = 16.dp), 
    ) {
        items(alignYourBodyData) { item ->
            AlignYourBodyElement(item.drawable, item.text)
        }
    }
}

在这里插入图片描述

此时正确的做法应当使用 LazyListcontentPadding 属性来设置内边距:

@Composable
fun AlignYourBodyRow(
    modifier: Modifier = Modifier
) {
    LazyRow(
        horizontalArrangement = Arrangement.spacedBy(8.dp), 
        contentPadding = PaddingValues(horizontal = 16.dp), // 为列表设置正确的内边距
        modifier = modifier
    ) {
        items(alignYourBodyData) { item ->
            AlignYourBodyElement(item.drawable, item.text)
        }
    }
}

在这里插入图片描述
可以仔细观察对比一下二者的不同。

显示返回顶部按钮

通过 scrollState.firstVisibleItemIndex > 0 判断是否向下滚动了,然后通过 scrollState.scrollToItem(0) 返回顶部

@Composable
fun LazyColumnList3() {
    val scrollState = rememberLazyListState(15) // 从index=15的开始显示
    Box {
        LazyColumn(
            Modifier
                .fillMaxHeight()
                .background(Color.Gray), 
            verticalArrangement = Arrangement.spacedBy(10.dp),
            state = scrollState
        ) {
            items(50) { index ->
                CardContent("我是序号第 $index 的卡片")
            }
        }
        val showScrollToTopButton by remember {
            derivedStateOf { scrollState.firstVisibleItemIndex > 10 } // 这里根据实际业务一页大概展示多少条,可以判断超过一页后再显示
        }
        
        if (showScrollToTopButton) {
            val scope = rememberCoroutineScope()
            ExtendedFloatingActionButton(
                text = {
                    Column {
                        Icon(Icons.Default.KeyboardArrowUp, null)
                        Text("Top")
                    }
                },
                onClick = {
                    scope.launch { scrollState.scrollToItem(0) }
                },
                shape = CircleShape, // RoundedCornerShape(15),
                modifier = Modifier.size(70.dp).align(Alignment.BottomEnd),
                backgroundColor = Color.Green
            )
        }
    }
}

在这里插入图片描述
可以使用 AnimatedVisibility 为按钮添加动画:

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun LazyColumnList3() {
    val scrollState = rememberLazyListState(15) // 从index=15的开始显示
    Box {
        LazyColumn(
            Modifier
                .fillMaxHeight()
                .background(Color.Gray),
            verticalArrangement = Arrangement.spacedBy(10.dp),
            state = scrollState
        ) {
            items(50) { index ->
                CardContent("我是序号第 $index 的卡片")
            }
        }
        val showScrollToTopButton by remember {
            derivedStateOf { scrollState.firstVisibleItemIndex > 10 }
        }
        AnimatedVisibility(
            visible = showScrollToTopButton,
            modifier = Modifier.align(Alignment.BottomEnd),
            enter = fadeIn() + scaleIn(),
            exit = fadeOut() + scaleOut(),
        ) { 
            val scope = rememberCoroutineScope()
            ExtendedFloatingActionButton(
                text = {
                    Column {
                        Icon(Icons.Default.KeyboardArrowUp, null)
                        Text("Top")
                    }
                },
                onClick = {
                    scope.launch { scrollState.scrollToItem(0) }
                },
                shape = CircleShape, // RoundedCornerShape(15),
                modifier = Modifier.size(70.dp),  
                backgroundColor = Color.Green
            )
        }
    }
}

使用 scrollToItemanimateScrollToItem 时发现,有时不一定能够滑动到顶部,可以通过指定滑动的距离解决,如 animateScrollToItem(0, -10000)或者 scrollBy(-10000f)

LazyListState可以帮助我们分析滑动事件, 一个常见的例子是,系统会在用户滚动经过某个点后发送分析事件。 为了高效地解决此问题,我们可以使用 snapshotFlow()

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

LazyListState 还可以通过 layoutInfo 属性提供有关当前显示的所有列表项以及这些项在屏幕上的边界的信息。

contentType 多类型列表

Compose 1.2 开始,为了最大限度地提高延迟布局的性能,建议将 contentType 添加到列表或网格中。 当列表或网格由多种不同类型的项组成时,这样可为布局的每一项指定内容类型:

@Composable
fun MultiTypeList() {
    val list by remember { mutableStateOf(prepareNewsList()) }
    LazyColumn(
        contentPadding = PaddingValues(15.dp),
        verticalArrangement = Arrangement.spacedBy(10.dp)
    ) {
        items(list, contentType = { it.type }) { item ->
            if (item.type == 1) {
                Card(elevation = 8.dp, modifier = Modifier.fillMaxWidth()) {
                    Row(
                        horizontalArrangement = Arrangement.spacedBy(20.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Image(
                            painter = painterResource(id = R.drawable.ic_sky),
                            contentDescription = null,
                            modifier = Modifier.size(100.dp),
                            contentScale = ContentScale.Crop
                        )
                        Text(item.name.repeat(2), fontSize = 20.sp)
                    }
                }
            } else {
                CardContent(text = item.name.repeat(3))
            }
        }
    }
}
fun prepareNewsList() : List<NewsItem> {
    return List(50) {
        NewsItem("NewsItem $it ", if(it % 2 == 0) 1 else 2)
    }
}
data class NewsItem(val name : String, val type : Int)

在这里插入图片描述

延迟列表的使用注意事项

为列表项指定 key

注意:实际业务中使用LazyColumn必须为每个item提供一个稳定的 唯一key (默认是将position作为索引)

键的类型必须受 Bundle 支持,这是 Android 的机制,旨在当重新创建 activity 时保持相应状态。

Bundle 支持基本类型、枚举或 Parcelable 等类型。

Bundle 必须支持该键,以便在重新创建 activity 时,甚至在滚动离开此项然后滚动回来时,此项可组合项中的 rememberSaveable 仍可以恢复。

@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        items(
            items = messages,
            key = { message ->
                // Return a stable + unique key for the item
                message.id
            }
        ) { message ->
            MessageRow(message)
        }
    }
}
避免使用大小为 0 像素的列表项

如果使用分页,尤其是在加载图片列表时,必须为每个列表项预先提供一个固定高度大小的占位符,因为LazyColumn会在首次测量时认为高度没有限制,会一直进行组合,直到计算的测量高度填满可用的视图窗口,然后停止组合。

如果没有提供占位符(高度是0px)或者提供了高度很小的占位符,则意味着在首次测量时,LazyColumn会组合它的所有项,因为高度为0的项可以很容易容纳到当前窗口中。而在几毫秒之后,图片又加载出来了,LazyColumn开始重组显示图片,但此时只有一部分能被容纳到窗口中,因此LazyColumn会舍弃最开始组合的不必要的其他所有项。为了避免这种性能损耗,应该为每个item提供一个默认的高度大小的占位符,最好的做法是保证加载前后的item高度大小不变

避免嵌套可以同方向滚动的组件

这仅适用于将没有预定义尺寸的可滚动子级嵌套在可向同一方向滚动的另一个父级中的情况。

例如,尝试在可垂直滚动的 Column 父级中嵌套没有固定高度的子级 LazyColumn

 // Throws IllegalStateException
 Column(
   modifier = Modifier.verticalScroll(state)
 ) {
    LazyColumn {
        // ...
    }
 }

Compose不支持嵌套同方向的滚动列表,比如LazyColumn里嵌套LazyColumn,这种小心思就别想了,Google不让你这么玩~

但是可以在Row中嵌套LazyColumn,如果一定要在Column中嵌套LazyColumn,必须为LazyColumn指定固定的大小:

 Column(
    modifier = Modifier.verticalScroll(scrollState)
 ) {
    LazyColumn(
        modifier = Modifier.height(200.dp)
    ) {
        // ...
    }
 }
尽量避免将多个元素放入一个 item{...} 项中
 LazyVerticalGrid(
    // ...
 ) {
    item { Item(0) }
    item {
        Item(1)
        Item(2)
    }
    item { Item(3) }
    // ...
 }

在此示例中,第二个项 lambda 在一个代码块中发出 2 个项,当多个元素作为一个项的一部分发出时,系统会将其作为一个实体进行处理,这意味着这些元素无法再单独组合

也就是说当其中一个元素需要重组时,系统会将lambda项中的所有元素都进行重组。如果过度使用,则可能会降低性能

将所有元素放入一个项属于极端情况,完全违背了使用延迟布局的目的。除了潜在的性能问题外,这种情况还会干扰 scrollToItem()animateScrollToItem()。如上面代码如果调用scrollToItem(index = 2) 则实际会滑动到Item(3)的位置,因为Item(1)Item(2)被视为一个项。

不过,也有一些将多个元素放入一个项的有效用例,例如在一个列表内添加多条分隔线。你可能不希望分隔线更改滚动索引,因为分割线不应被视为独立元素。此外,由于分隔线占空间很小,因此性能不会受到影响。
分隔线可能需要在其之前的那个项可见时显示,这样分割线就可纳入前一个项中:

 LazyVerticalGrid(
    // ...
 ) {
    item { Item(0) }
    item {
        Item(1)
        Divider()
    }
    item { Item(2) }
    // ...
 }

此外,如果要测试延时列表的性能应当在release版本的模式下测试,不应该测试debug版本(并且是在真机而不是模拟器上测试,多次测试结果)

LazyVerticalGrid 和 LazyHorizontalGrid

可以滑动的表格组件,同时支持Column和Row的verticalArrangementhorizontalArrangement 属性效果,可以通过 columnsrows 属性指定列数或行数,例如 GridCells.Fixed(3)

@Composable
fun LazyVerticalGridExample() { 
    val itemsIndexedList = ('A'..'Z').toList()
    LazyVerticalGrid(
        columns = GridCells.Fixed(3),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(5.dp)
    ) {
        itemsIndexed(itemsIndexedList) { index, item ->
            GridItem("Item at $index is $item")
        }
    }
}

在这里插入图片描述

@Composable
fun LazyHorizontalGridExample() {
    val itemsIndexedList = ('A'..'Z').toList()
    LazyHorizontalGrid(
        rows = GridCells.Fixed(3),
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(5.dp)
    ) {
        itemsIndexed(itemsIndexedList) { index, item ->
            GridItem("Item at $index is $item")
        }
    }
}

在这里插入图片描述

设置单元格的大小占比权重

可以通过 span 参数设置每个单元格的大小占比权重,span 默认值是 1, 将 span 设置为不同的值可以实现简单的瀑布流效果:

@Composable
fun LazyVerticalGridExample2_1() {
    val list = ('A'..'Z').toList()
    val spanList = listOf(1, 2, 1, 3, 1, 1, 3, 4, 2, 2, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 3, 1, 1, 1)
    LazyVerticalGrid(
        columns = GridCells.Fixed(4),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(5.dp)
    ) {
        itemsIndexed(list, span={index, item -> GridItemSpan( spanList[index])}) { index, item ->
            Text(
                "Item $item",
                modifier = Modifier
                    .clip(RoundedCornerShape(5.dp))
                    .background(colors[index % colors.size])
                    .height(80.dp)
                    .wrapContentSize()
                    .padding(5.dp),
                color = Color.White,
                fontSize = 16.sp
            )
        }
    }
}

在这里插入图片描述

如果设置 span = GridItemSpan(maxLineSpan) 则会独占一整行,可以用来实现常见的分组标题的效果:

@Composable
fun LazyVerticalGridExample2() {
    val sections = (0 until 25).toList().chunked(5)
    LazyVerticalGrid(
        columns = GridCells.Fixed(4),
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(5.dp)
    ) {
        sections.forEachIndexed { index, items ->
            // maxLineSpan 是 LazyGridItemSpanScope 的值之一
            // 设置 span = GridItemSpan(maxLineSpan) 即独占一整行
            item(span = { GridItemSpan(maxLineSpan) }) {
                Text(
                    "This is section $index",
                    Modifier
                        .clip(RoundedCornerShape(5.dp))
                        .background(MaterialTheme.colorScheme.primary)
                        .height(50.dp)
                        .wrapContentSize(),
                    color = Color.White,
                    fontSize = 16.sp
                )
            }
            items(
                items, 
                span = { GridItemSpan( if(it % 2 == 0) 1 else 2) }
            ) {
                GridItem("Item $it")
            }
        }
    }
}

在这里插入图片描述

@Composable
fun LazyHorizontalGridExample2() {
    val sections = (0 until 25).toList().chunked(5)
    LazyHorizontalGrid(
        rows = GridCells.Fixed(3),
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(5.dp)
    ) {
        sections.forEachIndexed { index, items ->
            item(span = { GridItemSpan(maxLineSpan) }) {
                Text(
                    "section $index",
                    Modifier
                        .clip(RoundedCornerShape(5.dp))
                        .background(MaterialTheme.colorScheme.primary)
                        .wrapContentSize()
                        .padding(8.dp),
                    color = Color.White,
                    fontSize = 16.sp
                )
            }
            items(
                items,
                // not required as it is the default
                span = { GridItemSpan(1) }
            ) {
                GridItem("Item $it")
            }
        }
    }
}

在这里插入图片描述

GridCells.Adaptive() 自适应屏幕

例如,以下代码设置每个单元格至少100dp, 这在旋转屏幕场景下会自适应:

@Composable
fun LazyVerticalGridExample3() {
    val itemsIndexedList = ('A'..'Z').toList()
    LazyVerticalGrid( 
        columns = GridCells.Adaptive(100.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(5.dp)
    ) {
        itemsIndexed(itemsIndexedList) { index, item ->
            GridItem("Item at $index is $item")
        }
    }
}

在这里插入图片描述

自定义columns的每一列的宽度占比

@Composable
fun LazyVerticalGridExample4() {
    val itemsIndexedList = ('A'..'Z').toList()
    LazyVerticalGrid(
        columns = object: GridCells {
            override fun Density.calculateCrossAxisCellSizes(
                availableSize: Int,
                spacing: Int
            ): List<Int> {
                // 将第一列的宽度调整为可用空间的2/3,第二列的宽度调整为可用空间的1/3
                val firstColumn = (availableSize - spacing) * 2 / 3
                val secondColumn = availableSize - spacing - firstColumn
                return listOf(firstColumn, secondColumn)
            }
        },
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(5.dp)
    ) {
        itemsIndexed(itemsIndexedList) { index, item ->
            GridItem("Item at $index is $item")
        }
    }
}

在这里插入图片描述

LazyHorizontalStaggeredGrid 和 LazyVerticalStaggeredGrid 瀑布流

LazyHorizontalStaggeredGridLazyVerticalStaggeredGrid 主要为了支持瀑布流效果

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyVerticalStaggeredGridExample() {
    LazyVerticalStaggeredGrid(
        columns = StaggeredGridCells.Fixed(3), 
        contentPadding = PaddingValues(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ){
        itemsIndexed(list){ index, item ->
            Card(
                shape = RoundedCornerShape(4.dp),
                backgroundColor = colors[index % colors.size],
            ) {
                Text(
                    text = "$index $item",
                    color = Color.White,
                    modifier = Modifier.padding(10.dp)
                )
            }
        }
    }
}

在这里插入图片描述

LazyVerticalStaggeredGrid 的单元格同样支持Adaptive自适应属性, 例如将上面的代码中 columns 属性改成 StaggeredGridCells.Adaptive(80.dp),则效果如下:

在这里插入图片描述

LazyHorizontalStaggeredGrid使用是类似的:

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyHorizontalStaggeredGridExample() {
    LazyHorizontalStaggeredGrid(
        rows = StaggeredGridCells.Adaptive(80.dp),
        contentPadding = PaddingValues(8.dp),
        verticalArrangement = Arrangement.Center,
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ){
        itemsIndexed(list){ index, item ->
            Card(
                shape = RoundedCornerShape(4.dp),
                backgroundColor = colors[index % colors.size],
            ) {
                Box {
                    Text(
                        text = "$index $item",
                        color = Color.White,
                        modifier = Modifier.padding(10.dp).align(Alignment.Center)
                    )
                }
            }
        }
    }
}

在这里插入图片描述
LazyHorizontalStaggeredGridLazyVerticalStaggeredGrid 中的 items DSL目前不支持设置span参数,这意味着不能在固定列数/行数的情况下指定某个item占据多个行或多个列。也就是目前还无法实现如下效果:

在这里插入图片描述
如果想要实现此效果,只能使用前面的 LazyVerticalGrid 然后结合 RowColumn甚至是ConstraintLayout来设置每个出现横纵交错的内容块里面的布局,比较麻烦,自适应性肯定不好,还是希望官方能给出支持。

下拉刷新 Modifier.pullRefresh

Compose 1.3.0开始,SDK自带了对下拉刷新的支持(在此之前的版本是通过Accompanist库中提供的SwipeRefresh组件),通过在Box组件上应用修饰符Modifier.pullRefresh即可。

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PullToRefreshModifier() {
    val refreshScope = rememberCoroutineScope()
    var refreshing by remember { mutableStateOf(false) }
    var itemCount by remember { mutableStateOf(15) }

    fun refresh() = refreshScope.launch {
        refreshing = true
        delay(1000)
        itemCount += 5
        refreshing = false
    }

    val state = rememberPullRefreshState(refreshing, ::refresh)

    Box(Modifier.pullRefresh(state)) {
        LazyColumn(Modifier.fillMaxSize()) {
            items(itemCount) {
                ListItem { Text(text = "Item ${itemCount - it}") }
            }
        }

        PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
    }
}

在这里插入图片描述
其中rememberPullRefreshState需要传递一个refreshing标记状态以及一个refresh方法,refresh是请求刷新数据的业务逻辑,实际项目中,应该放在ViewModel中:

val viewModel: MyViewModel = viewModel()
val refreshing by viewModel.isRefreshing

val pullRefreshState = rememberPullRefreshState(refreshing, { viewModel.refresh() })

Box(Modifier.pullRefresh(pullRefreshState)) {
    LazyColumn(Modifier.fillMaxSize()) {
        ...
    }
    PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))
}

此外,Modifier.pullRefresh还有一个重载函数,可以提供 onPullonRelease 两个动作的回调,方便我们在不同的触发时机进行一些业务操作,如显示自定义的Indicator等。

@ExperimentalMaterialApi
fun Modifier.pullRefresh(
    onPull: (pullDelta: Float) -> Float,
    onRelease: suspend (flingVelocity: Float) -> Unit,
    enabled: Boolean = true
): Modifier

以下代码在下拉时根据下拉距离改变顶部的水平进度条的进度,在释放时进行刷新请求数据并执行动画

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PullToRefreshModifier2() {
    val refreshScope = rememberCoroutineScope()
    val threshold = 160.dp.toPx()

    var refreshing by remember { mutableStateOf(false) }
    var itemCount by remember { mutableStateOf(15) }
    var currentDistance by remember { mutableStateOf(0f) }

    val progress = currentDistance / threshold

    fun refresh() = refreshScope.launch {
        refreshing = true
        delay(1000)
        itemCount += 5
        refreshing = false
    }

    fun onPull(pullDelta: Float): Float = when {
        refreshing -> 0f
        else -> {
            val newOffset = (currentDistance + pullDelta).coerceAtLeast(0f)
            val dragConsumed = newOffset - currentDistance
            currentDistance = newOffset
            dragConsumed
        }
    }

    suspend fun onRelease() {
        if (refreshing) return // Already refreshing - don't call refresh again.
        if (currentDistance > threshold) refresh()

        animate(
            initialValue = currentDistance,
            targetValue = 0f,
            animationSpec = tween(1000, easing = LinearOutSlowInEasing)
        ) { value, _ ->
            currentDistance = value
        }
    }

    Box(Modifier.pullRefresh(::onPull, { onRelease() })) {
        LazyColumn {
            items(itemCount) {
                ListItem { Text(text = "Item ${itemCount - it}") }
            }
        }

        // Custom progress indicator
        AnimatedVisibility(visible = (refreshing || progress > 0)) {
            if (refreshing) {
                LinearProgressIndicator(Modifier.fillMaxWidth())
            } else {
                LinearProgressIndicator(progress, Modifier.fillMaxWidth())
            }
        }
    }
}

在这里插入图片描述
将上面代码中的LinearProgressIndicator换成CircularProgressIndicator,还可以根据下拉的距离改变圆形Indicator到顶部的距离

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PullToRefreshModifier2() {
  .......
  Box(Modifier.pullRefresh(::onPull, { onRelease() })) {
        LazyColumn {
            items(itemCount) {
                ListItem { Text(text = "Item ${itemCount - it}") }
            }
        }
        if (currentDistance > 0) {
            CircularProgressIndicator(
                modifier = Modifier
                    .align(Alignment.TopCenter)
                    .size(50.dp)
                    .offset(y = if (refreshing) 0.dp else currentDistance.toDp()),
                color = Color.Red,
                strokeWidth = 3.dp
            )
        }
   }
}

在这里插入图片描述
官方还提供了一个很方便的修饰符 Modifier.pullRefreshIndicatorTransform 来根据PullRefreshState自动改变Indicator的平移位置和缩放大小

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PullToRefreshModifier4() {
    val refreshScope = rememberCoroutineScope()
    var refreshing by remember { mutableStateOf(false) }
    var itemCount by remember { mutableStateOf(15) }

    fun refresh() = refreshScope.launch {
        refreshing = true
        delay(1500)
        itemCount += 5
        refreshing = false
    }

    val state = rememberPullRefreshState(refreshing, ::refresh)
    val rotation = animateFloatAsState(state.progress * 120)

    Box(Modifier.fillMaxSize().pullRefresh(state)) {
        LazyColumn {
            items(itemCount) {
                ListItem { Text(text = "Item ${itemCount - it}") }
            }
        }

        Surface(
            modifier = Modifier
                .size(50.dp)
                .align(Alignment.TopCenter)
                .pullRefreshIndicatorTransform(state, scale = true)
                .rotate(rotation.value),
            shape = RoundedCornerShape(10.dp),
            color = Color.DarkGray,
            elevation = if (state.progress > 0 || refreshing) 20.dp else 0.dp,
        ) {
            Box {
                if (refreshing) {
                    CircularProgressIndicator(
                        modifier = Modifier
                            .align(Alignment.Center)
                            .size(30.dp),
                        color = Color.White,
                        strokeWidth = 3.dp
                    )
                }
            }
        }
    }
}

在这里插入图片描述


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/142160.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

就在今晚!如何在公益向善的路上坚持前行

&#xff08;本文阅读时间&#xff1a;3分钟&#xff09;如何从支教助学到慈善组织到社会创业&#xff1f;如何从内心深处找回向善的力量&#xff1f;如何在挣扎中坚定前行的方向&#xff1f;本期微软ATP Public 100 公益演讲特邀嘉宾还将为小伙伴们提供干货建议&#xff01;微…

JavaScript刷LeetCode拿offer-树的遍历

什么是树 一种分层数据的抽象模型。前端工作中常见的树包括&#xff1a;DOM树&#xff0c;级联选择&#xff0c;树形控件JS中没有树&#xff0c;可以用Object和Array构建树树的常用操作&#xff1a;深度/广度优先遍历&#xff0c;先中后序遍历 深度优先遍历 访问根节点对根节…

Windows Server【开机启动和任务计划程序】实现服务器重启后项目自启动(Windows Server 任务计划程序无法执行问题处理)

1.问题说明 有些时候我们希望计算机开机后就启动一些服务或应用程序&#xff0c;比如远程工具。这里介绍两种方式。 2.开机启动 使用WinR调出运行&#xff0c;输入&#xff1a; 1️⃣ shell:startup 用户开机自启动&#xff08;程序开机自启动只针对当前登录的用户&#xf…

2分布式微服务技术栈-SpringCloud<Feign>

分布式微服务技术栈Feign HTTP 客户端Feign-基于Feign远程调用Feign-自定义配置Feign HTTP 客户端 Feign-基于Feign远程调用 声明式 事务 spring 声明一个 远程调用 封装 所有 对 userservice 的 远程调用 类似于 controller 的 注解 把接口 注入进来 不仅是 http 客户端 …

Node.js 中的模块化

1、模块化的基本概念 1.1、什么是模块化 模块化是指解决一个复杂问题时&#xff0c;自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说&#xff0c;模块是可组合、分解和更换的单元。 1.2、编程领域中的模块化 编程领域中的模块化&#xff0c;就是遵守固定的规则&a…

探索性数据分析(Exploratory Data Analysis,EDA)

目录参考资料PART 02 探索性数据分析 探索性数据分析(Exploratory Data Analysis&#xff0c;简称EDA)&#xff0c;指对数据分析的过程中尽量不加入先验假设&#xff0c;而是通过作图表和统计等方式来探索数据结构和规律。 EDA最早由John W. Tukey在上世纪70年代被提出&#…

批量下载线上数据文件--业务诉求

诉求接到现场运维诉求&#xff0c;需要获取到指定业务节点的所有附件文件分析某个节点的业务信息&#xff0c;可以通过sql来筛选出全部需要的关联字段信息以及文件磁盘路径和上传路径通过脚本或命令&#xff0c;将sql筛选到的路径文件统一复制到指定目录将复制到的文件下载&…

再学C语言25:分支和跳转——continue、break和switch

一般地&#xff0c;进入循环体后&#xff0c;在下次循环判断之前程序执行循环体中所有语句 一、continue语句 continue&#xff1a;循环中&#xff0c;当运行到该语句时&#xff0c;其将导致剩余的迭代部分被忽略&#xff0c;开始下一次迭代 如果continue处于嵌套结构中&…

【深度学习】李宏毅2021/2022春深度学习课程笔记 - 各式各样神奇的自注意力机制(Self-Attention)变型

文章目录一、Self-Attention 各式各样的变型二、How to make self-attention efficient&#xff1f;三、Notice四、Local Attention / Truncated Attention五、Stride Attention六、Global Attention七、Many Different Choices八、Can we only focus on Critical Parts?8.1 C…

车牌识别应用搭建(含模型和源码)

车牌识别应用搭建 内容说明 本示例旨在展示如何在 DeepStream SDK 版本不低于 5.0.1 的情况下使用分级模型进行检测和分类。 本例中的模型均为TAO3.0模型。 PGIE(car detection) -> SGIE(car license plate detection) -> SGIE(car license plate recognization) 该流…

Linux系统下的rpm/yum管理

文章目录Linux系统下的rpm管理1.介绍2.rpm包的简单查询指令3.rpm包的其它查询指今4.卸载rpm包5.rpm6.yumLinux系统下的rpm管理 1.介绍 rpm用于互联网下载包的打包及安装工具&#xff0c;它包含在某些Linux分发版中。它生成具有.RPM扩展名的文件。RPM是RedHat Package Manager…

Java 集合的介绍和使用

1.什么是集合&#xff1f; 对一些数据的存储就叫做集合&#xff0c;相比于数组&#xff0c;这是一种动态的集合。 1.可以动态的保存任意多个对象 2.提供一些动态操作集合的方法比如&#xff1a;add ,remove ,set ,get 等。 3.使用集合更加方便&#xff0c;提高代码编写效率。…

创建 ASP.NET Core MVC 项目

目录 一、创建ASP.NET Core MVC项目 二、ASP.NET Core MVC目录结构 一、创建ASP.NET Core MVC项目 打开Visual Studio 2022 点击创建新项目 在列表中找到:ASP.NET Core Web应用(模型-试图-控制器):用于创建包含示例ASP.Net Core Mvc视图和控制器的Asp.NET Core应用程序…

Docker如何安装nacos最新版本2.2.0

本文介绍如何在docker容器下安装部署nacos最新版本2.2.0。一、单机本地配置1.1 拉取nacos镜像执行以下命令从docker远端拉取nacos镜像文件。docker pull nacos/nacos-server1.2 启动nacos镜像执行以下命令&#xff0c;以单机模式启动nacos镜像。docker run -d --name nacos -p …

P5594 【XR-4】模拟赛————C++、Python

目录题目【XR-4】模拟赛题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1样例 #2样例输入 #2样例输出 #2样例 #3样例输入 #3样例输出 #3提示解题思路C的Code运行结果Python的代码运行结果题目 【XR-4】模拟赛 题目描述 X 校正在进行 CSP 前的校内集训。 一共有 nnn …

人工智能算法模型--Minimax(极大极小)搜索算法学习笔记

⬜⬜⬜ &#x1f430;&#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea; (*^▽^*)欢迎光临 &#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea;&#x1f430;⬜⬜⬜ ✏️write in front✏️ &#x1f4dd;个人主页&#xff1a;陈丹宇jmu &a…

基于MicroPython的ESP32在OLED上实时刷新温湿度

基于MicroPython的ESP32用起来后&#xff0c;做些小扩展功能&#xff1a; 1、通过DHT11获取温湿度&#xff1b; 2、将获取的温湿度显示在OLED上&#xff1b;并通过定时器&#xff0c;定时刷新&#xff1b; 3、OLED可显示中文&#xff1b; 一、DHT11获取温湿度 这个比较简单&a…

Java泛型详细内容讲解

1.什么是泛型 1.1背景&#xff1a; JAVA推出泛型以前&#xff0c;程序员可以构建一个元素类型为Object的集合&#xff0c;该集合能够存储任意的数据类型对象&#xff0c;而在使用该集合的过程中&#xff0c;需要程序员明确知道存储每个元素的数据类型&#xff0c;否则很容易引…

mysql中的实现字段或字符串拼接的三种方式

一、CONCAT函数concat函数是将多个字段或字符串拼接为一个字符串&#xff1b;但是字符串之间没有任何分隔。concat函数官方介绍-- CONCAT函数的语法如下&#xff1a; CONCAT(str1,str2,...)1.1、拼接非空字段或字符串SELECT CONCAT(字段1,字段2,字段3,...) from 表名;-- 拼接表…

C#构建Web服务项目实战(一)

概述本文通过VS2017&#xff0c;利用C#语言构建一个Web应用项目&#xff08;旧的ASP.NETWeb应用&#xff0c;非微软最新的.NETCore&#xff09;&#xff0c;并演示了如何利用Windows的IIS服务发布项目&#xff08;网站&#xff09;&#xff0c;以及在发布项目&#xff08;允许用…