原文:Uberworks
作者:Marin Todorov
译者:kmyhy
本教程使用 Xcode 7.1 和 Swift 2。
这个月的主题又回到了这一系列文章的开始。事实上,我正在华盛顿参加 RWDevCon 大会,在会上我将推出 iOS Animations by Tutorials 和这一系列文章。我收到了一封来自 Uber 的主题为 Forget your keys 的邮件,它上面有一个烟火动画获得了我的喜爱。你可以在这篇 tweet 上看到这个动画:
这个动画于是就成为了我待办列表中的一项长达 8 个月之久,现在,是时候用 Core Animation 和 CAReplicatorLayer 来实现它了。
开始
我已经准备了一个开始项目,以节省你新建项目和创建 UI 和自动布局的工作。
下载Uberworks-starter.zip,解压缩,打开 Uberworks.xcodeproj。
这个项目包含了一个空的黑色的 view controller,以及 EasyAnimation 库。(关于 EasyAnimation 库你可以在 6 月份的教程中看到)。
要实现烟火效果,需要我们创建几个动画,在 ViewController.swift 中添加必须的代码。
开始来写代码吧!
创建尾焰
首先来创建烟花开始发射后的尾焰。
在 ViewController 中添加一个助手方法:
func animatedLineFrom(from: CGPoint, to: CGPoint) -> CAShapeLayer {
let linePath = UIBezierPath()
linePath.moveToPoint(from)
linePath.addLineToPoint(to)
}
animatedLineFrom(_:to:) 会创建一个 CAShapeLayer,允许你添加到 layer 树中。这个方法也会在这个 layer 上创建和运行动画,这样你可以直接显示它就行了。
第一段代码中创建了一个贝塞尔路径,代表了你将在屏幕上绘制的形状。对于尾焰来说,我们只需要一条由参数指定的从起点到终点的直线就可以了。
现在,在 aminatedLineFrom(_:to:) 方法底部创建 CALayer:
let line = CAShapeLayer()
line.path = linePath.CGPath
line.lineCap = kCALineCapRound
line.strokeColor = UIColor.cyanColor().CGColor
return line
这段代码创建了一个 CALayer,用于绘制这条线,设置颜色为蓝绿色,然后返回 CALayer。
现在来试试它。在 viewDidLoad() 方法中加入:
view.layer.addSublayer(
animatedLineFrom(CGPoint(x: 160, y: 550), to: view.center)
)
这段代码在屏幕上创建了一条青色的线条:
然后——我们对线条的起点和终点进行动画,创建出一种尾焰效果。回到animatedLineFrom(_:to:) 方法,在 return 之前加入:
line.strokeEnd = 0.0
UIView.animateWithDuration(1.0, delay: 0.0, options: [.CurveEaseOut], animations: {
line.strokeEnd = 1.0
}, completion: nil)
首先将终点设置为起点(也就是线条将变成 0 长度)。然后以动画的方式将终点重设回路径的终点(也就是动画将从 0% 逐渐变成 100%)。
关于 strokeEnd 和 strokeBegin 的内容,请参考 iOS Animations by Tutorials 的第 15 章“笔触和路径动画”。
运行 app,看看效果:
然后给这个动画添加一个在短暂停留后消失的尾迹效果,添加第二个动画:
UIView.animateWithDuration(0.75, delay: 0.9, options: [.CurveEaseOut], animations: {
line.strokeStart = 1.0
}, completion: nil)
这会将线段的起点朝终点移动直到消失:
很好——烟花动画的第一部分完成了!
你的第一支烟花
现在来实现最有趣的部分——焰火的绽放。
焰火的绽放需要用到一个 replicator 图层。如果你没用过 CAReplicatorLayer,建议你阅读 iOS Animations by Tutorials 第 16 章“复制动画”。
在 ViewController 中添加一个新的助手方法,用于创建一个添加到 view 树中的焰火。
func firework1(at atPoint: CGPoint) -> CAReplicatorLayer {
let replicator = CAReplicatorLayer()
replicator.position = atPoint
return replicator
}
这个方法创建了一个空的 replicator layer 并返回它。我们将在这个方法的 return 之前添加动画代码。
首先创建一个线条路径,用于表示绽开后的焰火粒子(和你之前创建的线条路径一样)。在 firework1(at:) 方法中加入:
//line path
let f11linePath = UIBezierPath()
f11linePath.moveToPoint(CGPoint(x: 0, y: -10))
f11linePath.addLineToPoint(CGPoint(x: 0, y: -100))
f11linePath 是表示“第一种焰火的第一条线条路径”。剩下的代码和之前一样。
然后,再次像前面一样,用一个 CAShapeLayer 绘制这条线段:
//line 1
let f11line = CAShapeLayer()
f11line.path = f11linePath.CGPath
f11line.strokeColor = UIColor.cyanColor().CGColor
replicator.addSublayer(f11line)
创建了线条所在的图层,然后设置其颜色为青色,最后将它添加到复制器图层中。
回到 viewDidLoad() 方法,继续添加代码:
delay(seconds: 1.0, completion: {
self.view.layer.addSublayer(self.firework1(at: self.view.center))
})
这将绘出焰火绽放之后的第一缕火焰:
然后,发挥 CAReplicatorLayer 的作用吧,在 firework1at(_:) 方法中追加:
replicator.instanceCount = 20
replicator.instanceTransform = CATransform3DMakeRotation(CGFloat(M_PI/10), 0, 0, 1)
这会复制你的绽放后的光束,将这些复制品排成环状:
猜到了吧?你的下一个动作就是和之前一样添加 strokeEnd 的动画。添加代码:
f11line.strokeEnd = 0
UIView.animateWithDuration(1.0, delay: 0.33, options: [.CurveEaseOut], animations: {
f11line.strokeEnd = 1.0
}, completion: nil)
干得不错,来看看这个爆炸的效果吧:
现在——我们来添加另一种爆炸后的线型。我们将用另外一个形状图层添加到 replicator 图层中,以区别于前面的代码。
加入:
//line path 2
let f12linePath = UIBezierPath()
f12linePath.moveToPoint(CGPoint(x: 0, y: -25))
f12linePath.addLineToPoint(CGPoint(x: 0, y: -100))
f12linePath.applyTransform(CGAffineTransformMakeRotation(CGFloat(M_PI/20)))
我们和之前一样画了一条线,但这次稍微旋转了一个角度——防止它和第一条线重叠。
添加创建第二个形状图层的代码,然后将它添加到复制器图层中:
let f12line = CAShapeLayer()
f12line.path = f12linePath.CGPath
f12line.lineDashPattern = [20, 2]
f12line.strokeColor = UIColor.cyanColor().CGColor
replicator.addSublayer(f12line)
f12line.strokeEnd = 0
和之前一样,创建了这个线条的 CALayer,设置颜色为青色。这次你将线型设置为虚线——虚线的样式为:20 像素的短线,2个像素的间距(也就是,这个图层会先绘制 20 像素的短线,间隔 2 个像素,又绘制另外 20 像素的短线,依次类推)。
然后设置 strokeEnd 为 0,接下来就要进行动画了。添加:
UIView.animateWithDuration(1.0, delay: 0.0, options: [.CurveEaseOut], animations: {
f12line.strokeEnd = 1.0
}, completion: nil)
基本的架子已经打起来了。接下来需要稍微加工一下,让它看起来更好看一些!
首先为它的消失增加一点动效。添加代码:
UIView.animateWithDuration(1.0, delay: 1.0, options: [.CurveEaseIn], animations: {
f11line.strokeStart = 1.0
f12line.strokeStart = 0.5
}, completion: nil)
和之前一样,你对 strokeStart 进行动画,让线段“消失”:
让我们将最后的变化再加一点动画——在最后一个动画块中加入:
f11line.transform = CATransform3DMakeRotation(CGFloat(M_PI_4), 0, 0, 1)
f12line.transform = CATransform3DMakeRotation(CGFloat(M_PI_4/2), 0, 0, 1)
replicator.opacity = 0.0
这段代码对两种线型的线段进行了一点旋转,然后让整个复制图层渐出,制造一种爆炸后原本就有的渐隐效果。
漂亮!这个烟花绽放的效果非常棒,和真实的效果非常接近。
如果你重用这段代码,别忘了动画结束之后,哪怕焰火和图层都看不见,但它们仍然还在图层树中存在,你必须在爆炸结束之后手动移除它们。
你的第二只烟花
现在,为了更好玩,我们将创建第二种类型的烟花,就和你在 Uber 视屏中看到的另一种烟火一样。
首先加一个助手方法,用于创建一个蓝绿色的点:
func animatedDot(withDistance delta: CGFloat, delay: Double) -> CALayer {
let dot = CALayer()
dot.backgroundColor = UIColor.cyanColor().CGColor
dot.frame = CGRect(x: 0, y: -10, width: 1, height: 1)
return dot
}
和之前的助手方法一样,创建一个图层,设置它的 frame、颜色然后返回它。这两个参数现在没有用到,暂不讨论。
接下来是第二个助手方法(和之前干的非常类似):
func firework2(at atPoint: CGPoint) -> CAReplicatorLayer {
let replicator = CAReplicatorLayer()
replicator.position = atPoint
replicator.instanceCount = 40
replicator.instanceTransform = CATransform3DMakeRotation(CGFloat(M_PI/20), 0, 0, 1)
return replicator
}
firework2at(at:) 方法创建了一个空的 CAReplicatorLayer,指定它的位置,然后创建 40 个实例围成环形。
现在尝试着在 replicator layer 中添加一个绿色小店——两个参数都使用 0,因为 animatedDot(_:delay:) 方法还没有用到这两个参数。在 return 之前加入:
replicator.addSublayer(
animatedDot(withDistance: 0, delay: 0)
)
为了让这些代码生效,需要回到 viewDidLoad() 方法,将这句:
self.view.layer.addSublayer(self.firework1(at: self.view.center))
替换成:
self.view.layer.addSublayer(self.firework2(at: self.view.center))
二者唯一的不同是方法名中的“1”变成了“2”。你也可以只修改这个数字就可以了。
这会让第二种焰火呈现:
现在,我们可以将复制动画中的点偏移一小点。找到 animatedDot(_:delay:) 方法,在 return 之前添加:
UIView.animateAndChainWithDuration(1.0, delay: delay, options: [.CurveEaseOut], animations: {
dot.transform = CATransform3DMakeTranslation(0, -delta, 0)
}, completion: nil)
这个动画块中修改了圆点图层的 transform 属性,将它的 Y 坐标移动了 delta 个像素。为了让这个起作用,你必须在调用 animatedDot(_:delay:) 方法时指定 delta 参数的值。在 firework2(_:) 方法中,找到animatedDot(withDistance: 0, delay: 0) 这句,将第一个参数修改为 50:
animatedDot(withDistance: 50, delay: 0)
现在可以来欣赏下这个简单而又漂亮的爆炸效果了:
好了——就快完成了。为这个点添加更多的动画吧。
在 animatedDot(_:delay:) 中添加更多的动画到动画序列中——你的动画代码看起来应该是这个样子:
UIView.animateAndChainWithDuration(1.0, delay: delay, options: [.CurveEaseOut], animations: {
dot.transform = CATransform3DMakeTranslation(0, -delta, 0)
}, completion: nil)
.animateWithDuration(2.0, animations: {
dot.transform = CATransform3DConcat(dot.transform, CATransform3DMakeRotation(CGFloat(M_PI_4), 0, 0, 1))
})
.animateWithDuration(1.0, animations: {
dot.transform = CATransform3DConcat(dot.transform, CATransform3DMakeTranslation(0, -delta, 0))
dot.opacity = 0.1
})
动画序列的第一部分是将圆点进行垂直移动。第二部分给圆点的 transform 属性加一小点旋转角度——那将使它们在爆炸之后所有的点旋转。最后让圆点在飞离的同时渐隐。需要执行的代码都很少:]
看起来很炫的效果!
好了——现在是最后一步:在爆炸中添加更多的点,看看要如何复制出来!
回到 firework2At(_:) 方法找到这段代码:
replicator.addSublayer(
animatedDot(withDistance: 50, delay: 0)
)
用一个循环替换这段代码,创建 10 个点,让它们加一点延迟和偏移距离,创建更复杂的复制动画。将上段代码替换为:
for i in 1...10 {
replicator.addSublayer(
animatedDot(withDistance: CGFloat(i*10), delay: 1/Double(i))
)
}
这将创建 10 个点并将它们添加到复制器中。每个点都会有不同的偏移距离以及不同的时间延迟。不用我多做解释,直接运行 app 看看:
今天就到此结束了!我希望你喜欢这个烟花动画并将它们用在你的 app 里:]
我截取了一段视频,显示了你今天完成的崭新的示例效果。
网页中嵌入的视频有点模糊——如果你想看到色彩逼真的顺滑的动画效果,请在 Vimeo 上打开视频并打开 HD。
接下来做什么?
如果你想了解更多关于笔触动画、复制动画和 EasyAnimation ——这些内容都包含在了 iOS Animation by Tutorials 的章节中。这本书的第二版包含了 3 个全新章节,内容也进行了相应的升级。
如果你购买了第一版的 PDF——别忘了进入你的 raywenderlich.com 的 profile 中,去下载免费升级的第二版!
如果你准备基于本教程的内容编写一些好玩的东东,请回复这封邮件或者 twitter 我 @icanzilb。