如何为带水平滚动的Compose Row实现可见性动画?
Jetpack Compose 实现水平列表的顺序折叠/展开动画
问题描述
我有一个带.horizontalScroll(scrollState)的Row,需要实现列表项的显示与隐藏动画:
- 隐藏时:所有元素依次折叠到第一个元素的位置,最后整体平滑消失
- 显示时:从第一个元素开始,依次展开为完整的水平列表
我尝试用AnimatedVisibility实现,但效果不符合预期,我的代码如下:
val delay = index * 100 AnimatedVisibility( visible = visible, enter = slideInHorizontally( initialOffsetX = { fullWidth -> -fullWidth / 2 } ) + fadeIn( animationSpec = tween(durationMillis = 300, delayMillis = delay) ) + scaleIn( initialScale = 0.8f, animationSpec = tween(durationMillis = 300, delayMillis = delay) ), exit = fadeOut( animationSpec = tween(durationMillis = 200) ) + shrinkHorizontally( targetWidth = { it / 2 } ) ) { // ... 列表项内容 }
期望实现的效果类似以下CSS动画:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sequential Collapse and Expand</title> <style> body { display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f0f0f0; } .list-container { display: flex; flex-direction: row; position: relative; gap: 10px; } .list-item { background-color: #3498db; color: white; font-size: 14px; font-family: Arial, sans-serif; text-align: center; line-height: 40px; width: 100px; height: 40px; border-radius: 5px; transform-origin: left center; transition: transform 0.5s ease-in-out, opacity 0.5s ease-in-out; } /* 默认状态(展开) */ .list-container:not(:hover) .list-item { transform: translateX(0) scaleX(1); opacity: 1; } /* hover状态(折叠) */ .list-container:hover .list-item { opacity: 0; } /* 顺序折叠动画 */ .list-container:hover .list-item:nth-child(5) { transform: translateX(-400px) scaleX(0); transition-delay: 0s; } .list-container:hover .list-item:nth-child(4) { transform: translateX(-300px) scaleX(0); transition-delay: 0.1s; } .list-container:hover .list-item:nth-child(3) { transform: translateX(-200px) scaleX(0); transition-delay: 0.2s; } .list-container:hover .list-item:nth-child(2) { transform: translateX(-100px) scaleX(0); transition-delay: 0.3s; } .list-container:hover .list-item:nth-child(1) { transform: translateX(0) scaleX(1); opacity: 0; transition-delay: 0.4s; } /* 顺序展开动画 */ .list-container:not(:hover) .list-item:nth-child(1) { transform: translateX(0) scaleX(1); transition-delay: 0.4s; } .list-container:not(:hover) .list-item:nth-child(2) { transform: translateX(0) scaleX(1); transition-delay: 0.3s; } .list-container:not(:hover) .list-item:nth-child(3) { transform: translateX(0) scaleX(1); transition-delay: 0.2s; } .list-container:not(:hover) .list-item:nth-child(4) { transform: translateX(0) scaleX(1); transition-delay: 0.1s; } .list-container:not(:hover) .list-item:nth-child(5) { transform: translateX(0) scaleX(1); transition-delay: 0s; } </style> </head> <body> <div class="list-container"> <div class="list-item">Item 1</div> <div class="list-item">Item 2</div> <div class="list-item">Item 3</div> <div class="list-item">Item 4</div> <div class="list-item">Item 5</div> </div> </body> </html>
解决方案
要实现这种顺序折叠/展开的效果,不能单纯依赖AnimatedVisibility,需要结合Animatable控制每个列表项的缩放、位移和透明度,同时处理反向延迟逻辑(折叠从最后一个元素开始,展开从第一个元素开始)。
完整实现代码
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp @Composable fun SequentialCollapseList() { val scrollState = rememberScrollState() var isExpanded by remember { mutableStateOf(true) } val itemWidth = 100.dp val itemGap = 10.dp val items = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5") val animationDuration = 500 val delayStep = 100 // 为每个元素创建独立的动画状态 val animationStates = remember(items.size) { List(items.size) { index -> Animatable(if (isExpanded) 1f else 0f).apply { if (!isExpanded) snapTo(0f) } } } // 状态变化时启动对应动画 LaunchedEffect(isExpanded) { items.forEachIndexed { index, _ -> // 折叠时反向延迟,展开时正向延迟 val delay = if (isExpanded) { index * delayStep } else { (items.size - 1 - index) * delayStep } animationStates[index].animateTo( targetValue = if (isExpanded) 1f else 0f, animationSpec = tween( durationMillis = animationDuration, delayMillis = delay ) ) } } Box( modifier = Modifier .fillMaxSize() .pointerInput(Unit) { detectTapGestures(onTap = { isExpanded = !isExpanded }) }, contentAlignment = Alignment.Center ) { Row( modifier = Modifier.horizontalScroll(scrollState), horizontalArrangement = Arrangement.spacedBy(itemGap) ) { items.forEachIndexed { index, text -> val progress = animationStates[index].value // 计算当前元素需要移动的总距离 val totalOffset = (items.size - 1 - index) * (itemWidth + itemGap).value val offsetX = (1 - progress) * totalOffset * (-1) // 第一个元素折叠时保持缩放为1,最后消失 val scaleX = if (index == 0 && !isExpanded) 1f else progress Box( modifier = Modifier .width(itemWidth) .height(40.dp) .background(Color(0xFF3498DB)) .offset(x = offsetX.dp) .scale(scaleX = scaleX, scaleY = 1f), contentAlignment = Alignment.Center ) { Text( text = text, color = Color.White, modifier = Modifier.alpha(progress) ) } } } } }
关键实现细节
- 反向延迟逻辑:
- 折叠时:最后一个元素先执行动画,延迟依次递减
- 展开时:第一个元素先执行动画,延迟依次递增
- 精准位移计算:每个元素的位移距离等于它到第一个元素的总宽度(包含自身宽度和间隙),确保折叠时完全贴合第一个元素
- 第一个元素特殊处理:折叠状态下保持缩放为1,最后再通过透明度消失,和CSS示例效果一致
- 独立动画控制:用
Animatable为每个元素单独管理动画进度,实现更细腻的顺序动画效果
内容的提问来源于stack exchange,提问作者testivanivan
相关产品推荐
相关产品推荐

