基础知识
让我们从基础知识开始。通常,我们将动画修改器附加到视图并更改一些状态变量。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 如何理解哪些值是可动画的呢?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))
}
}
}
在上面的示例中,我们通过实现可动画数据属性使 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))
}
}
}
好的,很好现在,我们可以为两个相同形状的值设置动画。但折线图呢?假设您正在处理图表库,并且想要构建一个可动画的折线图?可能有数百个值要设置动画。我们如何应对这一挑战?
引入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篇
- 《SwiftUI 一篇文章全面掌握TextField文本框 (教程和全部源码)》
- 《SwiftUI实战之TextField风格自定义与formatters》
- 《SwiftUI实战之TextField如何给键盘增加个返回按钮(隐藏键盘)》
- 《SwiftUI 当键盘出现时避免TextField被遮挡自动向上移动》
- 《SwiftUI实战之TextField如何给键盘增加个返回按钮(隐藏键盘)》
JSON文件篇
一篇文章系列
- SwiftUI一篇文章全面掌握List(教程和源码)
- 《SwiftUI 一篇文章全面掌握TextField文本框 (教程和全部源码)》
- SwiftUI一篇文章全面掌握Picker,解决数据选择(教程和源码)
- SwiftUI一篇文章全面掌握Form(教程和源码)
- SwiftUI Color 颜色一篇文章全解决
技术交流
QQ:3365059189
SwiftUI技术交流QQ群:518696470
- 请关注我的专栏icloudend, SwiftUI教程与源码
https://www.jianshu.com/c/7b3e3b671970