Animation — animation

Animation #

SwiftUI 中,动画能够为用户界面带来平滑且自然的动态效果,这不仅能提升用户体验,还能增强应用的交互性。了解动画的体系结构、具体实现方式及核心概念,是将其灵活运用于项目的关键。

接下来,我们从 SwiftUI Animation 的体系结构 入手,详细讲解 Animation 的类型、组成以及使用方式。

SwiftUI 中的 Animation 是一个描述性的数据结构,他定义了动画的行为(例如持续时间、缓动函数等),并将其应用在视图变化上。

动画的构成要素 #

  1. 触发动画的条件

    • 通过状态变化(State Change)驱动动画。
    • 通常绑定到 @State 或其他可以触发视图更新的属性。
  2. 动画类型和曲线(Timing Curve)

    • 包括动画的缓动效果(如 easeInOutlinear)、时长以及重复次数等。
  3. 动画修饰符

    • 使用 .animation(_:)withAnimation(_:) 将动画行为应用到视图或者特定代码逻辑。
  4. 组合动画(Chained/Composable Animation)

    • 多个动画行为可以被组合在一起,用于更复杂的效果。

1. Animation 的类型 #

SwiftUI 提供了多种内置动画类型,主要以时间函数(Timing Function)和缓动曲线(Easing)为核心,控制动画的速度、轨迹和效果。

1.1 基础动画类型 #

动画类型描述
linear线性动画,速度恒定,不受缓动影响。
easeIn渐入动画,动画开始时速度慢,随后逐渐加速。
easeOut渐出动画,动画开始时速度快,随后逐渐减速。
easeInOut渐入渐出动画,开始速度慢,随后加速,再渐渐减速,适合自然切换效果。
spring弹性动画,产生弹跳和物理感。

示例:不同类型动画

import SwiftUI

struct BasicAnimationExample: View {
    @State private var isMoved = false

    var body: some View {
        VStack(spacing: 20) {
            Button("Trigger Animation") {
                withAnimation(.easeInOut(duration: 1.5)) {
                    isMoved.toggle()
                }
            }

            // 动画类型:EaseInOut
            RoundedRectangle(cornerRadius: 16)
                .fill(Color.blue)
                .frame(width: 100, height: 100)
                .offset(x: isMoved ? 150 : -150, y: 0)
        }
    }
}

1.2 弹性动画(Spring Animations) #

弹性动画通过模拟物理弹簧的行为带来自然的动感。主要参数包括:

  • response:定义弹簧的响应速度,值越小速度越快。
  • dampingFraction:定义阻尼系数,控制弹跳的幅度(0 是无阻尼,1 是完全阻尼)。
  • blendDuration:定义动画被打断时的过渡平滑时间。

示例:弹性动画效果

import SwiftUI

struct SpringAnimationExample: View {
    @State private var isScaled = false

    var body: some View {
        Circle()
            .fill(Color.red)
            .frame(width: 100, height: 100)
            .scaleEffect(isScaled ? 1.5 : 1) // 放大和缩小
            .onTapGesture {
                withAnimation(.spring(response: 0.5, dampingFraction: 0.4)) {
                    isScaled.toggle()
                }
            }
    }
}

1.3 重复动画(Repeat Animations) #

通过 repeatCountrepeatForever,控制动画的重复次数或无限重复。

示例:无限旋转动画

import SwiftUI

struct RepeatingAnimationExample: View {
    @State private var rotation = 0.0

    var body: some View {
        Image(systemName: "arrow.2.circlepath")
            .font(.largeTitle)
            .rotationEffect(.degrees(rotation))
            .onAppear {
                withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) {
                    rotation = 360 // 无限旋转
                }
            }
    }
}

属性说明:

  • repeatForever(autoreverses:):是否在每次循环后反向播放。
  • repeatCount(_ count, autoreverses: true):设置具体重复次数。

1.4 自定义动画(Custom Timing Curves) #

SwiftUI 支持用贝塞尔曲线模拟时间函数(Timing Curve),通过 timingCurve 创建独特动画曲线。

示例:自定义贝塞尔曲线动画

import SwiftUI

struct CustomAnimationExample: View {
    @State private var isMoved = false

    var body: some View {
        RoundedRectangle(cornerRadius: 25)
            .frame(width: 100, height: 100)
            .offset(x: isMoved ? 150 : -150)
            .onTapGesture {
                withAnimation(.timingCurve(0.2, 0.8, 0.4, 1.0, duration: 1.5)) {
                    isMoved.toggle()
                }
            }
    }
}

2. Animation 的使用方式 #


2.1 修饰符 .animation(_:value:) #

animation(_:value:) 将动画绑定到视图属性的变化上,当指定的 value 发生变化时,系统会自动触发动画效果。

示例:绑定自动触发动画

struct AnimationBindingExample: View {
    @State private var isScaled = false

    var body: some View {
        Circle()
            .fill(Color.purple)
            .frame(width: isScaled ? 150 : 50, height: isScaled ? 150 : 50)
            .animation(Animation.easeInOut(duration: 1), value: isScaled) // 绑定动画
            .onTapGesture { isScaled.toggle() }
    }
}

2.2 withAnimation 函数 #

withAnimation 明确为某个代码块定义动画,适合对多个属性同时应用动画。

示例:点击缩放和旋转

struct WithAnimationExample: View {
    @State private var isAnimating = false

    var body: some View {
        RoundedRectangle(cornerRadius: 20)
            .fill(Color.orange)
            .frame(width: isAnimating ? 200 : 100, height: isAnimating ? 200 : 100)
            .rotationEffect(.degrees(isAnimating ? 45 : 0))
            .onTapGesture {
                withAnimation(.easeInOut(duration: 1.5)) {
                    isAnimating.toggle()
                }
            }
    }
}

3. 动画的可组合性(Composable Animations) #

SwiftUI 支持对动画进行组合,让多个动画行为在同一时间执行。

3.1 多个动画组合 #

通过不同的动画作用于同一视图的多个属性。

struct ComposableAnimationsExample: View {
    @State private var scale: CGFloat = 1
    @State private var opacity: Double = 1

    var body: some View {
        Circle()
            .scaleEffect(scale)
            .opacity(opacity)
            .onTapGesture {
                withAnimation(.easeInOut(duration: 1)) { scale = 1.5 }
                withAnimation(.linear(duration: 1)) { opacity = 0.5 }
            }
    }
}

3.2 动画时间线 #

为了实现依次执行不同动画,可以通过状态值切换让动画按时间线进行:

struct SequentialAnimationsExample: View {
    @State private var step = 0

    var body: some View {
        VStack {
            Circle()
                .offset(x: step >= 1 ? 100 : 0)
                .scaleEffect(step >= 2 ? 2 : 1)
                .onTapGesture {
                    withAnimation(Animation.easeOut(duration: 0.5)) { step = 1 }
                    withAnimation(Animation.easeInOut(duration: 0.5).delay(0.5)) { step = 2 }
                }
        }
    }
}

4. 动画的注意事项 #

  1. 使用 State 或绑定驱动动画
    动画需要通过可观察的数据发生变化时触发,常见的方式是通过 @State 或绑定到 Binding

  2. 结合视图生命周期(如 onAppear/onDisappear)触发动画
    有些动画可以通过 onAppear 自动启动:

    .onAppear {
        withAnimation { ... }
    }
    
  3. 防止不必要的动画
    如果某些视图变化不需要动画效果,可以通过手动设置 .animation(nil) 禁用动画。

  4. 优选系统内置动画曲线
    系统内置的缓动曲线(如 easeIneaseOut)大多符合用户期待,只有在特殊场景时才使用自定义曲线。


总结 #

SwiftUI 的动画体系简单明了,同时又具备高度灵活性,具有如下特点:

  1. 提供内置的丰富动画类型(lineareaseInspring 等)。
  2. 支持复杂的组合、时间线、顺序和自定义动画。
  3. 尽量通过状态驱动动画,不再需要复杂的手动管理。

可以根据项目需求选择合适的动画类型和方式,灵活运用动画完成精致的动态体验。

animation 和 withAnimation 区别 #

在 SwiftUI 中,animation 修饰符和 withAnimation 函数都用于实现动画效果,但它们的作用方式使用场景有显著区别。以下是两者的详细对比:


1. animation 修饰符 #

用途#

  • 为视图的某个特定状态变化添加动画
  • 直接附加到视图上,声明该视图的某个依赖项变化时触发的动画效果。

核心特点#

  • 细粒度控制:通过 value 参数指定监听的状态,仅当该值变化时触发动画。
  • 隐式动画:自动关联视图的依赖项变化,无需手动包裹状态修改代码。
  • 局部作用域:仅影响当前视图及其子视图的指定动画行为。

示例#

struct ContentView: View {
    @State private var isScaled = false

    var body: some View {
        Button("缩放") {
            isScaled.toggle() // 直接修改状态,无需包裹 withAnimation
        }
        .padding()
        .scaleEffect(isScaled ? 1.5 : 1)
        .animation(.easeInOut(duration: 0.3), value: isScaled) // 仅监听 isScaled 变化
    }
}

适用场景#

  • 当需要为特定视图的某个状态变化添加动画时。
  • 需要明确指定动画类型(如时长、曲线)和触发条件(通过 value 参数)。

2. withAnimation 函数 #

用途#

  • 显式触发一段动画代码块,包裹状态变化的逻辑。
  • 用于在多个视图或多个状态同时变化时同步动画

核心特点#

  • 显式触发:需手动将状态修改代码包裹在 withAnimation 闭包内。
  • 全局作用域:影响所有依赖被修改状态的视图的动画行为。
  • 灵活控制:可在闭包内执行多个状态修改,所有相关视图的动画会同步执行。

示例#

struct ContentView: View {
    @State private var isScaled = false
    @State private var isRotated = false

    var body: some View {
        Button("动画组合") {
            withAnimation(.spring(dampingFraction: 0.5)) { // 显式包裹状态变化
                isScaled.toggle()
                isRotated.toggle()
            }
        }
        .padding()
        .scaleEffect(isScaled ? 1.5 : 1)
        .rotationEffect(.degrees(isRotated ? 180 : 0))
        // 不需要单独添加 .animation 修饰符
    }
}

适用场景#

  • 需要同时触发多个状态变化的动画。
  • 希望在状态变化时显式控制动画逻辑(如动态调整动画参数)。
  • 需要在非视图层(如 ViewModel)中触发动画

关键区别总结 #

特性animation 修饰符withAnimation 函数
触发方式自动(监听指定 value 变化)显式(需包裹状态修改代码)
作用范围当前视图及其子视图所有依赖被修改状态的视图
动画控制通过 value 参数指定触发条件动态控制闭包内的多个状态变化
适用场景单个视图的特定状态变化多视图/多状态同步动画、复杂逻辑触发

选择建议 #

  1. 使用 animation 修饰符

    • 当需要为特定视图的某个属性添加动画。
    • 希望代码更简洁,避免显式包裹状态修改。
    • 例如:按钮点击时缩放、颜色渐变等。
  2. 使用 withAnimation 函数

    • 当需要同时触发多个视图或状态的动画
    • 需要动态调整动画参数(如根据条件选择不同的动画曲线)。
    • 例如:表单提交后多个视图联动、复杂交互逻辑中触发动画。

注意事项 #

  • 避免滥用隐式动画
    若未指定 value 参数(如 .animation(.default)),animation 修饰符会监听所有视图变化,可能导致意外动画。
  • 主线程安全
    withAnimation 必须在主线程调用,否则动画可能无法生效。
  • 性能优化
    对复杂视图层级使用 withAnimation 可能导致性能问题,优先考虑局部动画修饰符。

Bool 和 Animation #

在 SwiftUI 中,当你将 Bool 类型的状态更新放在 withAnimation 闭包中时,Bool 的值会立即变化,而动画是随后随着渲染引擎逐帧完成的。这意味着,Bool 的值更改和动画的执行是并行的,即动画开始的时刻值已经更新。


关键行为点 #

  1. Bool 值先立即更新:

    • 无论是在 withAnimation 内更改 Bool 的值,还是状态绑定到其他视图,Bool 的值总是 立刻同步更新,即 UI 知道状态值已经发生变化。
  2. 动画随后执行:

    • Bool 的值更新后,SwiftUI 会触发一个渲染更新,并及时应用动画效果。但动画需要一定的时间(取决于你设置的时长),因此这是一个按帧逐渐变化的视觉效果。

代码示例 #

示例 1:简单的 Bool 切换动画 #

struct BoolWithAnimationExample: View {
    @State private var isToggleOn = false // `Bool` 状态

    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 12)
                .fill(isToggleOn ? .green : .red) // 根据`Bool`变量调整颜色
                .frame(width: 100, height: 100)
                .rotationEffect(isToggleOn ? .degrees(360) : .degrees(0)) // 根据状态应用旋转动画

            Button("Toggle") {
                withAnimation(.linear(duration: 1)) {
                    isToggleOn.toggle() // 状态在 `withAnimation` 内更改
                }
            }
        }
    }
}

运行行为:

  1. 点击按钮后,isToggleOn 的状态值立即切换(false -> truetrue -> false)。
  2. 由于状态绑定到视图(颜色和旋转角度),视图的外观会随状态切换触发动画。
  3. 动画以线性模式执行,持续 1 秒,从起始状态过渡到目标状态。

示例 2:观察 Bool 更新时机 #

为了验证是 Bool 值先变化还是动画完成后变化,可以通过打印日志来跟踪状态更新顺序:

struct TrackBoolChangeExample: View {
    @State private var isExpanded = false

    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 12)
                .fill(.blue)
                .frame(width: isExpanded ? 200 : 100, height: 100)
                .animation(.easeInOut, value: isExpanded) // 添加动画

            Button("Expand/Collapse") {
                withAnimation {
                    print("Before changing: \(isExpanded)") // 打印状态
                    isExpanded.toggle()
                    print("After changing: \(isExpanded)") // 打印状态
                }
            }
        }
    }
}

输出日志(点击按钮执行):

Before changing: false
After changing: true

结果分析:

  • isExpanded 的值会在 toggle() 后立刻改变(即 false -> truetrue -> false),而不取决于动画是否完成。
  • 动画则是视觉上逐帧过渡的效果,开始于状态更新之后。

示例 3:多个状态值影响动画 #

如果多个状态值(不止 Bool)需要影响动画,所有这些值都会立即发生变化,而动画会针对所有已更改的状态渲染过渡。

struct MultiStateWithAnimation: View {
    @State private var isGreen = false
    @State private var isLarge = false

    var body: some View {
        VStack {
            Circle()
                .fill(isGreen ? .green : .red)
                .frame(width: isLarge ? 200 : 100, height: isLarge ? 200 : 100)

            Button("Toggle") {
                withAnimation(.spring()) { // 使用弹簧动画
                    isGreen.toggle() // 立即切换颜色的状态
                    isLarge.toggle() // 同时立即切换尺寸的状态
                }
            }
        }
    }
}

运行行为: 4. 无论是 isGreen 还是 isLarge,它们的值都会在 toggle() 方法调用时立刻更新。 5. 动画渲染会基于状态的更改同步发生,弹簧动画(spring)逐渐展现过渡效果。


使用场景提示和注意点 #

  1. 动画与状态更新无冲突:

    • 动画的视觉效果与状态更新 解耦,状态值的更改是即时的,而动画仅负责视觉上的平滑过渡。
    • 不需要担心动画完成后值才会更改。
  2. 动画的是视图属性而非值本身:

    • 动画实际应用在 视图的绑定属性 上(例如 frame, rotationEffect, scaleEffect 等),而 Bool 等状态只是用于驱动这些属性。
  3. 多个绑定值组合时:

    • 动画可以同步处理多个属性的变化,所有参与动画的绑定值都会在动画触发前更新为最新值。
  4. 性能考虑:

    • withAnimation 不适合频繁/快速切换的场景(如快速点击触发),应当结合 Task 或节流逻辑限制状态更新频率。
  5. async 任务结合:

  • 如果你需要等待动画完成后再执行某些逻辑,则需要使用异步工具(如 Task.sleep)手动处理动画等待,因为动画执行和状态变化始终是并行的。

完整结合:如何等待动画完成后执行逻辑? #

默认情况下,SwiftUI 不会在动画完成后触发任何回调。如果你想在动画完成后执行额外逻辑,需要借助 DispatchQueueasync/await

struct WaitForAnimationExample: View {
    @State private var isExpanded = false
    @State private var message = "Waiting..."

    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 12)
                .fill(.blue)
                .frame(width: isExpanded ? 200 : 100, height: 100)
                .animation(.easeInOut(duration: 1.0), value: isExpanded)
            
            Text(message) // 显示当前状态信息

            Button("Expand") {
                withAnimation {
                    isExpanded.toggle()
                }
                DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { // 等待动画完成后执行
                    message = "Animation Completed!" // 更新消息
                }
            }
        }
    }
}

总结 #

  • 值的改变(如 Bool先于动画完成立即发生
  • 动画仅控制视觉效果,不干涉变量本身的更新时机。
  • 如果需要等待动画完成后执行后续逻辑,可以通过诸如 DispatchQueue 或异步任务进行延迟处理。
本文共 4432 字,创建于 Jan 8, 2025
相关标签: SwiftUI