如何实现ScrollView中任务卡片固定/取消固定时的位移动画
实现ScrollView中任务卡片跨分组的位移动画方案
问题场景
我有一个SwiftUI的ScrollView,顶部是固定(pinned)任务项,底部是非固定(unpinned)任务项,布局如下:
______________ | Item 1 | | Item 2 | | Item 3 | |--------------| | Item 4 | | Item 5 | | Item 6 | | Item 7 | | Item 8 | |______________|
通过两个ForEach分别渲染两个集合:
var pinnedItems = ["Item 1", "Item 2", "Item 3"] var unpinnedItems = ["Item 4", "Item 5", "Item 6", "Item 7", "Item 8"]
现有实现代码:
ScrollView { ForEach(pinnedTasks) { task in NavigationLink(value: NavigationPath.task(task)) { TaskCardView(task: task, onPinning: { withAnimation { togglePinnedTask(task: task) } }) .padding(.bottom) }.buttonStyle(.plain) } Divider() .frame(minHeight: 3) .background(.gray) .padding(.bottom) ForEach(unPinnedTasks) { task in NavigationLink(value: NavigationPath.task(task)) { TaskCardView(task: task, onPinning: { withAnimation { togglePinnedTask(task: task) } }) .padding(.bottom) }.buttonStyle(.plain) } }
togglePinnedTask()函数负责将任务从一个集合移到另一个集合,当前默认只有淡入淡出动画,想要实现任务卡片“飞”到新位置的位移动画。
实现方案
核心是利用SwiftUI的matchedGeometryEffect,它能让同一个视图在不同布局位置间产生平滑的位移动画,具体步骤如下:
1. 定义全局命名空间
在视图结构体中声明一个命名空间,用于标识需要匹配的几何效果:
@Namespace private var animationNamespace
2. 给TaskCardView添加matchedGeometryEffect
修改两个ForEach中的TaskCardView,为其添加matchedGeometryEffect,用任务的唯一标识作为id:
// 固定分组中的卡片 TaskCardView(task: task, onPinning: { ... }) .padding(.bottom) .matchedGeometryEffect(id: task.id, in: animationNamespace) // 非固定分组中的卡片 TaskCardView(task: task, onPinning: { ... }) .padding(.bottom) .matchedGeometryEffect(id: task.id, in: animationNamespace)
注意:确保每个任务遵循
Identifiable协议,拥有唯一的id,这样系统才能正确匹配同一个任务在不同分组中的视图。
3. 优化动画触发逻辑
保持withAnimation包裹togglePinnedTask的调用,可指定弹簧动画让移动效果更自然:
withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) { togglePinnedTask(task: task) }
4. 额外优化点
- 统一数据源管理:如果两个分组的数据源从同一个总列表拆分而来,确保数据更新的原子性,避免动画过程中出现数据不一致。
- 处理ScrollView滚动:若动画触发时ScrollView处于滚动状态,可临时禁用滚动或用
ScrollViewReader配合scrollTo方法,在动画结束后将新位置的卡片滚动到可视区域。 - Divider动画适配:当分组数量变化时,给Divider添加动画让整体过渡更流畅:
Divider() .frame(minHeight: 3) .background(.gray) .padding(.bottom) .animation(.default, value: pinnedTasks.count)
内容的提问来源于stack exchange,提问作者esbenr
相关产品推荐
相关产品推荐

