文章源地址:[https://swiftui-lab.com/swiftui-animations-part1/)
作者: Javier
翻译: Liaoworking
本文我们将要深度探究一下SwiftUI的动画,也会广泛的讨论 Animatable protocol和其经常一起出现的animatableData, 功能强大但经常被忽略的GeometryEffect, 还有完全被忽略但强大的AnimatableModifier协议。
这些内容在官方文档中都没有写,在SwiftUI相关的文章中也没有怎么提及。不过苹果爸爸还是提供给我们一些创建炫酷动画的工具。
在我们探究宝藏之前,先对本页的SwiftUI动画概念做一个很快的总结。稍待片刻。
完整代码在此:
[https://gist.github.com/swiftui-lab/e5901123101ffad6d39020cc7a810798](https://gist.github.com/swiftui-lab/e5901123101ffad6d39020cc7a810798)
例子8所需要的图片和其他资源从这里下载:
[https://swiftui-lab.com/?smd_process_download=1&download_id=916](https://swiftui-lab.com/?smd_process_download=1&download_id=916)
显式和隐式动画
SwiftUI中有两种对动画类型,显式和隐式
隐式就是你用.animation()
指定的动画,无论View哪个可以做动画的参数改变了,就会有动画效果。如:size, offset, color, scale等。
显式动画就是用withAnimation { ... }
闭包修饰的,只有闭包中的可动参数改变了才会执行动画,来几个例子演示一下。
下面的例子用的隐式动画
来改变图片的大小和透明度。
struct Example1: View {
@State private var half = false
@State private var dim = false
var body: some View {
Image("tower")
.scaleEffect(half ? 0.5 : 1.0)
.opacity(dim ? 0.2 : 1.0)
.animation(.easeInOut(duration: 1.0))
.onTapGesture {
self.dim.toggle()
self.half.toggle()
}
}
}
下面的例子就是显示动画,透明度和缩放都改变的时候,但只有透明度会做动画,因为只有这一个参数在withAnimation
闭包中。
struct Example2: View {
@State private var half = false
@State private var dim = false
var body: some View {
Image("tower")
.scaleEffect(half ? 0.5 : 1.0)
.opacity(dim ? 0.5 : 1.0)
.onTapGesture {
self.half.toggle()
withAnimation(.easeInOut(duration: 1.0)) {
self.dim.toggle()
}
}
}
}
注意,我们可以通过隐式动画来达到同样的效果,只要改变修改器(modifiers)的位置即可。
struct Example2: View {
@State private var half = false
@State private var dim = false
var body: some View {
Image("tower")
.opacity(dim ? 0.2 : 1.0)
.animation(.easeInOut(duration: 1.0))
.scaleEffect(half ? 0.5 : 1.0)
.onTapGesture {
self.dim.toggle()
self.half.toggle()
}
}
}
你如果想要关闭动画,可以使用.animation(nil)
。
动画是怎么实现的
在所有的SwiftUI动画背后,都有一个叫做Animatable
的协议,它包括一个遵循VectorArithmetic(矢量运算)
协议的计算型属性。这使得系统可以随意插值。
当一个视图做动画的时候,SwiftUI已经很多次的生成视图了。每次都会修改动画参数,这样动画参数就可以从原始值逐渐变成最终值。
假设我们让一个视图的透明度做线性动画,从0.3到0.8, 系统就会很多次的生成新的视图,一点一点增加透明度,由于透明度是Double类型的,而且Double遵循了VectorArithmetic
协议,SwiftUI就能插入所需要的透明度。在系统的某处,可能就会有下面的计算方法。
let from:Double = 0.3
let to:Double = 0.8
for i in 0..<6 {
let pct = Double(i) / 5
var difference = to - from
difference.scale(by: pct)
let currentOpacity = from + difference
print("currentOpacity = \(currentOpacity)")
}
这段代码会逐渐将值从原始值改变到最终值。
currentOpacity = 0.3
currentOpacity = 0.4
currentOpacity = 0.5
currentOpacity = 0.6
currentOpacity = 0.7
currentOpacity = 0.8
为什么关注动画?
你可能想知道,为什么会关注这些细节。SwiftUI设置不透明的动画,就是改变不透明度,就只是简单的在初始值和最终值之间插值,但是我们接下来看到的并非这么简单。
先说几个比较大的概念:path(路径)、transform matrices(矩阵变换) 和 arbitrary view changes(任意视图更改, 例如文本框中的文字,渐变中的颜色数组或者中转点 等)。在这个例子中,系统并不知道要做什么,没有任何关于A到B的提前预行为,我们在第二第三部分将要讨论矩形变换和视图变化, 现在先关注一下shapes(形状)
动画的形状路径
假如你有一个多边形,是由path(路径)绘制出来的,我们可以实现一个指定多边形的图形
PolygonShape(sides: 3).stroke(Color.blue, lineWidth: 3)
PolygonShape(sides: 4).stroke(Color.purple, lineWidth: 4)
image
下面是多边形
的实现, 我用了一些三角学的知识,并不是本文需要必须掌握的,但如果你想要学习的话,你可以在我的"SwiftUI中的三角学"中了解更多。
struct PolygonShape: Shape {
var sides: Int
func path(in rect: CGRect) -> Path {
// hypotenuse
let h = Double(min(rect.size.width, rect.size.height)) / 2.0
// center
let c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0)
var path = Path()
for i in 0..<sides {
let angle = (Double(i) * (360.0 / Double(sides))) * Double.pi / 180
// Calculate vertex position
let pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h))
if i == 0 {
path.move(to: pt) // move to first vertex
} else {
path.addLine(to: pt) // draw line to next vertex
}
}
path.closeSubpath()
return path
}
}
我们可以更近一步,尝试和之前透明度变化动画相同的使用方法。
PolygonShape(sides: isSquare ? 4 : 3)
.stroke(Color.blue, lineWidth: 3)
.animation(.easeInOut(duration: duration))
你觉得SwiftUI是怎样将三角形转化成四边形的,系统其实也没有思路去做动画,你可能碰到动画就.animation() 但是这里三角形只会马上跳到四边形。原因很简单,SwiftUI知道如何绘制三角形和四边形,但是它不知道如何绘制3.379边形。
所以,想要有动画效果,我们需要两件东西:
1.我们需要去改变Shape相关的代码,因为它知道如何绘制非整数边形。
2.让系统通过增加可动参数去多次生成形状,我们希望形状被绘制多次,每次都有不同的边数值:3, 3.1, 3.15, 3.2, 3.25 一直到4.
一旦我们做到了这些,我们就可以做到任何边数中间的动画切换。
生成动画数据
想让图形动起来,我们需要SwiftUI使用初始值到最终值之间的所有值来多次渲染视图,幸运的是,Shape(图形)
已经遵守了Animatable
协议,这意味着我们可以用它们的计算型属性animatableData
来处理。
默认它是实现的,但只是设置的是EmptyAnimatableData
, 啥也没做。
为了解决我们的问题,先把Sides从Int改成Double类型,这样我们就有了小数类型,后面再讨论怎么把这个属性维护成Int类型,还可以做动画,这里为了简单先改成Double。
struct PolygonShape: Shape {
var sides: Double
...
}
这时候我们再创建计算型属性 animatableData, 这里就很简单了
struct PolygonShape: Shape {
var sides: Double
var animatableData: Double {
get { return sides }
set { sides = newValue }
}
...
}
用小数去画多边形的边
最后,我们需要告诉SwiftUI怎么用小数去画多边形的边,我们将稍微改变一下我们的代码,当小数部分增长的时候,新的边将会从0变成到真正的长度,其他顶点将相应的平滑定位。听起来很复杂,不过只是稍微改动一下代码。
func path(in rect: CGRect) -> Path {
// 斜边
let h = Double(min(rect.size.width, rect.size.height)) / 2.0
// 中心
let c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0)
var path = Path()
let extra: Int = Double(sides) != Double(Int(sides)) ? 1 : 0
for i in 0..<Int(sides) + extra {
let angle = (Double(i) * (360.0 / Double(sides))) * Double.pi / 180
// 计算顶点
let pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h))
if i == 0 {
path.move(to: pt) // 移动到第一个顶点
} else {
path.addLine(to: pt) // 画到下一个顶点的线
}
}
path.closeSubpath()
return path
}
完整的代码写在了文章顶部的gist file中例1中。
像之前提及到的,好像边长数为Double会很奇怪,按道理应该是Int类型的,幸运的是,我们可以在Shape的实现中完善一下代码。
struct PolygonShape: Shape {
var sides: Int
private var sidesAsDouble: Double
var animatableData: Double {
get { return sidesAsDouble }
set { sidesAsDouble = newValue }
}
init(sides: Int) {
self.sides = sides
self.sidesAsDouble = Double(sides)
}
...
}
这样的话,我们外面使用的是Int,内部使用的是Double, 现在看起来就更优雅一些了,用sidesAsDouble
来代替sides
,完整的代码在文章顶部的gist中的 Example2 里。
不止一个参数的动画
我们经常会发现需要给不止一个参数设置动画,我们可以使用AnimatablePair<First, Second>
,这里的First
和Second
都要遵循VectorArithmetic
,例如AnimatablePair<CGFloat, Double>
.
为了展示AnimatablePair的使用,对例子稍加改造,现在我们的多边形将有两个参数:sides
和scale
。两个都用Double来表示。
struct PolygonShape: Shape {
var sides: Double
var scale: Double
var animatableData: AnimatablePair<Double, Double> {
get { AnimatablePair(sides, scale) }
set {
sides = newValue.first
scale = newValue.second
}
}
...
}
完整代码可以在文章顶部的gist中的例3中找到,例4也在里面,甚至有更复杂的path,它们的形状相对,但是多了一条线,把每个顶点相互连接起来。
<video width='500px' autoplay preload='true' controls src="https://swiftui-lab.com/wp-content/uploads/2019/08/example4.mp4">
</video>
两个以上的动画参数
如果你有翻阅SwiftUI的声明文件,你就会发现框架中很多地方都用到了AnimatablePair
,例如CGSize
,CGPoint
,CGRect
. 虽然这些类型都没有遵守VectorArithmetic
,不过他们都可以做动画,因为它们遵循了Animatable
协议。
它们都在以一种或者多种形式使用AnimatablePair
,
extension CGPoint : Animatable {
public typealias AnimatableData = AnimatablePair<CGFloat, CGFloat>
public var animatableData: CGPoint.AnimatableData
}
extension CGSize : Animatable {
public typealias AnimatableData = AnimatablePair<CGFloat, CGFloat>
public var animatableData: CGSize.AnimatableData
}
extension CGRect : Animatable {
public typealias AnimatableData = AnimatablePair<CGPoint.AnimatableData, CGSize.AnimatableData>
public var animatableData: CGRect.AnimatableData
}
如果你仔细观察CGRect,你就会发现实际上它也有在使用:
AnimatablePair<AnimatablePair<CGFloat, CGFloat>, AnimatablePair<CGFloat, CGFloat>>
这也就意味着 矩形的x,y,width,height的值都可以通过first.first
, first.second
, second.first
和 second.second
来获得。
让你自己的类型可动(使用矢量运算)
下面这些类型都遵循 Animatable: Angle(角)
, CGPoint, CGRect, CGSize, EdgeInsets, StrokeStyle(线型) 和 UnitPoint(单位点)。
下面这些类型都遵循的 矢量计算:AnimatablePair(动画配对), CGFloat, Double, EmptyAnimatableData(空白动画数据) 和 Float。
你可以运用上面的类型来让你的图形做动画。
现有的类型已经足够有足够的灵活性来支持任何动画了,------------------
为了说明这一点,我们创建一个闹钟的样子,它将根据它的可变参数类型:ClockTime
来移动它的指针。
<video width='500px' autoplay preload='true' controls src="https://swiftui-lab.com/wp-content/uploads/2019/08/clock.mp4">
</video>
最终会以这样使用:
ClockShape(clockTime: show ? ClockTime(9, 51, 15) : ClockTime(9, 55, 00))
.stroke(Color.blue, lineWidth: 3)
.animation(.easeInOut(duration: duration))
我们先创建我们自定义类型 ClockTime. 它包括三个属性(hours, minutes and seconds),一些有用的初始化方法,和一些计算型属性和方法。
struct ClockTime {
var hours: Int // Hour needle should jump by integer numbers
var minutes: Int // Minute needle should jump by integer numbers
var seconds: Double // Second needle should move smoothly
// Initializer with hour, minute and seconds
init(_ h: Int, _ m: Int, _ s: Double) {
self.hours = h
self.minutes = m
self.seconds = s
}
// Initializer with total of seconds
init(_ seconds: Double) {
let h = Int(seconds) / 3600
let m = (Int(seconds) - (h * 3600)) / 60
let s = seconds - Double((h * 3600) + (m * 60))
self.hours = h
self.minutes = m
self.seconds = s
}
// compute number of seconds
var asSeconds: Double {
return Double(self.hours * 3600 + self.minutes * 60) + self.seconds
}
// show as string
func asString() -> String {
return String(format: "%2i", self.hours) + ":" + String(format: "%02i", self.minutes) + ":" + String(format: "%02f", self.seconds)
}
}
现在为了遵循 VectorArithmetic 协议,我们需要写如下的方法和计算型属性。
extension ClockTime: VectorArithmetic {
static var zero: ClockTime {
return ClockTime(0, 0, 0)
}
var magnitudeSquared: Double { return asSeconds * asSeconds }
static func -= (lhs: inout ClockTime, rhs: ClockTime) {
lhs = lhs - rhs
}
static func - (lhs: ClockTime, rhs: ClockTime) -> ClockTime {
return ClockTime(lhs.asSeconds - rhs.asSeconds)
}
static func += (lhs: inout ClockTime, rhs: ClockTime) {
lhs = lhs + rhs
}
static func + (lhs: ClockTime, rhs: ClockTime) -> ClockTime {
return ClockTime(lhs.asSeconds + rhs.asSeconds)
}
mutating func scale(by rhs: Double) {
var s = Double(self.asSeconds)
s.scale(by: rhs)
let ct = ClockTime(s)
self.hours = ct.hours
self.minutes = ct.minutes
self.seconds = ct.seconds
}
}
最后要做的就是正确定位指针的位置,完整代码在文章顶部gist的Example5里面。
SwiftUI + Metal
在创建复杂动画的时候可能会发现稍微会有一些卡顿,运用Metal可以让你解决这个问题。 这里有一个例子来看Metal到底有多流畅。
<video width='500px' autoplay preload='true' controls src="https://swiftui-lab.com/wp-content/uploads/2019/08/metal-comparison.mov">
</video>
模拟器上可能感觉不到差压,但在真机上可能会感觉到,视频是从2016版iPad上录制的,完整的代码你可以在gist的 Example6中找到。
幸运的是很简单就可以使用Metal, 你只需要在后面添加一个.drawingGroup() 修饰器。
FlowerView().drawingGroup()
在WWDC2019 Session 237中(Building Custom Views with SwiftUI)中讲到:绘图组是一个特殊的仅限于图形的渲染方式。 它可以把SwiftUI视图展平成单个NSView/UIView 并用Metal去渲染,你可以直接跳转到WWDC视频的37:27来获得一些更多细节。
如果你也想尝试一下,但是你的动画还不够复杂,还不能出现轻微的卡顿,可以加一个渐变和阴影,就会马上发现不同。
下面要讲什么
在系列文章的第二部分,我们将要学习到怎么用GeometryEffect协议,这将给你打开一扇新的大门来改变视图和做动画。和之前学的Paths一样。SwiftUI也没有任何关于不同矩阵转换的说明。(GeometryEffect)几何效果将会十分有用。
目前SwiftUI还没有关键帧的功能,我们即将看到如何通过基本动画来模拟出来。
在文字的第三部分,我们将介绍AnimatableModifier(动画修饰器),这是个非常强大的功能,可以让视图中任何改变以动画的形式呈现,甚至是Text!,三篇文章的动画内容都在下面的视频中了。
<video width='500px' autoplay preload='true' controls src="https://swiftui-lab.com/wp-content/uploads/2019/08/animations.mp4">
</video>
高级SwiftUI动画
可以在Twitter上关注我来确保获取更多的内容。 欢迎评论。如果你想有新的文章出来的时候收到提醒,下面有链接。
https://swiftui-lab.com/
网友评论