SwiftUI — Animatable

Animatable 协议:让 SwiftUI 动画不再“失控”

在 SwiftUI 中,Animatable 协议用于自定义视图或形状的动画插值过程。当系统默认的动画行为无法满足需求时,可以通过实现该协议,精细控制属性的过渡效果。以下是详细的使用场景和实现方法:


一、使用场景 #

  1. 自定义形状(Shape)的动画
    当形状的属性变化需要平滑过渡(如绘制进度、顶点位置)时,Animatable 可以驱动路径的变化。

  2. 多个属性的组合动画
    若视图依赖多个参数的同步变化(如缩放和旋转),可用 AnimatablePair 组合这些属性。

  3. 非标准动画类型
    当属性类型无法直接动画(如自定义枚举或结构体),需将其转换为可插值的数值(如 CGFloat)。

  4. 覆盖默认动画行为
    当系统默认插值逻辑不符合预期时,自定义 animatableData 的插值方式。


二、使用方法 #

1. 基本实现 #

对于遵循 ShapeView 的类型,通过实现 animatableData 属性定义动画依赖的数据。

struct ProgressRing: Shape {
    var progress: CGFloat

    // 将 progress 映射为可动画数据
    var animatableData: CGFloat {
        get { progress }
        set { progress = newValue }
    }

    func path(in rect: CGRect) -> Path {
        Path { path in
            let angle = 2 * .pi * progress
            path.addArc(center: CGPoint(x: rect.midX, y: rect.midY),
                        radius: rect.width / 2,
                        startAngle: .zero,
                        endAngle: Angle(radians: angle),
                        clockwise: false)
        }
    }
}

// 使用示例
struct ContentView: View {
    @State private var progress: CGFloat = 0.0
    
    var body: some View {
        ProgressRing(progress: progress)
            .stroke(Color.blue, lineWidth: 4)
            .onTapGesture {
                withAnimation(.easeInOut(duration: 1)) {
                    progress = progress > 0.5 ? 0 : 1
                }
            }
    }
}

2. 组合多个属性(AnimatablePair) #

当需要同时动画多个属性时,使用 AnimatablePair 包装它们。

struct ScaledShape: Shape {
    var scaleX: CGFloat
    var scaleY: CGFloat

    var animatableData: AnimatablePair<CGFloat, CGFloat> {
        get { AnimatablePair(scaleX, scaleY) }
        set {
            scaleX = newValue.first
            scaleY = newValue.second
        }
    }

    func path(in rect: CGRect) -> Path {
        let scaledRect = rect.applying(CGAffineTransform(scaleX: scaleX, y: scaleY))
        return Rectangle().path(in: scaledRect)
    }
}

// 使用示例
struct ContentView: View {
    @State private var scaleX: CGFloat = 1.0
    @State private var scaleY: CGFloat = 1.0

    var body: some View {
        ScaledShape(scaleX: scaleX, scaleY: scaleY)
            .fill(Color.red)
            .onTapGesture {
                withAnimation(.spring()) {
                    scaleX = 2.0
                    scaleY = 0.5
                }
            }
    }
}

3. 复杂类型动画(如角度转换) #

将非数值类型(如 Angle)转换为可插值的数值。

struct RotatingTriangle: Shape {
    var angle: Angle

    var animatableData: Double {
        get { angle.radians }
        set { angle = Angle(radians: newValue) }
    }

    func path(in rect: CGRect) -> Path {
        let center = CGPoint(x: rect.midX, y: rect.midY)
        var path = Path()
        // 根据 angle 绘制三角形路径
        return path
    }
}

三、关键点 #

  • 遵循 VectorArithmetic 的类型
    animatableData 必须为 VectorArithmetic 类型(如 CGFloatDoubleAnimatablePair 等)。

  • 性能优化
    避免在 animatableDataget/set 中执行复杂计算,确保插值高效。

  • 与动画修饰符配合
    通过 withAnimation.animation(_:) 触发动画,确保属性变化被系统捕获。


四、总结 #

通过 Animatable 协议,开发者可以:

  • 为自定义视图或形状添加复杂的动画逻辑。
  • 控制多个属性的同步插值。
  • 将非标准类型转换为可动画数据。

正确使用该协议能显著增强 SwiftUI 动画的灵活性和表现力。

本文共 891 字,创建于 Feb 27, 2025
相关标签: Xcode, SwiftUI