发布于2022年11月4日3年前 原文:Jetpack Compose学习(9)——Compose中的列表控件(LazyRow和LazyColumn) - Stars-One的杂货小窝 经过前面的学习,大致上已掌握了compose的基本使用了,本篇继续进行扩展,讲解下载Compose中的列表控件LazyRow和LazyColumn 之前也是讲解Jetpack Compose学习(6)——关于Modifier的妙用 | Stars-One的杂货小窝,可以通过Modifier属性将Row和Column组件改造为可滑动的 但是如果你需要显示大量的项目(或一个未知长度的列表),使用像 Column 这样的布局会导致性能问题,因为所有的项目都会被组合和布局,无论它们是否可见。 本系列以往文章请查看此分类链接Jetpack compose学习 基本使用 这里由于LazyRow和LazyColumn用法相似,只是展示的方向不同,所以便是不各自分个章节出来了,下文以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 - LazyRow、LazyColumn_乐翁龙的博客-CSDN博客 列表和网格 | Jetpack Compose | Android Developers 概述 | 你好 Compose SnapshotStateList | Android Developers 提问之前,请先看提问须知 点击右侧图标发起提问 http://wpa.qq.com/pa?p=2:1053894518:52或者加入QQ群一起学习 TornadoFx学习交流群:1071184701
创建帐户或登录后发表意见