SwiftUI 高级动画:Animation Phases 与 KeyframeAnimator
Wind your way through advanced animations in SwiftUI
2023年6月5日
一句话判断
SwiftUI 新增了 PhaseAnimator 和 KeyframeAnimator 两套动画 API——前者适合”循环播放”和”事件触发”的多步动画,后者适合精确控制每个关键帧的时间和曲线。如果你的动画需求超过了 withAnimation 的能力范围,看这场就对了。
这场 Session 讲了什么
Session 介绍了 SwiftUI 中两个新的动画构建工具,专门解决”不只是从 A 状态过渡到 B 状态”的复杂动画需求。
Animation Phases(PhaseAnimator)。传统的 withAnimation 只处理”旧状态到新状态”的过渡。PhaseAnimator 允许你定义多个阶段(phases),SwiftUI 自动在阶段之间循环切换。每个阶段有自己的持续时间和动画曲线。两种典型场景:循环动画(如加载指示器持续旋转)和事件驱动动画(如按钮被点击后播放一个脉冲效果)。
KeyframeAnimator。关键帧动画允许你为每个动画属性定义独立的轨迹(track),每个轨迹有自己的一组关键帧,每个关键帧有独立的时间和动画曲线。不同轨迹的关键帧可以交错——比如缩放和旋转在时间上不完全对齐,产生更丰富的视觉效果。
与传统动画的关系。PhaseAnimator 和 KeyframeAnimator 不替代 withAnimation,而是补充它。简单的状态过渡继续用 withAnimation,需要多步骤或精细控制的动画用新 API。
值得深挖的点
PhaseAnimator 的触发模式。循环模式下,动画完成后自动从最后一个阶段跳回第一个阶段继续播放。事件触发模式下,动画完成后停在最后一个阶段,等待下一次触发。两种模式的选择取决于你的使用场景——持续性效果用循环,一次性反馈用触发。
关键帧的时间模型:关键帧定义的不是”从何时开始”,而是”何时到达目标值”。两个关键帧之间的动画由曲线控制。这意味着你可以在一条轨迹上定义密集的关键帧来创造加速-减速效果,而另一条轨迹只定义少量关键帧保持匀速。
多轨迹的独立性:每条轨迹有自己的关键帧集和时间线。你可以让缩放在 0.3 秒内完成,旋转在 0.5 秒内完成,两条轨迹并行运行互不干扰。这比嵌套多个 withAnimation 调用要优雅得多。
代码片段
使用 PhaseAnimator 创建循环动画:
// 循环脉冲动画
PhaseAnimator([false, true]) { phase in
Circle()
.fill(phase ? Color.red : Color.blue)
.scaleEffect(phase ? 1.2 : 0.8)
} animation: { phase in
// 每个阶段的动画配置
switch phase {
case false: .easeInOut(duration: 0.6)
case true: .spring(response: 0.3, dampingFraction: 0.5)
}
}
使用 KeyframeAnimator 创建多轨迹动画:
KeyframeAnimator(
initialValue: AnimationValues(),
repeating: true
) { values in
CatView()
.scaleEffect(values.scale)
.rotationEffect(values.rotation)
.offset(y: values.verticalOffset)
} keyframes: { _ in
// 缩放轨迹
KeyframeTrack(\.scale) {
CubicKeyframe(1.0, duration: 0.3) // 正常大小
CubicKeyframe(1.3, duration: 0.2) // 放大
CubicKeyframe(1.0, duration: 0.3) // 恢复
}
// 旋转轨迹(时间线独立于缩放)
KeyframeTrack(\.rotation) {
CubicKeyframe(.zero, duration: 0.4)
CubicKeyframe(.degrees(15), duration: 0.15)
CubicKeyframe(.degrees(-15), duration: 0.15)
CubicKeyframe(.zero, duration: 0.2)
}
// 垂直偏移轨迹
KeyframeTrack(\.verticalOffset) {
CubicKeyframe(0, duration: 0.2)
CubicKeyframe(-20, duration: 0.3) // 跳起
CubicKeyframe(0, duration: 0.3) // 落下
}
}
最佳实践
- 简单的状态过渡继续用
withAnimation,不要为了用新 API 而过度工程化 - 循环动画用 PhaseAnimator 的循环模式,一次性效果用触发模式
- 关键帧动画中,为每个视觉属性(缩放、旋转、偏移)定义独立轨迹
- 不同轨迹使用不同的持续时间和曲线,创造错落有致的效果
- 在真机上测试动画性能——复杂的关键帧动画可能影响帧率
还有什么值得关注
- PhaseAnimator 与
onAppear配合使用的触发时机 - CubicKeyframe vs SpringKeyframe 的选择
- 关键帧动画在 List 等容器中的表现
- 动画可中断性——PhaseAnimator 中途触发新的动画会如何处理