如何在可滚动Column中嵌套带动画的LazyColumn卡片?
我想实现一个简化版的Jetpack Compose UI,每个分组下的卡片列表长度会随传入的项列表变化。我分别用Column和LazyColumn实现了两种卡片组件:
@Composable fun CardedColumn(items: ImmutableList<String>) { Card(modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(8.dp)) { Column { items.forEach { text: String -> Text(text = text, modifier = Modifier.padding(8.dp)) Divider(modifier = Modifier.fillMaxWidth()) } } } } @OptIn(ExperimentalFoundationApi::class) @Composable fun CardedLazyColumn(items: ImmutableList<String>) { Card(modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(8.dp).animateContentSize()) { LazyColumn { items(items = items, key = { it }) { text: String -> Text(text = text, modifier = Modifier.padding(8.dp).animateItemPlacement()) Divider(modifier = Modifier.fillMaxWidth()) } } } } // 预览代码 @Preview(widthDp = 320, heightDp = 320) @Composable fun CardedLazyColumn_Preview() { Column { val showItems = remember { mutableStateOf(true) } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Text("Show items: ") Switch(checked = showItems.value, onCheckedChange = { showItems.value = it }) } CardedLazyColumn(getItems(showItems.value)) } } @Preview(widthDp = 320, heightDp = 320) @Composable fun CardedColumn_Preview() { Column { val showItems = remember { mutableStateOf(true) } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Text("Show items: ") Switch(checked = showItems.value, onCheckedChange = { showItems.value = it }) } CardedColumn(getItems(showItems.value)) } } // 辅助函数 private fun getItems(showMoreItems: Boolean): ImmutableList<String> { return if(showMoreItems) { persistentListOf("Lorem", "ipsum", "dolor", "sit", "amet") } else { persistentListOf("Lorem", "dolor", "sit", "amet") } }
预览后能看到CardedLazyColumn会为项的显示/隐藏添加动画,而CardedColumn没有——这是因为我结合使用了item key和animateItemPlacement,所以更倾向用LazyColumn。
接下来我要构建一个包含多个分组卡片的页面:
@Composable fun MyScreen() { Scaffold(topBar = { T23TopAppBar() }) { paddingValues: PaddingValues -> Column( modifier = Modifier .padding(paddingValues) .fillMaxSize() // 我希望内容超出屏幕时整个页面可滚动 // .verticalScroll(rememberScrollState()), ) { Text( text = "Lorem ipsum dolor sit amet", style = MaterialTheme.typography.h6, modifier = Modifier.padding(8.dp) ) val showItems = remember { mutableStateOf(true) } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Text("Show items: ") Switch(checked = showItems.value, onCheckedChange = { showItems.value = it }) } Text( text = "GROUP 1", style = MaterialTheme.typography.overline, modifier = Modifier.padding(horizontal = 8.dp) ) CardedLazyColumn(getItems(showItems.value)) Spacer(modifier = Modifier.height(8.dp)) Text( text = "GROUP 2", style = MaterialTheme.typography.overline, modifier = Modifier.padding(horizontal = 8.dp) ) CardedLazyColumn(persistentListOf("Lorem", "ipsum", "dolor", "sit", "amet")) Spacer(modifier = Modifier.height(8.dp)) Text( text = "GROUP 3", style = MaterialTheme.typography.overline, modifier = Modifier.padding(horizontal = 8.dp) ) CardedLazyColumn(persistentListOf("Lorem", "ipsum", "dolor", "sit", "amet")) } } }
但给外层Column加上.verticalScroll(rememberScrollState())后,Compose会抛出IllegalStateException:"Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed..."。我知道不推荐嵌套同方向滚动组件,但我并不想让LazyColumn可滚动,只是想用它的项动画能力——即使把LazyColumn的userScrollEnabled设为false也没用。
我的疑问:
- 有没有办法让带
LazyColumn的卡片组件在可滚动Column里正常工作? - 如果不行,有没有办法给
Column复刻这种项动画效果? - 直接用无动画的
CardedColumn是不是更合理?
我能想到的唯一办法是把外层可滚动Column换成LazyColumn,把每个卡片内的项扁平成外层LazyColumn的项,但这样太复杂,还要处理卡片圆角这类样式问题。
1. 让LazyColumn在可滚动Column中正常工作
核心问题是外层滚动Column会给子组件传递无限高度约束,导致LazyColumn无法正确测量自身高度。只需要修改CardedLazyColumn中的LazyColumn,让它根据内容自动计算高度,并禁用用户滚动:
@OptIn(ExperimentalFoundationApi::class) @Composable fun CardedLazyColumn(items: ImmutableList<String>) { Card(modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(8.dp).animateContentSize()) { LazyColumn( modifier = Modifier.fillMaxWidth().wrapContentHeight(), userScrollEnabled = false ) { items(items = items, key = { it }) { text: String -> Text(text = text, modifier = Modifier.padding(8.dp).animateItemPlacement()) Divider(modifier = Modifier.fillMaxWidth()) } } } }
这样修改后,LazyColumn会根据内容高度确定自身尺寸,不会触发无限高度异常,同时保留项的添加/移除动画,外层Column也能正常滚动。
2. 给Column复刻项动画效果
如果不想依赖LazyColumn,可以用Compose自带的动画API实现类似效果:
方案一:用AnimatedVisibility实现单个项动画
@OptIn(ExperimentalAnimationApi::class) @Composable fun AnimatedCardedColumn(items: ImmutableList<String>) { Card(modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(8.dp).animateContentSize()) { Column { items.forEachIndexed { index, text -> key(index) { AnimatedVisibility( visible = true, enter = slideInVertically(initialOffsetY = { it }), exit = slideOutVertically(targetOffsetY = { -it }) ) { Column { Text(text = text, modifier = Modifier.padding(8.dp)) Divider(modifier = Modifier.fillMaxWidth()) } } } } } } }
通过给每个项绑定唯一key,确保项变化时动画能正确触发。
方案二:用AnimatedContent实现列表整体过渡动画
@OptIn(ExperimentalAnimationApi::class) @Composable fun AnimatedCardedColumn(items: ImmutableList<String>) { val transition = updateTransition(targetState = items, label = "items-transition") Card(modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(8.dp).animateContentSize()) { Column { transition.AnimatedContent( transitionSpec = { slideInVertically(initialOffsetY = { it }) with slideOutVertically(targetOffsetY = { -it }) } ) { currentItems -> Column { currentItems.forEach { text -> Text(text = text, modifier = Modifier.padding(8.dp)) Divider(modifier = Modifier.fillMaxWidth()) } } } } } }
这种方式会在列表项变化时,让新旧列表以滑动方式过渡,效果接近LazyColumn的animateItemPlacement。
3. 是否应该直接用无动画的CardedColumn?
取决于你的需求优先级:
- 如果动画不是核心交互需求,直接用
CardedColumn更简洁,没有嵌套组件的潜在问题,代码维护成本更低。 - 如果动画能提升用户体验(比如让用户清晰感知项的添加/移除),那值得花时间实现动画——不管是修改
LazyColumn的方案,还是给Column加动画的方案,在项数量不多的情况下,性能差异可以忽略。
内容的提问来源于stack exchange,提问作者JHowzer

