phaseAnimator

温馨提示:本文最后更新于2025-02-05 10:52:57,某些文章具有时效性,若有错误或已失效,请在下方留言

一个容器,通过自动循环遍历你提供的一系列阶段来对其内容进行动画处理,每个阶段定义了动画中的一个离散步骤。

具有阶段动画的弹跳

例如,前面展示的表情符号弹跳动画具有两个阶段:向上移动和返回。你可以使用布尔值truefalse来表示这些阶段。当阶段为true时,表情符号向上移动至-40.0。当阶段为false时,通过将偏移量设置为0.0,表情符号返回到原始位置。

struct TwoPhaseAnimationView: View {
    var emoji: String
    
    var body: some View {
        EmojiView(emoji: emoji)
            .phaseAnimator([false, true]) { content, phase in
                content.offset(y: phase ? -40.0 : 0.0)
            }
    }
}

例如,以下代码每次有人点击表情符号时都会递增状态变量likeCount。代码使用likeCount作为阶段动画器观察变化的值。现在,每当有人点击表情符号时,它都会向上移动并返回到其原始位置。

struct TwoPhaseAnimationView: View {
    var emoji: String
    @State private var likeCount = 1
    
    var body: some View {
        EmojiView(emoji: emoji)
            .phaseAnimator([false, true], trigger: likeCount) { content, phase in
                content.offset(y: phase ? -40.0 : 0.0)
            }
            .onTapGesture {
                likeCount += 1
            }
    }
}

例如,当阶段为 true 时,以下代码应用弹性动画;否则,应用默认动画:

struct TwoPhaseAnimationView: View {
    var emoji: String
    @State private var likeCount = 1
    
    var body: some View {
        EmojiView(emoji: emoji)
            .phaseAnimator([false, true], trigger: likeCount) { content, phase in
                content.offset(y: phase ? -40.0 : 0.0)
            } animation: { phase in
                phase ? .bouncy : .default
            }
            .onTapGesture {
                likeCount += 1
            }
    }
}

为动画添加更多阶段

例如,您可以使表情符号在向上移动时增大,然后再缩小到正常大小。为此,您将向动画添加第三个阶段:缩放。

要定义这些阶段,请创建一个列出可能阶段的自定义类型;例如:

private enum AnimationPhase: CaseIterable {
    case initial
    case move
    case scale
}

接下来,为了简化逻辑并降低复杂性,定义计算属性来返回要动画的值。例如,要设置垂直偏移量以移动表情符号,请创建一个计算属性,根据当前阶段返回偏移量:

private enum AnimationPhase: CaseIterable {
    case initial
    case move
    case scale
    
    var verticalOffset: Double {
        switch self {
        case .initial: return 0
        case .move, .scale: return -64
        }
    }
}

当处于初始阶段时,偏移量为 O,这是表情符号在屏幕上的原始位置。但是当阶段是 move 或 scale 时,偏移量为-64。

您可以使用相同的方法(创建计算属性)来实现缩放效果,以改变表情符号的大小。最初,表情符号以其原始大小出现,但在移动和缩放阶段增大,如下所示:

private enum AnimationPhase: CaseIterable {
    case initial
    case move
    case scale
    
    var verticalOffset: Double {
        switch self {
        case .initial: 0
        case .move, .scale: -64
        }
    }
    
    var scaleEffect: Double {
        switch self {
        case .initial: 1
        case .move, .scale: 1.5
        }
    }
}

要对表情符号进行动画处理,请将 phaseAnimator(_:trigger:content:animation:)修饰符应用于EmojiView。向动画器提供来自自定义AnimationPhase类型的所有情况。然后根据阶段改变内容,通过应用 scaleEffect(_:anchor:)offset(x:y:)修饰符。这些修饰符传入的值来自计算属性,有助于保持视图代码更易读。

struct ThreePhaseAnimationView: View {
    var emoji: String
    @State private var likeCount = 1
    
    var body: some View {
        EmojiView(emoji: emoji)
            .phaseAnimator(AnimationPhase.allCases, trigger: likeCount) { content, phase in
                content
                    .scaleEffect(phase.scaleEffect)
                    .offset(y: phase.verticalOffset)
            } animation: { phase in
                switch phase {
                case .initial: .smooth
                case .move: .easeInOut(duration: 0.3)
                case .scale: .spring(duration: 0.3, bounce: 0.7)
                }
            }
            .onTapGesture {
                likeCount += 1
            }
    }
}

代码还根据动画闭包中的阶段应用不同的动画类型,以赋予动画以更多的生气。

使用关键帧动画器获得更多控制

您可以使用 KeyframeAnimator 定义复杂的、协调的动画,并完全控制时间和运动。这个动画器允许您创建关键帧,这些关键帧在动画过程中的特定时间定义值。动画器使用这些值来在动画的每一帧之间生成插值值。

与阶段动画器不同,在阶段动画器中,您建模单独的、离散的状态,而关键帧动画器生成您指定类型的插值值。在动画进行中,动画器为您提供此类型的值,因此您可以通过对其应用修饰符来更新动画视图。

您可以将该类型定义为包含您想要独立动画的属性的结构。例如,以下代码定义了四个属性,确定表情符号的缩放、拉伸、位置和角度:

private struct AnimationValues {
    var scale = 1.0
    var verticalStretch = 1.0
    var verticalOffset = 0.0
    var angle = Angle.zero
}

KeyframeAnimator 可以对符合 Animatable 协议的任何值进行动画处理。

要使用关键帧动画器创建动画,请将 keyframeAnimator(initialValue:repeating:content:keyframes:)keyframeAnimator(initialValue:trigger:content:keyframes:)修饰符应用于要动画处理的视图。例如,以下代码将第二个修饰符应用于 EmojiView。动画的初始值是一个 Animationvalues 的新实例,状态变量 likeCount 是动画器观察变化的值,就像在之前的阶段动画示例中一样。

struct KeyframeAnimationView: View {
    var emoji: String
    @State private var likeCount = 1
    
    var body: some View {
        EmojiView(emoji: emoji)
            .keyframeAnimator(
                initialValue: AnimationValues(),
                trigger: likeCount
            ) { content, value in
                // ...
            } keyframes: { _ in
                // ...
            }
            .onTapGesture {
                likeCount += 1
            }
    }
}

在动画期间对视图应用修饰符,请为关键帧动画器提供一个 content 闭包。此闭包包括两个参数:

content 正在进行动画的视图。

value 当前的插值值。

使用这些参数来对 SwiftUI 正在动画处理的视图应用修饰符。例如,以下代码使用这些参数来旋转、缩放、拉伸和移动一个表情符号:

struct KeyframeAnimationView: View {
    var emoji: String
    @State private var likeCount = 1
    
    var body: some View {
        EmojiView(emoji: emoji)
            .keyframeAnimator(
                initialValue: AnimationValues(),
                trigger: likeCount
            ) { content, value in
                content
                    .rotationEffect(value.angle)
                    .scaleEffect(value.scale)
                    .scaleEffect(y: value.verticalStretch)
                    .offset(y: value.verticalOffset)
            } keyframes: { _ in
                // ...
            }
            .onTapGesture {
                likeCount += 1
            }
    }
}

SwiftUI 在动画的每一帧上调用关键帧动画器的 content 闭包,因此避免直接在其中执行任何昂贵的操作。

接下来,定义关键帧。关键帧让您可以使用不同的关键帧为不同的属性构建复杂的动画。为了实现这一点,您将关键帧组织成轨道。每个轨道控制您正在动画处理的类型的不同属性。通过在创建轨道时提供属性的关键路径,您将属性与轨道关联起来。例如,以下代码为 scale 属性添加了一个 KeyframeTrack

struct KeyframeAnimationView: View {
    var emoji: String
    @State private var likeCount = 1
    
    var body: some View {
        EmojiView(emoji: emoji)
            .keyframeAnimator(
                initialValue: AnimationValues(),
                trigger: likeCount
            ) { content, value in
                content
                    .rotationEffect(value.angle)
                    .scaleEffect(value.scale)
                    .scaleEffect(y: value.verticalStretch)
                    .offset(y: value.verticalOffset)
            } keyframes: { _ in
                KeyframeTrack(\.scale) {
                    // ...
                }
            }
            .onTapGesture {
                likeCount += 1
            }
    }
}

在创建轨道时,您可以在SwiftUl中使用声明性语法向轨道添加关键帧。有不同类型的关键帧,如 CubickeyframeLinearkeyframespringKeyframe。您可以在轨道内混合和匹配不同类型的关键帧。例如,以下代码为 scale属性添加了一个轨道,执行线性和弹簧动画的组合:

struct KeyframeAnimationView: View {
  var emoji: String
  @State private var likeCount = 1
  
  var body: some View {
    EmojiView(emoji: emoji)
      .keyframeAnimator(
          initialValue: AnimationValues(),
          trigger: likeCount
      ) { content, value in
          content
              .rotationEffect(value.angle)
              .scaleEffect(value.scale)
              .scaleEffect(y: value.verticalStretch)
              .offset(y: value.verticalOffset)
      } keyframes: { _ in
          KeyframeTrack(\.scale) {
              LinearKeyframe(1.0, duration: 0.36)
              SpringKeyframe(1.5, duration: 0.8,
                  spring: .bouncy)
              SpringKeyframe(1.0, spring: .bouncy)
          }
      }
      .onTapGesture {
          likeCount += 1
      }
  }
}

每种关键帧类型都接收一个值。动画器使用此值在帧之间生成插值值,并在调用动画器的内容闭包之前设置轨道的关键路径中指定的属性。例如,在上面的代码清单中,线性关键帧期间的缩放值为 1.0,这会保持表情符号的原始大小。然后,在第一个弹簧关键帧期间,缩放变为 1.5。这导致表情符号变大。最后一个弹簧关键帧将缩放设置为 1.0,将表情符号返回到其原始大小。

SwiftUl 在轨道内的连续运动中跨多个关键帧保留速度(即动画的速度)。

在实现关键帧动画时,为每个要动画处理的属性包括一个轨道。例如,

  • scale
  • verticalStretch
  • verticalOffset
  • angle

要对所有四个属性进行动画处理,动画器需要四个关键帧轨道,如下面的代码所示: AnimationValues 有四个属性:

struct KeyframeAnimationView: View {
    var emoji: String
    @State private var likeCount = 1
    
    var body: some View {
        EmojiView(emoji: emoji)
            .keyframeAnimator(
                initialValue: AnimationValues(),
                trigger: likeCount
            ) { content, value in
                content
                    .rotationEffect(value.angle)
                    .scaleEffect(value.scale)
                    .scaleEffect(y: value.verticalStretch)
                    .offset(y: value.verticalOffset)
            } keyframes: { _ in
                KeyframeTrack(\.scale) {
                    LinearKeyframe(1.0, duration: 0.36)
                    SpringKeyframe(1.5, duration: 0.8, spring: .bouncy)
                    SpringKeyframe(1.0, spring: .bouncy)
                }
                
                KeyframeTrack(\.verticalOffset) {
                    LinearKeyframe(0.0, duration: 0.1)
                    SpringKeyframe(20.0, duration: 0.15, spring: .bouncy)
                    SpringKeyframe(-60.0, duration: 1.0, spring: .bouncy)
                    SpringKeyframe(0.0, spring: .bouncy)
                }


                KeyframeTrack(\.verticalStretch) {
                    CubicKeyframe(1.0, duration: 0.1)
                    CubicKeyframe(0.6, duration: 0.15)
                    CubicKeyframe(1.5, duration: 0.1)
                    CubicKeyframe(1.05, duration: 0.15)
                    CubicKeyframe(1.0, duration: 0.88)
                    CubicKeyframe(0.8, duration: 0.1)
                    CubicKeyframe(1.04, duration: 0.4)
                    CubicKeyframe(1.0, duration: 0.22)
                }


                KeyframeTrack(\.angle) {
                    CubicKeyframe(.zero, duration: 0.58)
                    CubicKeyframe(.degrees(16), duration: 0.125)
                    CubicKeyframe(.degrees(-16), duration: 0.125)
                    CubicKeyframe(.degrees(16), duration: 0.125)
                    CubicKeyframe(.zero, duration: 0.125)
                }
            }
            .onTapGesture {
                likeCount += 1
            }
    }
}

这些关键帧轨道的组合创建了一个动画,将表情符号压缩和拉伸,然后将其向上弹起。当表情符号向其最高点移动时,它变得更大。当表情符号达到最高点时,它会稍微摆动。然后,当表情符号回到原始位置时,它会稍微弹跳,以回到原来的位置。

与阶段动画一样,使用Xcode中的画布预览来帮助确定要应用于关键帧动画的动画类型和值。对代码进行更改,并在画布预览中看到这些更改的反映。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容