如何在 SwiftUI 中创建复杂的系列动画

问题描述 投票:0回答:1

如何在 SwiftUI 中组合“复杂”的动画?其中复杂是指完整的动画由多个子动画组成。

示例

我正在努力将现有项目从

UIKit
迁移到
SwiftUI
。该项目使用自定义
UIView
来显示进度条,以动画方式显示正值和负值之间的变化。正值用绿色条显示,负值用红色条显示。

假设

maxValue
为 100:

  • +60 的值将显示为 60% 绿色条
  • 将值更改为 +20 将使 绿色条在 1 秒内降低至 20%
  • 将值更改为 -40 会在 1/3 秒内将绿色条动画降至 0,然后在 2/3 秒内将红色条从 0 无缝动画到 40%。这个完整的动画总共也需要1秒。

因此,无论值如何变化,动画总长度始终为 1 秒。当从正值更改为负值(或反之亦然)时,条形会缩小到 0,然后再改变颜色并再次变长。

简单进度条

在 SwiftUI 中创建一个简单的进度条没什么大不了的:

struct SomeProgressBar: View {
    var height: CGFloat = 20
    
    var minValue: Double = 0
    var maxValue: Double = 100
    var currentValue: Double = 20
    
    var body: some View {
        let maxV = max(minValue, maxValue)
        let minV = min(minValue, maxValue)
        
        let currentV = max(min(abs(currentValue - minValue), maxV), minV)
        let isNegative = currentValue < minValue
        
        let progress = (currentV - minV) / (maxV - minV)
        
        Rectangle()
            .fill(.gray)
            .frame(height: height)
            .overlay(alignment: .leading) {
                GeometryReader { geometry in
                    Rectangle()
                        .fill(isNegative ? .red : .green)
                        .frame(width: geometry.size.width * progress)
                }
            }
            .animation(.easeInOut(duration: 0.5), value: progress)
    }
}

struct SomeProgressBarTestView: View {
    @State var value: Double = 40
    
    var body: some View {
        VStack {
            SomeProgressBar(currentValue: value)
            
            Button("Change") {
                value = .random(in: -100...100)
            }
        }
    }
}

问题

虽然在仅使用正值或仅使用负值时效果很好,但正值和负值之间的切换不会按预期设置动画。例如,在 +x 和 -x 之间切换只会更改条形颜色,但不会使条形动画化。这并不奇怪,因为宽度没有改变,值改变前后是 x%。然而,期望的结果是将宽度从 x 动画化到 0,然后再返回到 x。

到目前为止我发现的所有来源仅区分隐式动画和显式动画。虽然这改变了动画的实现/描述方式,但它似乎并不影响动画的功能。

创建所需的“复杂”动画的“正确”方法是什么?

两个不同的动画?

当然,可以计算是否发生正负之间的切换,并在这种情况下使用

.withAnimation { ... }
触发两个不同的动画。根据值差计算第一个动画和第二个动画的长度是没有问题的。

但是,这仅适用于线性动画。当使用任何缓动功能(如 spring 等)时,使用这种方法不可能实现连续动画。

此外,进度条只是一个示例。如果需要链接更多动画才能获得所需的结果怎么办?

有没有解决方案来创建这样的自定义动画?

ios swift xcode swiftui swiftui-animation
1个回答
0
投票

您可以遵守

Animatable
。将与动画相关的部分提取到符合
ViewModifier
Animatable

var body: some View {
    let maxV = max(minValue, maxValue)
    let minV = min(minValue, maxValue)
    
    let currentV = max(min(abs(currentValue - minValue), maxV), minV)
    let progress = (currentV - minV) / (maxV - minV)
    
    Rectangle()
        .fill(.gray)
        .frame(height: height)
        .overlay {
            Rectangle()
                .modifier(ProgressBarModifier(
                    progress: currentValue < minV ? -progress : progress
                ))
        }
        .animation(.linear(duration: 1), value: progress)
}

struct ProgressBarModifier: ViewModifier, Animatable {
    var progress: Double // between -1 and 1
    
    nonisolated var animatableData: Double {
        get { progress }
        set { progress = newValue }
    }
    
    func body(content: Content) -> some View {
        content
            .foregroundStyle(progress < 0 ? .red : .green)
            // this is clearer than GeometryReader, but GeometryReader should also work
            .scaleEffect(x: abs(progress), anchor: .leading)
    }
}

在动画的每一帧中,SwiftUI 都会将

animatableData
设置为动画起点和终点之间的某个插值。然后它将调用
body
并使用新框架更新 UI。


您也可以使整个

SomeProgressBar
Animatable
保持一致,但随后需要将
animation
修饰符移至父视图。

© www.soinside.com 2019 - 2024. All rights reserved.