持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第18天,点击查看活动详情
概述
一般情况下,我们应用中的一个或者几个页面都可能需要使用列表来向用户展示大量的元素。在之前,我们已经学习过Column
和Row
这两种布局方式,使用这两种布局方式我们通常也能够实现类似于列表的效果。但是问题在于:这种实现方式往往会造成性能问题,因为对于列表中的每一个元素我们都创建了其对应的可组合项,但是对于处于屏幕外边的列表项这其实是不需要的。因此在Compose
中为我们提供了LazyColumn
和LazyRow
来实现不同方向上的列表。
延迟列表
LazyColumn
和LazyRow
也称为延迟列表,和RecyclerView
的效果相似,使用延迟列表创建的可组合项,只会对在组件窗口中可见的列表项进行组合和布局。LazyColumn
生成的是垂直滚动列表,而LazyRow
生成的是水平滚动列表。
延迟组件与Compose
中的大多数布局不同,它不是通过接受@Composable
内容快参数来允许应用直接发出可组合项,而是提供了一个LazyListScope.()
允许应用描述列表项内容,然后,延迟组件负责按照布局和滚动位置的要求添加每个列表项的内容。
定义一个延迟列表
延迟列表的定义非常简单,只不过和别的可组合项不同的是,content
参数接收一个LazyListScope.()
的扩展函数,下面的代码就定义了一个垂直方向滚动的延迟列表:
LazyColumn(content = {
},
modifier = Modifier
.weight(0.65f)
.background(Color.Cyan)
.fillMaxHeight()
)
复制代码
modifier是针对延迟列表布局的定义,可以不看,主要就是content
参数。
添加一个元素
LazyListScope
中提供了很多方法用于对延迟列表进行操作,其中使用item()
方法就可以向延迟列表中添加一项,如下面的代码所示:
item("first"){
Text(text = "这是第一个item")
}
复制代码
上面的代码就向延迟列表中添加了一个元素,我们指定了这个元素的key
是first
,第二个参数传递了一个可组合项,上面的代码最终的效果如下:
需要注意的是:每一项的key
都是不一样的,不能使用同样的key
,我们可以不指定key
的值,默认情况下使用当前的位置作为key
。
添加多个元素
使用items()
可以向延迟列表中添加多个元素,这个方法接收三个参数,分别是:
count
: 需要添加的元素的个数key: (index -> Any?)
: 基于当前位置设置的key信息,和上面的方法一样,key对于每一个item都是唯一的,同一个key不能给多个item使用itemContent
: 每一项的内容
下面的代码向延迟列表添加了一些元素:
items(count = 5,key = null){index ->
Text(text = "这是第${index}个元素")
}
复制代码
运行上面的代码,最终的效果如下:
添加标题
很多时候我们都需要在列表中添加标题,这可以用于对数据进行分类,每个分类下包含一组数据,列表滑动的时候标题将停留在顶部,直到有下一个标题滚动到顶部的时候会取代之前的标题,使用stickyHeader()
方法可以很容易地向延迟列表中添加标题,和添加普通的item
一样,我们仍然需要向这个标题添加唯一的key
以及其所对应的可组合项。
下面的代码演示了向延迟列表中添加标题的功能:
//添加一个标题
stickyHeader(key = "第一组数据") {
Text(
text = "这是第一组标题",
modifier = Modifier
.padding(vertical = 10.dp)
.background(Color.White)
)
}
复制代码
运行上面的代码可以看到如下的效果:
需要注意的是:这个方法被标记为实验性质的API,可能会在后期修改或者移除,我们应该谨慎使用这个方法。
扩展函数
LazyListScope
除了提供上面的方法之外,还向我们提供了一些扩展函数,通过这些扩展函数,我们可以更方便地对延迟列表进行操作。
LazyListScope.items()
和之前的items()
不同的地方在于这个方法允许我们传递一个集合,我们可以通过这个集合去设置每一个列表项的数据,一般情况下我们更倾向于使用这个方法,因为我们列表中的数据有很多来源,具体的数据类型也不清楚,所以使用这个方法能够更加满足我们的需求,下面的代码演示了使用这个方法添加一些列表项:
//定义一个集合
private val mDataList = mutableListOf<String>()
//向集合中添加元素
for (i in 0 until 20) {
mDataList.add("这是第${i}项")
}
//将元素设置到延迟列表中
items(mDataList) {
Text(
text = it,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp)
)
}
复制代码
运行上面的代码可以看到如下的效果:
LazyListScope.itemsIndexed()
和上面的方法相比,这个方法对其中的itemContent
参数进行了扩展,上面的方法中,我们只知道每一个列表项的内容是什么,在这个方法中,我们还可以知道每一个列表项是在集合的第几个位置。注意这里是集合的位置,而不是延迟列表的第几个位置。
下面的代码延迟了这个方法的使用:
itemsIndexed(mNameList) { index, item ->
Text(
text = "我是:$item",
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 5.dp)
.background(if (index % 2 == 0) Color.Gray else Color.Yellow)
)
}
复制代码
上面的代码中我们按照item在列表中的位置设置了不同的背景,运行上面的代码可以看到如下的效果:
其它扩展方法
LazyListScope
中还提供了剩余的两个扩展方法,这不过是这两个方法和上面的items()
,itemsIndexed()
方法基本相同,只是第一个参数由集合换成了数组,下面是这两个方法的签名:
inline fun <T> LazyListScope.items(
items: Array<T>,
noinline key: ((item: T) -> Any)? = null,
crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
) = items(items.size, if (key != null) { index: Int -> key(items[index]) } else null) {
itemContent(items[it])
}
inline fun <T> LazyListScope.itemsIndexed(
items: Array<T>,
noinline key: ((index: Int, item: T) -> Any)? = null,
crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit
) = items(items.size, if (key != null) { index: Int -> key(index, items[index]) } else null) {
itemContent(it, items[it])
}
复制代码
使用方式和上面的方法一样。
状态
我们可以向延迟列表提供state
参数来控制列表的滚动等,默认情况下这里使用的是LazyListState(0,0)
,也就是说初始显示的位置是第0个,并且偏移量也是0,我们在这里可以创建自己的LazyListState()
并传递自定义的参数来控制列表的状态。比如下面的代码设置了初始滚动到第10个列表项,并且偏移量为50,如下所示:
//创建state
private val listState by lazy {
LazyListState(firstVisibleItemIndex = 10,firstVisibleItemScrollOffset = 50)
}
//将state设置给延迟列表
state = listState
复制代码
下面的图片演示了设置成这样的效果:
可以看到:列表一开始就滚动到了相应的位置。
设置列表滚动的位置
LazyListState
还提供了控制列表滚动的方法,我们可以在需要的时候控制列表滚动的位置,其中animateScrollToItem
可以使用动画滚动到指定的位置,而scrollToItem
则是直接跳到指定的位置,这两个方法都接受两个参数,一个是要滚动到的位置,一个是偏移量。
下面的代码使用这两个方法将列表滚动到不同的位置:
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
listState.animateScrollToItem(0,0)
}
},modifier = Modifier.fillMaxWidth()) {
Text(text = "使用动画滚动到第一个位置")
}
Button(onClick = {
scope.launch {
listState.scrollToItem(20,0)
}
}) {
Text(text = "直接滚动到第20个位置")
}
复制代码
运行上面的代码。可以得到如下的效果:
其它方法
除了提供控制滚动的方法,该参数还可以提供以下方法和作用:
方法 | 说明 |
---|---|
firstVisibleItemIndex | 当前显示的第一个item所处的位置index |
firstVisibleItemScrollOffset | 当前显示的第一个item的偏移量 |
layoutInfo | 获取当前列表布局的相关信息,其中包括: ① 当前所有可见项目的列表 ②窗口的最大和最小偏移量,使用这两个参数可以判断可见列表中的哪些项目是完全可见的 ③延迟列表的总item数 |
isScrollInProgress | 判断列表当前是否正在滚动 |
我们可以获取其中的信息并打印出来,用以做相关的判断,如下所示:
private fun getScrollInfo() {
//获取当前可见的第一个item所处的位置和偏移量
val firstVisibleIndex = listState.firstVisibleItemIndex
val firstVisibleOffset = listState.firstVisibleItemScrollOffset
Log.i(TAG, "first item: $firstVisibleIndex,$firstVisibleOffset")
//layoutInfo
val layoutInfo = listState.layoutInfo
val count = layoutInfo.totalItemsCount
val startOffset = layoutInfo.viewportStartOffset
val endOffset = layoutInfo.viewportEndOffset
val list = listState.layoutInfo.visibleItemsInfo
Log.i(TAG, "count:$count -- visible count:${list.size},offset:$startOffset -- $endOffset")
val builder = StringBuilder()
for (item in list) {
builder.append(item.index).append(" -- ").append(item.key).append(" -- ")
.append(item.offset).append(" -- ").append(item.size).append("\n")
}
Log.i(TAG,"visible item:\n$builder")
}
复制代码
打印的信息如下:
2022-06-18 15:17:53.424 8178-8178/com.zyf.mycompose I/LazyListFragment: first item: 0,25
2022-06-18 15:17:53.424 8178-8178/com.zyf.mycompose I/LazyListFragment: count:34 -- visible count:7,offset:0 -- 580
2022-06-18 15:17:53.424 8178-8178/com.zyf.mycompose I/LazyListFragment: visible item:
0 -- first -- -25 -- 96
1 -- DefaultLazyKey(index=1) -- 71 -- 96
2 -- DefaultLazyKey(index=2) -- 167 -- 96
3 -- DefaultLazyKey(index=3) -- 263 -- 96
4 -- DefaultLazyKey(index=4) -- 359 -- 96
5 -- DefaultLazyKey(index=5) -- 455 -- 96
6 -- DefaultLazyKey(index=6) -- 551 -- 96
复制代码
可以看到:当前显示的第一个位置的数据就是列表第0项的数据,偏移量为25.
延迟列表总总共有34个数据,当前显示出来的是7个数据,延迟列表的最小偏移量为0,最大偏移量为580.
结合上一步的数据,我们发现,显示出来的那些数据中,第0个和第6个没有完全显示。要看某一个item是否完全显示,只需要使用当前item的偏移量和延迟列表的偏移量进行对比即可。比较的方法为:
- 当前item的偏移量大于延迟列表的最小偏移量
- 当前item的偏移量加上size(也就是offset + size)小于延迟列表的最大偏移量
同时满足上面两个条件就可以认为当前item完全显示了。
比如上面的延迟列表最大偏移量为580,最小偏移量为0,第一个item的偏移量为-25,不满足第一个条件,所以没有完全显示。最后一个item的offset + size (551 + 96 = 647)大于延迟列表的最大偏移量580,所以最后一个item也没完全显示。
另外需要注意的是:我们可以通过给延迟列表设置contentPadding
属性来设置延迟列表的内容边距。这里和直接通过modifier
设置编剧的不同之处在于:contentPadding
相当于给第一个项目添加了marginTop
,给最后一个项目设置了marginBottom
,给所有的项目设置了marginHorizontal
的效果。