今天我们会对Compose的列表的实现方式进行讲解,并对LazyColumn,LazyRow,LazyVerticalGrid这三种控件的使用以及他们的每个属性进行介绍讲解。
一:Column实现竖直列表滚动
Column我们前面的文章介绍过Column , Row ,Box 的用法,他是竖直布局,当我们的列表项不是那么多的实现,我们可以通过Column去实现列表的滚动,例子如下:
// 首先我们定义一个Student类
class Student(var name:String=""){}
// 其次我们往高度为100dp的Column里面放置这20个文本控件。每个文本控件的高度是30dp,文本控件显示的是student的name属性
@Preview
@Composable
fun columnScrollTest(){
val list = ArrayList<Student>()
for(i in 0..20){
list.add(Student(i.toString()))
}
studentList(students = list)
}
@Composable
fun studentList(students:List<Student>){
Column(modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.verticalScroll(rememberScrollState())
) {
students.forEach {
studentItem(student = it)
}
}
}
@Composable
fun studentItem(student:Student){
Text(text = student.name,modifier = Modifier.fillMaxWidth().height(30.dp),fontSize = 14.sp,textAlign = TextAlign.Center)
}
二:Row实现横向列表滚动
跟Column类似,我们可以通过Modifier.horizontalScroll设置横向的滚动
// 首先我们定义一个Student类
class Student(var name:String=""){}
@Composable
fun studentRowItem(student:Student){
Text(text = student.name,modifier = Modifier
.fillMaxHeight()
.width(60.dp),fontSize = 14.sp,textAlign = TextAlign.Center)
}
@Preview
@Composable
fun rowScrollTest(){
val list = ArrayList<Student>()
for(i in 0..20){
list.add(Student(i.toString()))
}
Row(modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.horizontalScroll(rememberScrollState())
) {
list.forEach {
studentRowItem(student = it)
}
}
}
三:LazyColumn实现竖直列表
Column是会一次性的对所有的item项进行布局,所以当我们的列表很长的时候,Column 等布局可能会导致性能问题。这时候我们可以使用LazyColumn控件,竖直列表。这个类似RecyclerView的竖直排列方式。首先我们来看看 LazyColumn的代码:
@Composable
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
){
...
}
- content 列表里显示的控件。这里的content是LazyListScope类型,我们来看看LazyListScope的代码
@LazyScopeMarker interface LazyListScope { fun item(key: Any? = null, content: @Composable LazyItemScope.() -> Unit) fun items( count: Int, key: ((index: Int) -> Any)? = null, itemContent: @Composable LazyItemScope.(index: Int) -> Unit ) @ExperimentalFoundationApi fun stickyHeader(key: Any? = null, content: @Composable LazyItemScope.() -> Unit) } inline fun <T> LazyListScope.items(items: List<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: List<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]) }
- item是添加一个item项
- items是一次性添加多少条item项
- stickerHeader 是粘性的头部(比如我们联系人列表经常性的把字幕作为粘性的置顶在头部) 同时LazyListScope还有两个扩展函数一个是items,一个是itemsIndexed
- items是通过传入的集合数量创建item
- itemsIndexed 也是通过传入的集合数量去创建item,只不过多了一个index参数 举个例子
stickerHeader我们单独举个联系人的例子如下:@Preview @Composable fun lazyColumnTest(){ val list = ArrayList<Student>() for(i in 0..4){ list.add(Student(i.toString())) } LazyColumn() { item{ Text(text = "标题") } items(2) { Text(text = "两条副标题") } items(list){ student-> Text(text = student.name) } itemsIndexed(list){ index: Int, item: Student -> Row() { Text(text = item.name) Text(text = "索引位置$index") } } } }
注意使用stickerHeader,需要添加@ExperimentalFoundationApi该注解@ExperimentalFoundationApi @Preview @Composable fun lazyColumnTest2(){ val map = HashMap<String,List<Student>>() map.put("A",ArrayList<Student>().apply { add(Student("我是A1")) add(Student("我是A2")) add(Student("我是A3")) }) map.put("B",ArrayList<Student>().apply { add(Student("我是B1")) add(Student("我是B2")) add(Student("我是B3")) }) map.put("C",ArrayList<Student>().apply { add(Student("我是C1")) add(Student("我是C2")) add(Student("我是C3")) }) map.put("D",ArrayList<Student>().apply { add(Student("我是D1")) add(Student("我是D2")) add(Student("我是D3")) }) map.put("E",ArrayList<Student>().apply { add(Student("我是E1")) add(Student("我是E2")) add(Student("我是E3")) }) map.put("F",ArrayList<Student>().apply { add(Student("我是F1")) add(Student("我是F2")) add(Student("我是F3")) }) LazyColumn() { map.forEach { (firstChar, students) -> stickyHeader { CharacterHeader(firstChar) } items(students) { student -> ContactListItem(student) } } } } @Composable fun CharacterHeader(firstChar:String){ Column(verticalArrangement = Arrangement.Center,modifier = Modifier.fillMaxWidth().height(30.dp).background(Color.Red).padding(start = 10.dp)) { Text(text = firstChar,fontSize = 10.sp,color = Color.White) } } @Composable fun ContactListItem(student:Student){ Column(verticalArrangement = Arrangement.Center,modifier = Modifier.fillMaxWidth().height(50.dp).background(Color.White).padding(start = 10.dp)) { Text(text = student.name,fontSize = 14.sp,color = Color.Black) } }
- modifier 修饰符,我们以前的文章讲过Modifier用法详解
- contentPadding 内容内边距 通过PaddingValues赋值。相当于以前给RecyclerView设置padding属性
举例,比如lazycolumn的背景是红色,而内容的背景是绿色,我们就可以一目了然看出contentPadding的效果
@Preview @Composable fun lazyColumnTest3(){ LazyColumn(contentPadding = PaddingValues(10.dp),modifier = Modifier.background(Color.Red)) { items(2){ Box(modifier = Modifier.background(Color.Green).fillMaxWidth().height(30.dp)) } } }
- reverseLayout 是否反着排版
@Preview @Composable fun lazyColumnTest3(){ LazyColumn(reverseLayout = true) { item{ Text(text = "我是第一个") } item{ Text(text = "我是第二个") } } }
- verticalArrangement 是竖直方向上对子View的排版,可以去设置每个子View竖直方向上的间距。我们在介绍Column , Row ,Box 的用法的文章里面就有讲到。默认是Arrangement.Top表示竖直方式上尽可能的靠近主轴的顶部。有如下几种取值
- Arrangement.Top 垂直放置子对象,使其尽可能靠近主轴顶部。
- Arrangement.BOTTOM 垂直放置子对象,使其尽可能靠近主轴底部。
- Arrangement.CENTER 垂直子对象,使其尽可能靠近主轴的中间。
- Arrangement.SpaceBetween 垂直放置子对象时,使它们沿主轴均匀分布,在第一个子对象之前或最后一个子对象之后没有可用空间。意思是第一个在最顶部,最后一个在最底部。而中间的按同等间隔去均分放置。
- Arrangement.SpaceEvenly 垂直放置子对象,使他们同等间隔均分放置
- Arrangement.SpaceAround 垂直放置子对象,第一个放置在距离顶部x间隔的地方,最后一个放置在距离底部x间隔的地方。中间的按同等间距均分放置。
几个属性如下图所示:引用康康的图
我们这里也可以通过Arrangement.spacedBy()方法来设置,每个item之间的间距。比如上面的contentPadding的例子,我们给设置item的间距是4dp。代码如下:
@Preview @Composable fun lazyColumnTest3(){ LazyColumn(contentPadding = PaddingValues(10.dp),verticalArrangement = Arrangement.spacedBy(4.dp),modifier = Modifier.background(Color.Red)) { items(2){ Box(modifier = Modifier.background(Color.Green).fillMaxWidth().height(30.dp)) } } }
- horizontalAlignment 水平方向上的对齐方式。默认是Alignment.Start。比如当LazyColumn的宽度大于item项的宽度的时候,这时候设置horizontalAlignment就能看出效果。代码如下:
@Preview @Composable fun lazyColumnTest4(){ LazyColumn(contentPadding = PaddingValues(10.dp),modifier = Modifier.fillMaxWidth().background(Color.Red) ,verticalArrangement = Arrangement.spacedBy(4.dp), horizontalAlignment = Alignment.End) { items(2){ Box(modifier = Modifier.background(Color.Green).width(60.dp).height(30.dp)) } } }
- state 一种状态对象,可以被用来控制和观察滚动。大多数情况下,通过rememberLazyListState创建。来看看LazyListState的代码
@Stable class LazyListState constructor( firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOffset: Int = 0 ){...}
- firstVisibleItemIndex第一个显示的位置
- firstVisibleItemScrollOffset 第一个可见项的滚动偏移量。向前滚动是正数-即项目向后偏移的量 举例,比如上面的联系人列表的例子,我们在滚动到第二条数据的时候,显示回到顶部的按钮,点击回到顶部按钮回 到顶部。
这里讲解几点@ExperimentalAnimationApi @ExperimentalFoundationApi @Preview @Composable fun lazyColumnTest2(){ val map = HashMap<String,List<Student>>() map.put("A",ArrayList<Student>().apply { add(Student("我是A1")) add(Student("我是A2")) add(Student("我是A3")) }) map.put("B",ArrayList<Student>().apply { add(Student("我是B1")) add(Student("我是B2")) add(Student("我是B3")) }) map.put("C",ArrayList<Student>().apply { add(Student("我是C1")) add(Student("我是C2")) add(Student("我是C3")) }) map.put("D",ArrayList<Student>().apply { add(Student("我是D1")) add(Student("我是D2")) add(Student("我是D3")) }) map.put("E",ArrayList<Student>().apply { add(Student("我是E1")) add(Student("我是E2")) add(Student("我是E3")) }) map.put("F",ArrayList<Student>().apply { add(Student("我是F1")) add(Student("我是F2")) add(Student("我是F3")) }) val scope = rememberCoroutineScope() Box(modifier = Modifier.fillMaxSize(),contentAlignment=Alignment.BottomEnd) { val listState = rememberLazyListState() LazyColumn(state = listState) { map.forEach { (firstChar, students) -> stickyHeader { CharacterHeader(firstChar) } items(students) { student -> ContactListItem(student) } } } val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex>0 } } AnimatedVisibility(visible = showButton) { Column( modifier = Modifier.padding(bottom = 10.dp,end = 10.dp)) { FloatingActionButton( onClick = { scope.launch { listState.scrollToItem(0) // 或者 // listState.animateScrollToItem(0) } } ) { Text(text = "回到顶部",color = Color.White,fontSize = 12.sp) } } } } }
- AnimatedVisibility 这个可以控制控件的显示隐藏, 使用该控件需要添加 @ExperimentalAnimationApi注解。后面我们讲动画的时候会细讲该控件
- listState.scrollToItem或者 listState.animateScrollToItem(0)表示滚动到某个位置,它们是挂起函数,需要在协程里调用。Compose里获取协程的方法是rememberCoroutineScope()。\
- derivedStateOf 如果某个状态是从其他状态对象计算或派生得出的,请使用 derivedStateOf。使用此函数可确保仅当计算中使用的状态之一发生变化时才会进行计算。从而避免在每次重组时执行。
- flingBehavior 默认是ScrollableDefaults.flingBehavior()
四:LazyRow实现横向列表
同样的,在数据比较多的情况下,Row有性能的问题。LazyRow就类似于RecyclerView的横向布局,我们里看看LazyRow的代码:
@Composable
fun LazyRow(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
horizontalArrangement: Arrangement.Horizontal =
if (!reverseLayout) Arrangement.Start else Arrangement.End,
verticalAlignment: Alignment.Vertical = Alignment.Top,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
content: LazyListScope.() -> Unit
)
- modifier 修饰符
- state 跟LazyColumn的一致,这里不在描述
- contentPadding 内容边距,跟LazyColumn一致。类似设置RecyclerView的padding
- reverseLayout 跟LazyColumn的一致,数据是否反着排版
- horizontalArrangement 水平方向上对子View的排版。可以设置每个子View水平方向上的间距。
默认是水平放置子view,居左开始布局Arrangement.Start。主要有如下几种取值
- Arrangement.START 水平放置子对象,使其尽可能靠近主轴左边。
- Arrangement.END 水平放置子对象,使其尽可能靠近主轴右边。
- Arrangement.CENTER 水平放置子对象,使其尽可能靠近主轴的中间。
- Arrangement.SpaceBetween 水平放置子对象时,使它们沿主轴均匀分布,在第一个子对象之前或最后一个子对象之后没有可用空间。意思是第一个在最左边,最后一个在最右部。而中间的按同等间隔去均分放置。
- Arrangement.SpaceEvenly 水平放置子对象,使他们同等间隔均分放置
- Arrangement.SpaceAround 水平放置子对象,第一个放置在距离顶部x间隔的地方,最后一个放置在距离底部x间隔的地方。中间的按同等间距均分放置。
- 可以通过Arrangement.spacedBy()去设置item之间的间距
- verticalAlignment 竖直方向上的对齐方式
- content 跟LazyColumn的一致 内容控件
- flingBehavior 默认是ScrollableDefaults.flingBehavior() 举例如下:
@Preview()
@Composable
@ExperimentalAnimationApi
fun lazyRowTest(){
val list = ArrayList<Student>().apply {
add(Student("第一个学生"))
add(Student("第二个学生"))
add(Student("第三个学生"))
}
val scope = rememberCoroutineScope()
Box(modifier = Modifier
.fillMaxWidth()
.height(200.dp),contentAlignment = Alignment.CenterEnd) {
val listState = rememberLazyListState()
LazyRow(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.background(Color.Red),
contentPadding = PaddingValues(10.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
reverseLayout = false,
state = listState
) {
item {
studentRowItem(Student("开头的学生"))
}
items(2){
studentRowItem(Student("中间的学生"))
}
items(list){
item: Student ->
studentRowItem(item)
}
itemsIndexed(list){
index, item: Student ->
item.name+=index
studentRowItem(item)
}
item {
studentRowItem(Student("结尾的学生"))
}
}
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex>0
}
}
AnimatedVisibility(visible = showButton) {
Column( modifier = Modifier.padding(end = 10.dp)) {
FloatingActionButton(
modifier = Modifier.size(40.dp),
onClick = {
scope.launch {
listState.animateScrollToItem(0)
}
}
) {
Text(text = "置顶",color = Color.White,fontSize = 12.sp)
// Icon(imageVector = Icons.Filled.ArrowDropDown, contentDescription ="icon图标")
}
}
}
}
}
五:LazyVerticalGrid实现网格列表
LazyVerticalGrid是用于实现网格列表的控件。代码如下:
@ExperimentalFoundationApi
@Composable
fun LazyVerticalGrid(
cells: GridCells,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
content: LazyGridScope.() -> Unit
) {
...
}
- cells 参数控制如何将单元格构建为列。cells是一个GridCells。我们来看看GridCells的代码
我们可以看到主要通过两个方法获取GridCells@ExperimentalFoundationApi sealed class GridCells { @ExperimentalFoundationApi class Fixed(val count: Int) : GridCells() @ExperimentalFoundationApi class Adaptive(val minSize: Dp) : GridCells() }
- GridCells.Fixed(count) 参数count是表示几列
- GridCells.Adaptive(minSiez) 表示最小的宽度是多少 注意使用GridCells需要添加注解 @ExperimentalFoundationApi
- modifier 修饰符
- state 跟LazyColumn一直
- contentPadding 内容边距 跟LazyColumn一致
- content 内容控件 这里的内容控件是一个LazyGridScope。我们来看看LazyGridScope的代码
@ExperimentalFoundationApi interface LazyGridScope { fun item(content: @Composable LazyItemScope.() -> Unit) fun items(count: Int, itemContent: @Composable LazyItemScope.(index: Int) -> Unit) } @ExperimentalFoundationApi inline fun <T> LazyGridScope.items( items: List<T>, crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit ) = items(items.size) { itemContent(items[it]) } @ExperimentalFoundationApi inline fun <T> LazyGridScope.itemsIndexed( items: List<T>, crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit ) = items(items.size) { itemContent(it, items[it]) }
- item 添加单个item
- items(count) 添加count个item
- items(list) 添加list.size个item
- itemsIndex 添加list.size个item,回调里比items(list) 多了个index参数 举例,我们显示一个3列的图片列表,也是一样有个回到顶部按钮。
@ExperimentalAnimationApi
@ExperimentalFoundationApi
@Preview()
@Composable
fun lazyVerticalGridTest2(){
val scope = rememberCoroutineScope()
var images = mutableListOf<String>().apply {
add("https://picsum.photos/300/300")
add("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=670316187,1943310392&fm=26&gp=0.jpg")
add("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3285777805,2966380382&fm=26&gp=0.jpg")
add("https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=3002379740,3965499425&fm=26&gp=0.jpg")
add("https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=3754055165,2750208206&fm=26&gp=0.jpg")
add("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3003090038,2159771512&fm=26&gp=0.jpg")
add("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1200627027,2802353342&fm=11&gp=0.jpg")
add("https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=1181608161,298129206&fm=15&gp=0.jpg")
add("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2454668203,2080813436&fm=11&gp=0.jpg")
add("https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=879236219,1029264205&fm=15&gp=0.jpg")
add("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=262059124,473213094&fm=26&gp=0.jpg")
add("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=1221488731,1161675439&fm=26&gp=0.jpg")
add("https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=3451177997,3474973814&fm=26&gp=0.jpg")
add("https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=518137785,1411448949&fm=15&gp=0.jpg")
add("https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=1157318634,2153192559&fm=15&gp=0.jpg")
add("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=1676386382,595885724&fm=26&gp=0.jpg")
add("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=591224018,3084564296&fm=26&gp=0.jpg")
add("https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=976756369,247819196&fm=15&gp=0.jpg")
add("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1624886268,208163879&fm=15&gp=0.jpg")
add("https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4113813764,8099815&fm=26&gp=0.jpg")
add("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2036388755,3109868010&fm=15&gp=0.jpg")
}
Box(modifier = Modifier.fillMaxSize(),contentAlignment=Alignment.BottomEnd) {
val listState = rememberLazyListState()
LazyVerticalGrid(
cells = GridCells.Fixed(3),
modifier = Modifier.fillMaxSize(),
state = listState,
contentPadding = PaddingValues(4.dp)) {
items(images){
src->
CoilImage(
data = src,
modifier = Modifier.fillMaxWidth().height(120.dp),
contentScale = ContentScale.Crop,
contentDescription = "My content description",
fadeIn = true,
requestBuilder = {
transformations(RoundedCornersTransformation(4f))
}
)
}
}
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex>0
}
}
AnimatedVisibility(visible = showButton) {
Column( modifier = Modifier.padding(bottom = 10.dp,end = 10.dp)) {
FloatingActionButton(
onClick = {
scope.launch {
listState.animateScrollToItem(0)
}
}
) {
Text(text = "回到顶部",color = Color.White,fontSize = 12.sp)
}
}
}
}
}
六 对于列表我们讲一个项键的概念
默认情况下,每个列表项的状态均与该项在列表中的位置相对应。但是,如果数据集发生变化,这可能会导致问题,因为位置发生变化的列表项实际上会丢失任何记忆状态。想象一下 LazyColumn 中的 LazyRow 场景,如果某个行更改了项位置,用户将丢失在该行内的滚动位置。
为避免出现此情况,您可以为每个列表项提供一个稳定的唯一键,为 key 参数提供一个块。提供稳定的键可使项状态在发生数据集更改后保持一致:
@Composable
fun MessageList(students: List<Student>) {
LazyColumn {
items(
items = students,
key = { student ->
// Return a stable + unique key for the item
student.id
}
) { student ->
studentItem(student)
}
}
}