You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Jetpack Compose的LazyColumn实现类Apple Wallet拖拽排序动画

实现Jetpack Compose LazyColumn中类似Apple Wallet的卡片拖拽排序动画

问题分析

要实现的核心动画逻辑是:拖拽卡片排序时,被穿过的其他卡片先平滑下移,待拖拽卡片通过后再上移归位。现有方案存在两个关键问题:

  1. 原生Modifier.animateItem()仅支持单一布局过渡,无法实现"先下移再上移"的两段式动画
  2. 用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 15:05:19