原文: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<String>() //构造数据
repeat(30) {
list.add("卡片$it")
} ComposeDemoTheme {
Column() {
LazyColumn {
items(list) {
Text(
it, modifier = Modifier
.fillMaxWidth()
.height(50.dp)
)
}
}
}
} }

效果如下所示:

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

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

LazyColumn {
// 添加单个项目
item {
Text(text = "First item")
} // 添加五个项目
items(5) { index ->
Text(text = "Item: $index")
} // 添加其他单个项目
item {
Text(text = "Last item")
} }

可观察数据列表 mutableStateListOf()

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

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

如下面例子:

@SuppressLint("UnrememberedMutableState")
@Preview(showBackground = true)
@Composable
fun ListPageDemo() { //可触发重组的List
val list = mutableStateListOf<String>() 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
)
}
}
} }
}

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

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
){}

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)
)
}
}

效果:

reverseLayout

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

示例代码:

LazyColumn(reverseLayout = true) {
items(list) {
Text(
it, modifier = Modifier
.fillMaxWidth()
.height(50.dp)
)
}
}

效果:

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)
)
}
}

效果:

horizontalAlignment

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

PS:LazyRow中的属性则是verticalAlignment

LazyColumn(Modifier.fillMaxWidth(),horizontalAlignment = Alignment.End) {
items(list) {
Text(
it, modifier = Modifier
.height(50.dp)
.background(color = Color.Yellow)
)
}
}

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

效果:

state

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

LazyListState有以下常用属性:

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

对滚动位置做出反应示例

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

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

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

val state = rememberLazyListState()
val showButton by remember {
derivedStateOf {
state.firstVisibleItemIndex > 5
}
}

示例代码:

@SuppressLint("UnrememberedMutableState")
@Preview(showBackground = true)
@Composable
fun ListPageDemo() { //可触发重组的List
val list = mutableStateListOf<String>() 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
)
}
}
} }
}

可以从下图看到,当列表的第一项为卡片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
)
}
}

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

效果如下图所示:

高级使用

1.粘性标题

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

代码:

//可触发重组的List
val list = mutableStateListOf<String>() repeat(30) {
list.add("title1 卡片$it")
} //可触发重组的List
val list1 = mutableStateListOf<String>() 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)
)
} }

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

2.item动画

  • animateItemPlacement 但item重新排序的动画

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

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

示例代码:

LazyColumn {
items(books, key = { it.id }) {
Row(Modifier.animateItemPlacement()) {
// ...
}
}
}

自定义动画:

LazyColumn {
items(books, key = { it.id }) {
Row(Modifier.animateItemPlacement(
tween(durationMillis = 250)
)) {
// ...
}
}
}

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<Int, Message>) {
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.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)
)
}
}

2.使用contentType更好复用组件

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

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

LazyColumn {
items(elements, contentType = { it.type }) {
// ...
}
}

3.item的注意项

  • item最好定义固定宽高

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

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

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

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

如以下不推荐的代码:

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

参考

Jetpack Compose学习(9)——Compose中的列表控件(LazyRow和LazyColumn)的更多相关文章

  1. 【WPF学习】第二十三章 列表控件

    WPF提供了许多封装项的集合的控件,本章介绍简单的ListBox和ComboBox控件,后续哈会介绍更特殊的控件,如ListView.TreeView和ToolBar控件.所有这些控件都继承自Item ...

  2. soui中,列表控件动态高度的使用注意

    1.listview的模板template中,需要增加defHeight属性,即默认高度,同时,不能出现itemHeight属性,否则动态高度会失效 2.数据适配器中,重写getViewDesired ...

  3. MFC列表控件更改一行的字体颜色

    参考自(http://blog.csdn.net/ribut9225/article/details/6720639) 1.首先从CListCtrl 继承一个类,命名为CListCtrlCl 在头文件 ...

  4. Delphi中,indy控件实现收发邮件的几点学习记录( 可以考虑加入多线程,用多个邮箱做一个邮箱群发器) 转

    关于用Delphi中的Indy控件实现收发邮件的几点学习记录             这几天心里颇不宁静,不是因为项目延期,而是因为自己几个月前做的邮件发送程序至今无任何进展,虽然一向谦虚的人在网上发 ...

  5. Flex 列表控件中的操作

    主要操作包括:显示提示,使用图标,编辑列表条目中数据. 1.使用数据提示: 当鼠标停留在条目上时,可以显示该条目的相关数据提示. 当利用滚动条时,可以显示滚动条的相关提示. 在列表控件中使用showD ...

  6. 【WPF开发备忘】使用MVVM模式开发中列表控件内的按钮事件无法触发解决方法

    实际使用MVVM进行WPF开发的时候,可能会用到列表控件中每行一个编辑或删除按钮,这时直接去绑定,发现无法响应: <DataGridTemplateColumn Header="操作& ...

  7. 列表控件ListBox关联的MFC中的类:CListBox

    列表控件ListBox关联的MFC中的类:CListBox ######################################################## 1.在列表的结尾添加一项: ...

  8. 高级列表控件ListCtrl关联的MFC中的类:CListCtrl

    高级列表控件ListCtrl关联的MFC中的类:CListCtrl■ 报表样式ListCtrl常用操作:1.添加列标题头:InsertColumn2.获取与设置列宽:GetColumnWidth.Se ...

  9. Android TV开发总结(七)构建一个TV app中的剧集列表控件

    原文:Android TV开发总结(七)构建一个TV app中的剧集列表控件 版权声明:我已委托"维权骑士"(rightknights.com)为我的文章进行维权行动.转载务必转载 ...

随机推荐

  1. Fail2ban 命令详解 fail2ban-client

    Fail2ban的客户端操作命令,用于控制服务端. root@ubuntu:~# fail2ban-client --help Usage: /usr/bin/fail2ban-client [OPT ...

  2. 06vim --- gcc库的制作及使用

    VIM 命令模式下的操作 保存退出 快捷键 操作 ZZ 保存退出 代码格式化 快捷键 操作 gg=G 代码的格式化 光标移动(键盘上下左右键课代替) 快捷键 操作 h 光标左移 j 光标下移 k 光标 ...

  3. Android 实现开机自启APP

    原文地址:Android 实现开机自启APP - Stars-One的杂货小窝 公司有个项目,需要实现自启动的功能,本来想着是设置桌面启动器的方式去实现,但是设备是华为平板(EMUI系统),不允许设置 ...

  4. SpringSecurity简单入门

    1.简介 Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spr ...

  5. Canvas 线性图形(五):多边形

    前言 CanvasRenderingContext2D 没有提供绘制多边形的函数,所以只能由我们自己来实现绘制多边形的函数.以六边形为基础,需要用到三角函数:sin 和 cos. 点 A 坐标 (一) ...

  6. python基础学习8

    python基础学习8 内容概要 字典的内置方法 元组的内置方法 集合的内置方法 垃圾回收机制 内容详情 字典的内置方法 一.类型转换 res = dict(name='jason', pwd=123 ...

  7. 2021.06.05【NOIP提高B组】模拟 总结

    T1 题意:给你一个 \(n\) 个点 \(n\) 条边的有向图, 求每个店经过 \(K\) 条边后的边权和.最小边权 \(K\le 10^{10}\) 考试时:一直想着环,结果一直不知道怎么做 正解 ...

  8. 【Java面试】Mybatis中#{}和${}的区别是什么?

    一个工作2年的粉丝,被问到一个Mybatis里面的基础问题. 他跑过来调戏我,说Mic老师,你要是能把这个问题回答到一定高度,请我和一个月奶茶. 这个问题是: "Mybatis里面#{}和$ ...

  9. Vmware 10~16激活码/序列号 汇总

    Vmware 16 ZF3R0-FHED2-M80TY-8QYGC-NPKYF YF390-0HF8P-M81RQ-2DXQE-M2UT6 ZF71R-DMX85-08DQY-8YMNC-PPHV8 ...

  10. 循环码、卷积码及其python实现

    摘要:本文介绍了循环码和卷积码两种编码方式,并且,作者给出了两种编码方式的编码译码的python实现 关键字:循环码,系统编码,卷积码,python,Viterbi算法 循环码的编码译码 设 \(C\ ...