• Jetpack Compose学习(9)——Compose中的列表控件(LazyRow和LazyColumn)


    🚀 优质资源分享 🚀

    学习路线指引(点击解锁)知识定位人群定位
    🧡 Python实战微信订餐小程序 🧡进阶级本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
    💛Python量化交易实战💛入门级手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

    本文为作者原创,转载请注明出处,谢谢配合作者:https://blog.csdn.net/stars-one/p/15468564.html

    本篇大约有9444个字,阅读预计需要11.80分钟


    原文:Jetpack Compose学习(9)——Compose中的列表控件(LazyRow和LazyColumn) - Stars-One的杂货小窝

    经过前面的学习,大致上已掌握了compose的基本使用了,本篇继续进行扩展,讲解下载Compose中的列表控件LazyRowLazyColumn

    之前也是讲解Jetpack Compose学习(6)——关于Modifier的妙用 | Stars-One的杂货小窝,可以通过Modifier属性将Row和Column组件改造为可滑动的

    但是如果你需要显示大量的项目(或一个未知长度的列表),使用像 Column 这样的布局会导致性能问题,因为所有的项目都会被组合和布局,无论它们是否可见。

    本系列以往文章请查看此分类链接Jetpack compose学习

    基本使用

    这里由于LazyRowLazyColumn用法相似,只是展示的方向不同,所以便是不各自分个章节出来了,下文以LazyColumn为例讲解

    @SuppressLint("UnrememberedMutableState")
    @Preview(showBackground = true)
    @Composable
    fun ListPageDemo() {
    
        //可触发重组的List
        val list = arrayListOf()
    
     //构造数据
     repeat(30) {
     list.add("卡片$it")
     }
     
     ComposeDemoTheme {
     Column() {
     LazyColumn {
     items(list) {
     Text(
     it, modifier = Modifier
     .fillMaxWidth()
     .height(50.dp)
     )
     }
     }
     }
     }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    效果如下所示:

    上面主要使用了LazyListScope里提供的items方法来构造列表

    除此之外,LazyListScope也是提供了几个不同的方法来构造列表

    LazyColumn {
        // 添加单个项目
        item {
            Text(text = "First item")
        }
    
        // 添加五个项目
        items(5) { index ->
            Text(text = "Item: $index")
        }
    
        // 添加其他单个项目
        item {
            Text(text = "Last item")
        }
        
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    可观察数据列表 mutableStateListOf()

    上面那种,由于我们是使用的基本数据类型的ArrayList,所以在列表数据发生变更时,不会触发重组

    如果我们想要实现可触发重组的数据列表,可以使用Compose中提供的mutableStateListOf()方法来创建数据列表

    如下面例子:

    @SuppressLint("UnrememberedMutableState")
    @Preview(showBackground = true)
    @Composable
    fun ListPageDemo() {
    
        //可触发重组的List
        val list = mutableStateListOf()
    
     repeat(30) {
     list.add("卡片$it")
     }
     ComposeDemoTheme {
     Box(modifier = Modifier) {
     Column() {
     LazyColumn {
     items(list) {
     Text(
     it, modifier = Modifier
     .fillMaxWidth()
     .height(50.dp)
     )
     }
     }
     }
     //设置靠右下角
     Column(
     horizontalAlignment = Alignment.End,
     verticalArrangement = Arrangement.Bottom,
     modifier = Modifier.fillMaxSize().padding(end = 16.dp,bottom = 16.dp)
     ) {
     FloatingActionButton(onClick = {
     //移除列表最后一个数据
     list.removeLast()
     }) {
     Icon(
     imageVector = Icons.Default.Clear,
     contentDescription = null
     )
     }
     FloatingActionButton(onClick = {
     //添加一个新的数据
     val time = System.currentTimeMillis()
     list.add(time.toString())
     }) {
     Icon(
     imageVector = Icons.Default.Add,
     contentDescription = null
     )
     }
     }
     }
    
     }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    为了方便演示,加了两个悬浮按钮,用来测试数据的增加和删除,效果如下所示:

    mutableStateListOf()方法返回的类型是SnapshotStateList,此类型和ArrayList一样,有着相关的添加,移除数据等方法,同时还能触发Compose中的重组操作

    属性

    我们从构造方法来看下LazyColumn具有什么参数可以设置

    fun LazyColumn(
     modifier: Modifier = Modifier,
     state: LazyListState = rememberLazyListState(),
        contentPadding: PaddingValues = PaddingValues(0.dp),
        reverseLayout: Boolean = false,
        verticalArrangement: Arrangement.Vertical =
            if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
        horizontalAlignment: Alignment.Horizontal = Alignment.Start,
        flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
        content: LazyListScope.() -> Unit
    ){}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    modifier想必也不用多说了,不清楚了可以看下前面的文章

    FlingBehavior这个属性是用于定义滑动动作释放后的速度变化逻辑的,比如,当滑动动作释放后,列表还将继续滑动,速度依时递减

    此属性有点不太常用,就不讲解了

    由于state这个属性涉及东西较多,所以单独放在后面讲解

    contentPadding

    此属性主要是用来设置内边距的,取值为PaddingValues

    PaddingValues方法的参数有三种,根据需要选择即可:

    • PaddingValues(all:Dp)
    • PaddingValues(horizontal: Dp, vertical: Dp)
    • PaddingValues(start: Dp = 0.dp,top: Dp = 0.dp,end: Dp = 0.dp,bottom: Dp = 0.dp)

    示例代码(设置内边距为16dp):

    LazyColumn(contentPadding = PaddingValues(16.dp)) {
        items(list) {
            Text(
                it, modifier = Modifier
                    .fillMaxWidth()
                    .height(50.dp)
            )
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    效果:

    reverseLayout

    将列表顺序反转过来,接收一个boolean数值

    示例代码:

    LazyColumn(reverseLayout = true) {
        items(list) {
            Text(
                it, modifier = Modifier
                    .fillMaxWidth()
                    .height(50.dp)
            )
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    效果:

    PS:这个时候如果新增一个数据项item,item会出现在最上面的位置

    verticalArrangement

    此属性组主要是用来设置item的相互间距,针对的是LazyColumn

    PS: LazyRow则是horizontalArrangement属性

    LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
        items(list) {
            Text(
                it, modifier = Modifier
                    .fillMaxWidth()
                    .height(50.dp)
                    .background(color = Color.Yellow)
            )
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    效果:

    horizontalAlignment

    设置水平对齐方式,是针对LazyColumn

    PS:LazyRow中的属性则是verticalAlignment

    LazyColumn(Modifier.fillMaxWidth(),horizontalAlignment = Alignment.End) {
        items(list) {
            Text(
                it, modifier = Modifier
                    .height(50.dp)
                    .background(color = Color.Yellow)
            )
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    注意,要设置LazyColumn为填充最大宽度,然后item项是没有最大宽度的,才会看到效果

    效果:

    state

    接收LazyListState对象,主要是提供一个可观察的状态,用来实现控制和观察列表组件,如滚动到列表某一项的时候,需要展示一个悬浮按钮等逻辑

    LazyListState有以下常用属性:

    • firstVisibleItemIndex 当前页面列表显示的第一项的下标
    • firstVisibleItemScrollOffset 当前页面列表显示的第一项的滑动偏移量
    • interactionSource 当列表被拖拽时候,会触发对应的分发事件,interactionSource存放着相关的事件state
    • layoutInfo 列表布局相关信息
    • isScrollInProgress 一个boolean数值,标识当前列表是否处于滑动状态

    对滚动位置做出反应示例

    我们以firstVisibleItemIndex为例,可以实现滚动到列表某一项的时候,需要展示一个悬浮按钮的功能

    要实现上述功能,肯定得要一个boolean的state对象才行,但firstVisibleItemIndex只是一个Int类型,如何将其转换为可观察的boolean数值(MutableState)呢?

    这里可以使用derivedStateOf()方法来进行转换

    val state = rememberLazyListState()
    val showButton by remember {
        derivedStateOf {
            state.firstVisibleItemIndex > 5
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    示例代码:

    @SuppressLint("UnrememberedMutableState")
    @Preview(showBackground = true)
    @Composable
    fun ListPageDemo() {
    
        //可触发重组的List
        val list = mutableStateListOf()
    
     repeat(30) {
     list.add("卡片$it")
     }
     val state = rememberLazyListState()
    
    
     val showButton by remember {
     derivedStateOf {
     state.firstVisibleItemIndex > 5
     }
     }
     
     ComposeDemoTheme {
     Box(modifier = Modifier) {
     Column() {
     LazyColumn(state = state,modifier = Modifier.fillMaxWidth()) {
     items(list) {
     Text(
     it, modifier = Modifier
     .height(50.dp)
     .background(color = Color.Yellow)
     )
     }
     }
     }
    
     if (showButton) {
     //这里由于要设置悬浮按钮的位置,所以外层需要一个Box布局
     Box(Modifier.fillMaxSize().padding(bottom = 16.dp),contentAlignment = Alignment.BottomCenter) {
     FloatingActionButton(onClick = {
     }) {
     Icon(
     imageVector = Icons.Default.KeyboardArrowUp,
     contentDescription = null
     )
     }
     }
     }
     //设置靠右下角
     Column(
     horizontalAlignment = Alignment.End,
     verticalArrangement = Arrangement.Bottom,
     modifier = Modifier
     .fillMaxSize()
     .padding(end = 16.dp, bottom = 16.dp)
     ) {
     FloatingActionButton(onClick = {
     //移除列表最后一个数据
     list.removeLast()
     }) {
     Icon(
     imageVector = Icons.Default.Clear,
     contentDescription = null
     )
     }
     FloatingActionButton(onClick = {
     //添加一个新的数据
     val time = System.currentTimeMillis()
     list.add(time.toString())
     }) {
     Icon(
     imageVector = Icons.Default.Add,
     contentDescription = null
     )
     }
     }
     }
    
     }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79

    可以从下图看到,当列表的第一项为卡片6的时候,按钮即显示出来了:

    控制滚动

    除此之外,LazyListState还提供了一些方法,可以让我们控制列表自动滚动

    • scrollToItem(index:Int,scrollOffset: Int = 0) 滚动到指定的数据项
    • animateScrollToItem(index:Int,scrollOffset: Int = 0) 平滑滚动到指定的数据项

    注意这两个方法都是挂起方法,需要在协程中使用

    我们基于上面的例子,加以改造下,实现点击悬浮按钮,列表滚动回顶部的功能

    为例方便阅读,下面的代码稍微省略了一些不重要的代码:

    @SuppressLint("UnrememberedMutableState")
    @Preview(showBackground = true)
    @Composable
    fun ListPageDemo() {
    
        ...
        
        // 记住一个协程作用域,以便能够启动滚动操作
        val coroutineScope = rememberCoroutineScope()
       
        FloatingActionButton(onClick = {
            coroutineScope.launch {
                //滚动到第一项
                state.animateScrollToItem(0)
            }
        }) {
            Icon(
                imageVector = Icons.Default.KeyboardArrowUp,
                contentDescription = null
            )
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这里需要注意的是,使用滚动得通过协程来进行调用,通过rememberCoroutineScope()来得到一个协程作用域对象

    效果如下图所示:

    高级使用

    1.粘性标题

    LazyListScope除了items等方法,还有stickyHeader方法,帮助我们快速实现粘性标题的效果,如下图所示:

    代码:

    //可触发重组的List
    val list = mutableStateListOf()
    
    repeat(30) {
     list.add("title1 卡片$it")
    }
    
    //可触发重组的List
    val list1 = mutableStateListOf()
    
    repeat(30) {
     list1.add("title2 卡片$it")
    }
     
    LazyColumn(state = state, modifier = Modifier.fillMaxWidth()) {
    
     stickyHeader {
     Text(
     "title1",
     modifier = Modifier
     .fillMaxWidth()
     .background(color = Color.Green)
     )
     }
     items(list){
     Text(
     it, modifier = Modifier
     .height(50.dp)
     .background(color = Color.Yellow)
     )
     }
     stickyHeader {
     Text(
     "title2",
     modifier = Modifier
     .fillMaxWidth()
     .background(color = Color.Green)
     )
     }
     items(list1){
     Text(
     it, modifier = Modifier
     .height(50.dp)
     .background(color = Color.Yellow)
     )
     }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    上面与之前的代码,多了一个数据源list1来,当然还可以把代码通过循环来精简一下,这里不再过多补充了

    2.item动画

    • animateItemPlacement 但item重新排序的动画

    截止到目前,item动画目前只有重新排序的动画,其他的添加和移除item的动画官方还在开发中,详情可看问题 150812265

    测试的时候发现没有此API,似乎也是1.2.0以后的版本才有的API,而且是在实验中

    示例代码:

    LazyColumn {
        items(books, key = { it.id }) {
            Row(Modifier.animateItemPlacement()) {
                // ...
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    自定义动画:

    LazyColumn {
        items(books, key = { it.id }) {
            Row(Modifier.animateItemPlacement(
                tween(durationMillis = 250)
            )) {
                // ...
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    PS: 关于自定义动画,后面我会再出新的章节讲解,目前代码就先贴着

    3.分页

    借助 Paging 库,应用可以支持包含大量列表项的列表,根据需要加载和显示小块的列表。Paging 3.0 及更高版本通过 androidx.paging:paging-compose 库提供 Compose 支持。

    注意:只有 Paging 3.0 及更高版本提供 Compose 支持。如果您使用的是较低版本的 Paging 库,则需先迁移到 3.0。

    如需显示分页内容列表,可以使用 collectAsLazyPagingItems() 扩展函数,然后将返回的 LazyPagingItems 传入 LazyColumn 中的 items()。

    与视图中的 Paging 支持类似,您可以通过检查 item 是否为 null,在加载数据时显示占位符

    import androidx.paging.compose.collectAsLazyPagingItems
    import androidx.paging.compose.items
    
    @Composable
    fun MessageList(pager: Pager) {
        val lazyPagingItems = pager.flow.collectAsLazyPagingItems()
    
        LazyColumn {
            items(
              items = lazyPagingItems,
              // The key is important so the Lazy list can remember your
              // scroll position when more items are fetched!
              key = { message -> message.id }
            ) { message ->
                if (message != null) {
                    MessageRow(message)
                } else {
                    MessagePlaceholder()
                }
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    暂时贴上些代码,后面可能与网络请求一起讲解,敬请期待…

    更优雅使用列表组件

    1.item绑定key

    在上文开始也提到,我们是使用LazyListScope里的items方法来构建数据项的,但是我们并没有为我们的每一项数据设置一个key,所以会导致以下问题:

    如果数据集发生变化,这可能会导致问题,因为改变位置的 item 会失去任何记忆中的状态。
    如果你想象一下 LazyRow 在LazyColumn 中的情景,如果该行改变了 item 的位置,用户就会失去他们在该行中的滚动位置。

    所以这个时候,我们可以通过items方法里的keys参数来进行设置

    此参数接收一个lambda表达式(item: T) -> Any

    这里的Any是这里可返回任何类型的数据,但必须要要所提供的数据必须能够被存储在一个Bundle中,具体可点击链接查看该类文档

    LazyColumn(state = state, modifier = Modifier.fillMaxWidth()) {
        items(list, key = {
            //这里可返回任何类型的数据(Any),但必须要要所提供的数据必须能够被存储在一个 Bundle 中
            //直接使用本身作为字符串
            it
        }) {
            Text(
                it, modifier = Modifier
                    .height(50.dp)
                    .background(color = Color.Yellow)
            )
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2.使用contentType更好复用组件

    如果需要复用数据项内容时,可使用contentType来标明类型,如下的示例代码

    PS:此API在1.2.0版本提供,截止发文日期,1.2.0还处于beta中,可以查阅Jetpack各库版本

    LazyColumn {
        items(elements, contentType = { it.type }) {
            // ...
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.item的注意项

    • item最好定义固定宽高

    例如,当您希望在后期阶段异步检索一些数据(例如图片)以填充列表项时。

    这会使延迟布局在首次衡量时组合其所有项,因为项的高度为 0 像素,所以这类项可完全适合视口大小。

    待这些项加载完毕且高度增加后,延迟布局随后会舍弃首次不必要组合起来的所有其他项,因为这些项实际上无法适合视口。

    @Composable
    fun Item() {
        Image(
            painter = rememberImagePainter(data = imageUrl),
            modifier = Modifier.size(30.dp),
            // ...
        )
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 建议多个元素放入一个项中
    LazyColumn(
        // ...
    ) {
        item { Item(0) }
        item {
            Item(1)
            Divider()
        }
        item { Item(2) }
        // ...
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 避免嵌套可向同一方向滚动的组件

    如以下不推荐的代码:

    // Throws IllegalStateException
    Column(
        modifier = Modifier.verticalScroll(state)
    ) {
        LazyColumn {
            // ...
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    参考

  • 相关阅读:
    服务器端文档组件
    Github操作—SSH免密登录(六)——Git
    ABC310F Make 10 Again
    docker搭建 Nexus3 私服
    win10系统下使用opencv-dnn部署yolov5模型
    【代码精读】optee的线程向量表
    C++左值引用与右值引用
    深度学习论文: Rethinking Mobile Block for Efficient Attention-based Models及其PyTorch实现
    产品生命周期(PLM)发展历程及技术核心分析指导
    包装类与数据类型
  • 原文地址:https://blog.csdn.net/qq_43479892/article/details/126283041