欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SwiftUI 动画之 animatableData和VectorArithmeti高级使用

程序员文章站 2024-03-24 12:16:46
...

基础知识
让我们从基础知识开始。通常,我们将动画修改器附加到视图并更改一些状态变量。SwiftUI 文档表示,动画修改器将给定的动画应用于此视图中的所有可动画值。下面是一个小示例,用于为每一次点击按钮设置动画。

import SwiftUI

struct ContentView: View
{
  @State private var scale: CGFloat = 1

    var body: some View {
        Button("可变大的按钮") {
            self.scale += 1
        }
        .padding()
        .foregroundColor(.white)
        .background(Color.blue)
        .cornerRadius(8)
        .scaleEffect(scale)
        .animation(.default)
    }

}
SwiftUI 动画之 animatableData和VectorArithmeti高级使用
Jietu20200618-211805.gif

但是 SwiftUI 如何理解哪些值是可动画的呢?SwiftUI 引入了一种称为可动画的协议。此协议具有描述动画期间更改的唯一要求(可动画数据属性)。因此,在状态更改期间,SwiftUI 遍历视图层次结构,找到符合可动画协议的所有值,并通过了解特定项的可动画数据为其更改设置动画。让我们看一下另一个例子。

struct Line1: Shape {
    let coordinate: CGFloat

    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: .zero)
            path.addLine(to: CGPoint(x: coordinate, y: coordinate))
        }
    }
}

struct RootView: View {
    @State private var coordinate: CGFloat = 0

    var body: some View {
        Line1(coordinate: coordinate)
            .stroke(Color.red)
            .animation(Animation.linear(duration: 1).repeatForever())
            .onAppear { self.coordinate = 100 }
    }
}


在这里,我们有一个符合形状协议的线结构。SwiftUI 中的所有形状都符合可动画协议,但如您所见,在运行示例时没有动画。SwiftUI 不会为我们的行设置动画,因为框架不知道如何为其设置动画。

默认情况下,形状返回 EmptyAnimatableData作为其可动画数据。SwiftUI 允许我们使用 EmptyAnimatableData,每当我们不知道如何为值设置动画时。让我们通过将 EmptyAnimatableData作一些值来解决此问题。



struct RootView: View {
    @State private var coordinate: CGFloat = 0

    var body: some View {
        Line1(coordinate: coordinate)
            .stroke(Color.red)
            .animation(Animation.linear(duration: 1).repeatForever())
            .onAppear { self.coordinate = 100 }
    }
}


struct Line1: Shape {
    var coordinate: CGFloat

    var animatableData: CGFloat {
        get { coordinate }
        set { coordinate = newValue }
    }

    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: .zero)
            path.addLine(to: CGPoint(x: coordinate, y: coordinate))
        }
    }
}

SwiftUI 动画之 animatableData和VectorArithmeti高级使用
Jietu20200618-212550.gif

在上面的示例中,我们通过实现可动画数据属性使 Line 可执行动画。但是,我们如何为同一形状的多个属性设置动画?还有另一种类型称为动画配对,可帮助我们为配对值设置动画。让我们使我们的线在垂直和水平方向上可伸张。

struct RootView: View {
    @State private var coordinate: CGFloat = 0
     @State private var x: CGFloat = 100
     @State private var y: CGFloat = 100

    var body: some View {
        VStack{
        Line1(coordinate: coordinate)
            .stroke(Color.red)
            .animation(Animation.linear(duration: 1).repeatForever())
            .onAppear { self.coordinate = 100 }
        
        Line2(x: x, y: y)
                   .stroke(Color.red)
                   .animation(Animation.linear(duration: 1).repeatForever())
                   .onAppear {
                    self.x = 400
                    self.y = 350
                    
        }
              
    }
    }
}


struct Line1: Shape {
    var coordinate: CGFloat

    var animatableData: CGFloat {
        get { coordinate }
        set { coordinate = newValue }
    }

    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: .zero)
            path.addLine(to: CGPoint(x: coordinate, y: coordinate))
        }
    }
}


struct Line2: Shape {
    var x: CGFloat
    var y: CGFloat

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

    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: .zero)
            path.addLine(to: CGPoint(x: x, y: y))
        }
    }
}

SwiftUI 动画之 animatableData和VectorArithmeti高级使用
Jietu20200618-213329.gif

好的,很好现在,我们可以为两个相同形状的值设置动画。但折线图呢?假设您正在处理图表库,并且想要构建一个可动画的折线图?可能有数百个值要设置动画。我们如何应对这一挑战?

引入VectorArithmetic 协议

正如您所看到的,我们已经使用 CGFloat 和可动画配对类型作为animatable data。但这并不意味着您可以在此处使用任何类型。Animatable protocol对animatableData 属性有限制。任何符合 Vector算术协议的类型都可以用作animatableData 。SwiftUI 为我们提供了几种符合 Vector算术协议开箱即用的类型。例如,浮动、双、CGFloat 和 AnimatablePair。

让我们回到我们的折线图想法。我想制作一个折线图形状,为所有值设置动画。可能有几百个点,它看起来像一个符合 Vector算术协议的自定义类型的优秀候选点。Vector算术有几个要求,如缩放、添加、减法等。您应该描述 SwiftUI 必须如何处理这些类型的操作。下面是一组值的分项实现。

import SwiftUI
import enum Accelerate.vDSP

struct AnimatableVector: VectorArithmetic {
    static var zero = AnimatableVector(values: [0.0])

    static func + (lhs: AnimatableVector, rhs: AnimatableVector) -> AnimatableVector {
        let count = min(lhs.values.count, rhs.values.count)
        return AnimatableVector(values: vDSP.add(lhs.values[0..<count], rhs.values[0..<count]))
    }

    static func += (lhs: inout AnimatableVector, rhs: AnimatableVector) {
        let count = min(lhs.values.count, rhs.values.count)
        vDSP.add(lhs.values[0..<count], rhs.values[0..<count], result: &lhs.values[0..<count])
    }

    static func - (lhs: AnimatableVector, rhs: AnimatableVector) -> AnimatableVector {
        let count = min(lhs.values.count, rhs.values.count)
        return AnimatableVector(values: vDSP.subtract(lhs.values[0..<count], rhs.values[0..<count]))
    }

    static func -= (lhs: inout AnimatableVector, rhs: AnimatableVector) {
        let count = min(lhs.values.count, rhs.values.count)
        vDSP.subtract(lhs.values[0..<count], rhs.values[0..<count], result: &lhs.values[0..<count])
    }

    var values: [Double]

    mutating func scale(by rhs: Double) {
        values = vDSP.multiply(rhs, values)
    }

    var magnitudeSquared: Double {
        vDSP.sum(vDSP.multiply(values, values))
    }
}

如您所见,我使用加速框架,它为我们提供了基于矢量的高性能算术。通过使用可动画矢量结构,我们可以根据需要为尽可能多的值设置动画,并且它将以超快的速度工作,因为它使用加速框架。现在,我们拥有实现可动画折线图形状所需的一切。

import SwiftUI

struct LineChart: Shape {
    var vector: AnimatableVector

    var animatableData: AnimatableVector {
        get { vector }
        set { vector = newValue }
    }

    func path(in rect: CGRect) -> Path {
        Path { path in
            let xStep = rect.width / CGFloat(vector.values.count)
            var currentX: CGFloat = xStep
            path.move(to: .zero)

            vector.values.forEach {
                path.addLine(to: CGPoint(x: currentX, y: CGFloat($0)))
                currentX += xStep
            }
        }
    }
}

完整代码

import SwiftUI

struct ContentView: View
{
  @State private var scale: CGFloat = 1

    var body: some View {
      RootView()
    }

}
import enum Accelerate.vDSP

struct AnimatableVector: VectorArithmetic {
    static var zero = AnimatableVector(values: [0.0])

    static func + (lhs: AnimatableVector, rhs: AnimatableVector) -> AnimatableVector {
        let count = min(lhs.values.count, rhs.values.count)
        return AnimatableVector(values: vDSP.add(lhs.values[0..<count], rhs.values[0..<count]))
    }

    static func += (lhs: inout AnimatableVector, rhs: AnimatableVector) {
        let count = min(lhs.values.count, rhs.values.count)
        vDSP.add(lhs.values[0..<count], rhs.values[0..<count], result: &lhs.values[0..<count])
    }

    static func - (lhs: AnimatableVector, rhs: AnimatableVector) -> AnimatableVector {
        let count = min(lhs.values.count, rhs.values.count)
        return AnimatableVector(values: vDSP.subtract(lhs.values[0..<count], rhs.values[0..<count]))
    }

    static func -= (lhs: inout AnimatableVector, rhs: AnimatableVector) {
        let count = min(lhs.values.count, rhs.values.count)
        vDSP.subtract(lhs.values[0..<count], rhs.values[0..<count], result: &lhs.values[0..<count])
    }

    var values: [Double]

    mutating func scale(by rhs: Double) {
        values = vDSP.multiply(rhs, values)
    }

    var magnitudeSquared: Double {
        vDSP.sum(vDSP.multiply(values, values))
    }
}

struct LineChart: Shape {
    var vector: AnimatableVector

    var animatableData: AnimatableVector {
        get { vector }
        set { vector = newValue }
    }

    func path(in rect: CGRect) -> Path {
        Path { path in
            let xStep = rect.width / CGFloat(vector.values.count)
            var currentX: CGFloat = xStep
            path.move(to: .zero)

            vector.values.forEach {
                path.addLine(to: CGPoint(x: currentX, y: CGFloat($0)))
                currentX += xStep
            }
        }
    }
}

struct RootView: View {
    @State private var vector: AnimatableVector = .zero

    var body: some View {
        LineChart(vector: vector)
            .stroke(Color.red)
            .animation(Animation.default.repeatForever())
            .onAppear {
                self.vector = AnimatableVector(values: [50, 100, 75, 100,120,20,90])
            }
    }
}

推荐

基础文章推荐

经典教程推荐

技术源码推荐

推荐文章

CoreData篇

Combine篇

TextField篇

JSON文件篇


一篇文章系列

技术交流

QQ:3365059189
SwiftUI技术交流QQ群:518696470