MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

SwiftUI动画与过渡效果

2022-08-073.4k 阅读

SwiftUI 动画基础

隐式动画

在 SwiftUI 中,隐式动画是一种非常方便的方式来为视图添加动画效果。当视图的某些属性发生变化时,SwiftUI 可以自动为这些变化添加动画,而无需开发者手动编写复杂的动画代码。

例如,考虑一个简单的矩形视图,当用户点击时,我们希望它的颜色发生变化并且这个变化是带有动画效果的。

import SwiftUI

struct AnimatedRectangle: View {
    @State private var isTapped = false

    var body: some View {
        Rectangle()
          .fill(isTapped? Color.blue : Color.red)
          .frame(width: 200, height: 100)
          .onTapGesture {
                withAnimation {
                    self.isTapped.toggle()
                }
            }
    }
}

struct AnimatedRectangle_Previews: PreviewProvider {
    static var previews: some View {
        AnimatedRectangle()
    }
}

在上述代码中,我们使用 @State 来跟踪矩形是否被点击。withAnimation 闭包包裹了 isTapped.toggle(),这就意味着当 isTapped 的值发生变化时,Rectanglefill 属性的变化会自动带有动画效果。这里 withAnimation 没有传入任何参数,所以它会使用默认的动画设置,即线性动画,持续时间为 0.3 秒。

显式动画

显式动画给予开发者更多的控制权,可以定制动画的各种参数,如持续时间、曲线、延迟等。

假设我们要创建一个可以控制大小变化的圆形视图,并且可以设置动画的持续时间和曲线。

import SwiftUI

struct ExplicitAnimatedCircle: View {
    @State private var isExpanded = false
    @State private var duration: Double = 1.0
    @State private var curve: AnimationCurve = .easeInOut

    var body: some View {
        VStack {
            Circle()
              .fill(Color.green)
              .frame(width: isExpanded? 200 : 100, height: isExpanded? 200 : 100)
            HStack {
                Slider(value: $duration, in: 0.1...5.0, step: 0.1) {
                    Text("Duration: \(duration, specifier: "%.1f")s")
                }
                Picker("Curve", selection: $curve) {
                    Text("Ease In").tag(AnimationCurve.easeIn)
                    Text("Ease Out").tag(AnimationCurve.easeOut)
                    Text("Ease In Out").tag(AnimationCurve.easeInOut)
                    Text("Linear").tag(AnimationCurve.linear)
                }
              .pickerStyle(SegmentedPickerStyle())
            }
            .padding()
            Button("Toggle Size") {
                withAnimation(Animation
                              .linear(duration: duration)
                              .curve(curve)) {
                    self.isExpanded.toggle()
                }
            }
        }
    }
}

struct ExplicitAnimatedCircle_Previews: PreviewProvider {
    static var previews: some View {
        ExplicitAnimatedCircle()
    }
}

在这个例子中,我们使用 @State 变量 isExpanded 来控制圆形的大小,duration 控制动画持续时间,curve 控制动画曲线。Button 的点击事件中,withAnimation 传入了一个定制的 Animation 对象,它根据用户在 SliderPicker 中的选择来设置动画的持续时间和曲线。

动画类型

线性动画

线性动画是最简单的动画类型,它以恒定的速度执行动画。例如,在前面的显式动画示例中,如果我们将动画设置为线性:

withAnimation(Animation.linear(duration: 1.0)) {
    self.isExpanded.toggle()
}

圆形视图的大小变化将以恒定速度进行,从初始大小到目标大小的过渡过程中速度不会改变。

缓动动画

缓动动画包含 easeIneaseOuteaseInOut 等类型。easeIn 动画开始时速度较慢,然后逐渐加快;easeOut 动画开始时速度较快,然后逐渐减慢;easeInOut 动画则在开始和结束时都较慢,中间速度较快。

easeIn 为例:

withAnimation(Animation.easeIn(duration: 1.0)) {
    self.isExpanded.toggle()
}

当圆形视图进行大小变化时,开始阶段变化会比较缓慢,随着时间推移,变化速度会加快。

弹簧动画

弹簧动画模拟了弹簧的物理行为,使动画更加生动自然。在 SwiftUI 中,可以通过 Animation.spring 来创建弹簧动画。

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

    var body: some View {
        Rectangle()
          .fill(Color.orange)
          .frame(width: 150, height: 100)
          .offset(x: isMoved? 100 : 0, y: 0)
          .onTapGesture {
                withAnimation(Animation.spring(response: 0.5, dampingFraction: 0.7, blendDuration: 0)) {
                    self.isMoved.toggle()
                }
            }
    }
}

struct SpringAnimatedView_Previews: PreviewProvider {
    static var previews: some View {
        SpringAnimatedView()
    }
}

在上述代码中,Animation.springresponse 参数控制弹簧的响应速度,值越小响应越快;dampingFraction 参数控制弹簧的阻尼,值越小弹簧振荡越明显;blendDuration 参数用于控制弹簧动画与其他动画的混合时间。

过渡效果

基本过渡

淡入淡出过渡

淡入淡出过渡是一种常见的过渡效果,用于在视图出现或消失时逐渐改变其透明度。在 SwiftUI 中,可以使用 opacity 结合动画来实现简单的淡入淡出,也可以使用 transition 修饰符来更方便地实现。

struct FadeTransitionView: View {
    @State private var isShowing = false

    var body: some View {
        VStack {
            Button("Toggle View") {
                withAnimation {
                    self.isShowing.toggle()
                }
            }
            if isShowing {
                Text("Hello, World!")
                  .transition(.opacity)
            }
        }
    }
}

struct FadeTransitionView_Previews: PreviewProvider {
    static var previews: some View {
        FadeTransitionView()
    }
}

在这个例子中,当按钮被点击时,isShowing 的值发生变化,Text 视图会根据 transition(.opacity) 进行淡入淡出的过渡效果。

滑动过渡

滑动过渡可以让视图从一个位置滑动到另一个位置,或者从屏幕外滑入屏幕内。

struct SlideTransitionView: View {
    @State private var isShowing = false

    var body: some View {
        VStack {
            Button("Toggle View") {
                withAnimation {
                    self.isShowing.toggle()
                }
            }
            if isShowing {
                Rectangle()
                  .fill(Color.purple)
                  .frame(width: 200, height: 100)
                  .transition(.slide)
            }
        }
    }
}

struct SlideTransitionView_Previews: PreviewProvider {
    static var previews: some View {
        SlideTransitionView()
    }
}

这里 Rectangle 视图在显示和隐藏时会以滑动的方式进行过渡,transition(.slide) 默认是从底部向上滑动,如果想要指定方向,可以使用 transition(.slide(edge:.leading)) 从左侧滑动等。

自定义过渡

创建自定义过渡效果

有时候基本的过渡效果不能满足需求,我们可以创建自定义的过渡效果。自定义过渡通常需要结合 AnyTransition 来实现。

假设我们想要创建一个旋转并缩放的过渡效果。

extension AnyTransition {
    static var rotateAndScale: AnyTransition {
        AnyTransition.modifier(
            active: RotateAndScaleModifier(isActive: true),
            identity: RotateAndScaleModifier(isActive: false)
        )
    }
}

struct RotateAndScaleModifier: ViewModifier {
    var isActive: Bool

    func body(content: Content) -> some View {
        content
          .scaleEffect(isActive? 1.5 : 1.0)
          .rotationEffect(.degrees(isActive? 360 : 0))
          .animation(.easeInOut(duration: 0.5))
    }
}

struct CustomTransitionView: View {
    @State private var isShowing = false

    var body: some View {
        VStack {
            Button("Toggle View") {
                withAnimation {
                    self.isShowing.toggle()
                }
            }
            if isShowing {
                Text("Custom Transition")
                  .transition(.rotateAndScale)
            }
        }
    }
}

struct CustomTransitionView_Previews: PreviewProvider {
    static var previews: some View {
        CustomTransitionView()
    }
}

在上述代码中,我们通过扩展 AnyTransition 创建了一个 rotateAndScale 的自定义过渡。RotateAndScaleModifier 是实现旋转和缩放效果的 ViewModifier,在 activeidentity 状态下分别设置不同的效果,并通过 animation 来控制动画的曲线和持续时间。

过渡与容器视图

在容器视图如 ZStackVStackHStack 等中使用过渡效果,可以实现非常有趣的界面交互。

struct ContainerTransitionView: View {
    @State private var isFirstViewShowing = true

    var body: some View {
        ZStack {
            if isFirstViewShowing {
                Rectangle()
                  .fill(Color.yellow)
                  .frame(width: 200, height: 200)
                  .transition(.slide(edge:.top))
            } else {
                Circle()
                  .fill(Color.blue)
                  .frame(width: 150, height: 150)
                  .transition(.scale)
            }
            Button("Toggle Views") {
                withAnimation {
                    self.isFirstViewShowing.toggle()
                }
            }
        }
    }
}

struct ContainerTransitionView_Previews: PreviewProvider {
    static var previews: some View {
        ContainerTransitionView()
    }
}

在这个 ZStack 示例中,当按钮被点击时,两个不同的视图(RectangleCircle)会根据各自设置的过渡效果进行切换。Rectangle 以从顶部滑动的方式过渡,Circle 以缩放的方式过渡。

复杂动画与过渡组合

动画序列

有时候我们需要按顺序执行多个动画,而不是同时执行。SwiftUI 提供了 animation(_:value:) 方法来实现动画序列。

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

    var body: some View {
        VStack {
            Rectangle()
              .fill(Color.green)
              .frame(width: 150, height: 100)
              .offset(x: step == 0? 0 : step == 1? 100 : 200, y: 0)
              .animation(.easeInOut(duration: 0.5), value: step)
            Button("Next Step") {
                if self.step < 2 {
                    self.step += 1
                } else {
                    self.step = 0
                }
            }
        }
    }
}

struct AnimationSequenceView_Previews: PreviewProvider {
    static var previews: some View {
        AnimationSequenceView()
    }
}

在这个例子中,Rectangleoffset 属性根据 step 的值进行变化,每次点击按钮,step 增加,Rectangle 会以动画的方式移动到下一个位置。animation(_:value:) 中的 value 参数确保只有当 step 的值发生变化时才会触发动画,从而实现了动画的序列执行。

动画组

动画组允许我们同时执行多个动画,并且可以对每个动画设置不同的参数。

struct AnimationGroupView: View {
    @State private var isAnimated = false

    var body: some View {
        Circle()
          .fill(Color.red)
          .frame(width: 150, height: 150)
          .scaleEffect(isAnimated? 2.0 : 1.0)
          .rotationEffect(.degrees(isAnimated? 360 : 0))
          .animation(
                Animation.group {
                    Animation.easeInOut(duration: 1.0).scaleEffect(2.0)
                    Animation.linear(duration: 1.0).rotationEffect(.degrees(360))
                }
            )
          .onTapGesture {
                withAnimation {
                    self.isAnimated.toggle()
                }
            }
    }
}

struct AnimationGroupView_Previews: PreviewProvider {
    static var previews: some View {
        AnimationGroupView()
    }
}

在上述代码中,Circle 视图同时进行缩放和旋转动画。Animation.group 中定义了两个动画,一个是缓动的缩放动画,另一个是线性的旋转动画,它们会同时开始执行,并且各自有不同的动画曲线和持续时间。

过渡与动画结合

将过渡效果与动画相结合可以创造出更加丰富的用户体验。

struct TransitionAndAnimationView: View {
    @State private var isShowing = false

    var body: some View {
        VStack {
            Button("Toggle View") {
                withAnimation {
                    self.isShowing.toggle()
                }
            }
            if isShowing {
                Rectangle()
                  .fill(Color.pink)
                  .frame(width: 200, height: 100)
                  .transition(.slide(edge:.leading))
                  .animation(.spring(response: 0.5, dampingFraction: 0.7, blendDuration: 0))
            }
        }
    }
}

struct TransitionAndAnimationView_Previews: PreviewProvider {
    static var previews: some View {
        TransitionAndAnimationView()
    }
}

在这个例子中,Rectangle 视图在显示时不仅会从左侧滑动进入(过渡效果),而且这个滑动过程会带有弹簧动画的效果,使得过渡更加生动自然。

动画与过渡的性能优化

减少不必要的重绘

在 SwiftUI 中,视图的重绘是比较消耗性能的操作。尽量减少视图属性的频繁变化可以提高性能。例如,如果一个视图的某个属性在短时间内多次变化,并且这些变化并不需要都以动画的形式展示,可以考虑合并这些变化,只在最终状态时触发动画。

struct OptimizedAnimatedView: View {
    @State private var counter = 0

    var body: some View {
        VStack {
            Text("Counter: \(counter)")
            Button("Increment") {
                // 这里先累加,然后在最后统一触发动画
                self.counter += 1
                withAnimation {
                    self.counter += 1
                }
            }
        }
    }
}

struct OptimizedAnimatedView_Previews: PreviewProvider {
    static var previews: some View {
        OptimizedAnimatedView()
    }
}

在这个例子中,每次点击按钮,counter 先进行一次非动画的累加,然后再进行一次带有动画的累加,这样可以避免不必要的中间状态的动画重绘。

合理使用动画参数

选择合适的动画持续时间、曲线和其他参数也对性能有影响。例如,过长的动画持续时间可能会导致用户等待时间过长,而过于复杂的动画曲线可能会增加计算量。在保证动画效果的前提下,尽量选择简单且合适的参数。

struct ParameterOptimizedView: View {
    @State private var isAnimated = false

    var body: some View {
        Rectangle()
          .fill(Color.blue)
          .frame(width: 150, height: 100)
          .scaleEffect(isAnimated? 1.5 : 1.0)
          .animation(.easeInOut(duration: 0.5))
          .onTapGesture {
                withAnimation {
                    self.isAnimated.toggle()
                }
            }
    }
}

struct ParameterOptimizedView_Previews: PreviewProvider {
    static var previews: some View {
        ParameterOptimizedView()
    }
}

这里选择了一个相对较短且常见的缓动动画曲线和持续时间,既保证了动画的流畅性,又不会过度消耗性能。

异步加载与动画

如果动画涉及到加载大量数据或执行复杂计算,可以考虑使用异步操作,以避免阻塞主线程。SwiftUI 中的 Task 可以方便地实现异步任务与动画的结合。

struct AsyncAnimationView: View {
    @State private var dataLoaded = false
    @State private var isAnimated = false

    var body: some View {
        VStack {
            if dataLoaded {
                Rectangle()
                  .fill(Color.green)
                  .frame(width: 200, height: 100)
                  .scaleEffect(isAnimated? 1.5 : 1.0)
                  .animation(.easeInOut(duration: 0.5))
            }
            Button("Load Data and Animate") {
                Task {
                    // 模拟异步加载数据
                    await loadData()
                    withAnimation {
                        self.dataLoaded = true
                        self.isAnimated = true
                    }
                }
            }
        }
    }

    func loadData() async {
        // 这里可以是实际的异步数据加载操作,如网络请求等
        await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    }
}

struct AsyncAnimationView_Previews: PreviewProvider {
    static var previews: some View {
        AsyncAnimationView()
    }
}

在这个例子中,点击按钮后,先通过 Task 异步执行 loadData 方法(模拟数据加载),数据加载完成后,再以动画的方式显示并缩放 Rectangle 视图,这样可以确保主线程不会因为数据加载而卡顿,保证了动画的流畅性。

通过合理运用动画与过渡效果,并进行性能优化,可以为用户带来更加流畅、生动且高效的交互体验。在实际开发中,需要根据具体的应用场景和需求,灵活选择和组合各种动画与过渡技术。