如何在Jetpack Compose的LazyColumn实现类Apple Wallet拖拽排序动画
实现Jetpack Compose LazyColumn中类似Apple Wallet的卡片拖拽排序动画
问题分析
要实现的核心动画逻辑是:拖拽卡片排序时,被穿过的其他卡片先平滑下移,待拖拽卡片通过后再上移归位。现有方案存在两个关键问题:
- 原生
Modifier.animateItem()仅支持单一布局过渡,无法实现"先下移再上移"的两段式动画 - 用
AnimatedVisibility替代时,快速拖拽会因协程延迟导致动画失效,还会出现卡片裁剪问题
解决方案
核心思路是基于Reorderable库的状态,用状态驱动的位移动画替代布局过渡/进出动画,避免协程依赖,同时保证动画实时响应拖拽操作。
1. 自定义ReorderableItem的动画Modifier
替换默认的animateItem(),通过监听拖拽状态计算每个item的偏移量,用animateAsState实现平滑的两段式位移:
@Composable fun LazyItemScope.ReorderableItem( key: Any, state: ReorderableLazyListState, content: @Composable LazyItemScope.() -> Unit ) { val itemState = rememberReorderableItemState(key, state) // 根据拖拽状态计算目标偏移量 val targetOffsetY = when { itemState.isDragging -> 0f state.isDragging -> { val currentIndex = state.currentList.indexOfFirst { it == key } val dragIndex = state.draggedItemIndex // 判断当前item是否在拖拽路径中,计算偏移方向 if ((dragIndex < currentIndex && state.dragDirection == DragDirection.Down) || (dragIndex > currentIndex && state.dragDirection == DragDirection.Up) ) { // 拖拽卡片靠近时偏移,通过后复位 if (state.isDraggingCompleted) 0f else 60f * if (state.dragDirection == DragDirection.Down) 1 else -1 } else { 0f } } else -> 0f } val offsetY by animateAsState( targetValue = targetOffsetY, animationSpec = tween( durationMillis = 300, easing = CubicBezierEasing(0.2f, 0.8f, 0.2f, 1f) ), label = "item offset animation" ) Box( modifier = Modifier .fillMaxWidth() .offset(y = offsetY) .reorderableItem(itemState) ) { content() } }
2. 优化拖拽状态配置
调整ReorderableLazyListState的拖拽结束动画,确保整体动画连贯:
val items = remember { mutableStateListOf("卡片1", "卡片2", "卡片3", "卡片4", "卡片5") } val state = rememberReorderableLazyListState( onMove = { fromIndex, toIndex -> // 处理数据排序 val item = items.removeAt(fromIndex) items.add(toIndex, item) }, // 拖拽结束时的复位动画 dragEndAnimationSpec = tween(durationMillis = 200) ) LazyColumn( state = state.listState, modifier = Modifier.reorderable(state) ) { items(items, key = { it }) { item -> ReorderableItem(key = item, state = state) { Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 8.dp) .draggableHandle(), elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { Text( text = item, modifier = Modifier.padding(24.dp), fontSize = 18.sp ) } } } }
关键改进点
- 状态驱动动画:用
animateAsState替代协程延迟,动画实时响应拖拽状态变化,解决快速拖拽时动画失效问题 - 位移动画替代进出动画:用
Modifier.offset实现卡片的平移,避免AnimatedVisibility带来的布局重排和裁剪问题 - 两段式动画逻辑:通过判断拖拽方向和item位置,实现"拖拽靠近时偏移,通过后复位"的效果,贴近Apple Wallet的动画表现
内容的提问来源于stack exchange,提问作者user924
相关产品推荐
相关产品推荐

