Animations
SwiftUI 的动画系统通过插值(Interpolation)在视图的两个状态之间创建平滑过渡。它深度集成在声明式语法中,使得添加复杂的动画效果变得简单直观。
SwiftUI 的动画系统通过插值(Interpolation)在视图的两个状态之间创建平滑过渡。它深度集成在声明式语法中,使得添加复杂的动画效果变得简单直观。
动画基础(Creating Animations)·
在 SwiftUI 中,动画主要通过两种方式触发:隐式动画和显式动画。
隐式动画(Implicit Animation)
使用 .animation(_:value:) 修饰符。当指定的 value 发生变化时,SwiftUI 会自动为该视图及其子视图的属性变化应用动画。
struct ImplicitAnimationExample: View {
@State private var scale = 1.0
var body: some View {
Button("Scale Me") {
scale += 0.5
}
.scaleEffect(scale)
.animation(.default, value: scale) // 仅当 scale 变化时触发动画
}
}显式动画 (Explicit Animation)
使用 withAnimation 函数。在闭包内修改状态,所有受该状态影响的视图变化都会被动画化。这种方式提供了更细粒度的控制。
struct ExplicitAnimationExample: View {
@State private var opacity = 1.0
var body: some View {
Button("Fade") {
withAnimation(.easeIn(duration: 1.0)) {
opacity = 0.0
}
}
.opacity(opacity)
}
}动画类型(Animation Types)
SwiftUI 提供了多种预定义的动画曲线,适用于不同的场景。
标准动画 (Standard)
基于时间曲线的动画,行为可预测。
.linear: 匀速运动,机械感强。.easeIn: 慢进快出,适合物体移出屏幕。.easeOut: 快进慢出,适合物体移入屏幕。.easeInOut: 慢进慢出,最自然的通用选择。
.animation(.easeInOut(duration: 0.5), value: isVisible)弹簧动画 (Spring) - iOS 17+
iOS 17 引入了更具表现力的弹簧动画 API,基于物理模型,通过 duration(持续时间)和 bounce(弹性)来配置。
.spring: 通用弹簧动画。.bouncy: 高弹性,生动活泼。.smooth: 无弹性,平滑自然。.snappy: 小弹性,快速响应。
// iOS 17+
.animation(.bouncy(duration: 0.5, extraBounce: 0.2), value: offset)
.animation(.smooth, value: offset)对于旧版本或更底层的控制,仍可使用 .interpolatingSpring(mass:stiffness:damping:)。
自定义曲线 (Custom)
使用贝塞尔曲线定义动画速度。
.animation(.timingCurve(0.2, 0.8, 0.2, 1.0, duration: 1.0), value: offset)动画配置(Configuration)
可以通过修饰符进一步调整动画的行为。
.delay(0.5): 延迟动画开始。.speed(2.0): 加速或减速动画(不改变曲线形状)。.repeatCount(3, autoreverses: true): 重复指定次数。.repeatForever(autoreverses: true): 无限重复。
Circle()
.scaleEffect(isPulsing ? 1.2 : 1.0)
.animation(
.easeInOut(duration: 1.0)
.repeatForever(autoreverses: true),
value: isPulsing
)事务(Transactions)
Transaction 是 SwiftUI 状态更新的上下文,它携带了动画信息。我们可以通过它来禁用动画或覆盖现有的动画。
禁用动画
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
// 这里的状态改变不会触发动画
isExpanded.toggle()
}或者使用 .transaction 修饰符:
Toggle("Toggle", isOn: $isOn)
.transaction { transaction in
transaction.animation = nil // 禁用此视图的动画
}阶段动画(Phase Animations) - iOS 17+
PhaseAnimator 允许视图在一系列离散的阶段(Phases)之间循环动画。非常适合创建多步骤的动画序列。
enum Phase: CaseIterable {
case initial, move, scale
}
PhaseAnimator(Phase.allCases, trigger: startAnimation) { phase in
Circle()
.foregroundStyle(.blue)
// 根据当前 phase 定义视图状态
.scaleEffect(phase == .scale ? 1.5 : 1.0)
.offset(x: phase == .move ? 100 : 0)
} animation: { phase in
// 为每个 phase 转换定义不同的动画
switch phase {
case .initial: .bouncy
case .move: .smooth(duration: 1.0)
case .scale: .spring(duration: 0.5)
}
}关键帧动画(Keyframe Animations)- iOS 17+
KeyframeAnimator 提供了最强大的控制力,允许独立控制多个属性的动画轨道(Tracks),并使用关键帧定义每一帧的值。
struct KeyframeExample: View {
@State private var runAnimation = false
var body: some View {
KeyframeAnimator(
initialValue: AnimationValues(),
trigger: runAnimation
) { values in
// 使用计算出的 values 更新视图
Circle()
.scaleEffect(values.scale)
.offset(y: values.verticalOffset)
.opacity(values.opacity)
} keyframes: { _ in
// 定义多个轨道
KeyframeTrack(\.scale) {
CubicKeyframe(1.2, duration: 0.2)
LinearKeyframe(1.0, duration: 0.5)
}
KeyframeTrack(\.verticalOffset) {
SpringKeyframe(-50, duration: 0.3)
SpringKeyframe(0, duration: 0.3)
}
KeyframeTrack(\.opacity) {
LinearKeyframe(0.5, duration: 0.2)
LinearKeyframe(1.0, duration: 0.4)
}
}
.onTapGesture { runAnimation.toggle() }
}
}
struct AnimationValues {
var scale = 1.0
var verticalOffset = 0.0
var opacity = 1.0
}自定义动画数据(Animatable)
当需要动画化非标准属性(如绘制路径中的点)时,需要遵循 Animatable 协议。核心是实现 animatableData 属性,该属性必须遵循 VectorArithmetic。
SwiftUI 会在动画过程中多次设置 animatableData 的值,从而驱动视图重绘。
struct AnimatableCorner: Shape {
var cornerRadius: CGFloat
// 告诉 SwiftUI 这个属性是可以动画的
var animatableData: CGFloat {
get { cornerRadius }
set { cornerRadius = newValue }
}
func path(in rect: CGRect) -> Path {
Path { path in
path.addRoundedRect(in: rect, cornerSize: CGSize(width: cornerRadius, height: cornerRadius))
}
}
}
// 使用
// withAnimation { cornerRadius = 50 }
// AnimatableCorner(cornerRadius: cornerRadius)对于多个属性,可以使用 AnimatablePair 组合。
var animatableData: AnimatablePair<CGFloat, CGFloat> {
get { AnimatablePair(firstValue, secondValue) }
set {
firstValue = newValue.first
secondValue = newValue.second
}
}上次更新于