原文:How to Create a Complex Loading Animation in Swift
时至今日,iOS 应用商店已经拥有超过了140万 应用,让你自己的应用脱颖而出确实是个不小的挑战。不过,在你的应用掉入默默无闻的大黑洞之前,你拥有一个小小的机遇窗,它能帮你吸引用户的注意。
想让你的用户喝彩尖叫,没有比应用加载界面更好的地方 ,在这个地方,你可以添加一个讨人喜欢的动画来作为你登陆或者认证流程的先导。
在这个教程中,你将要学会如何利用先进的技术来创建一个流畅并且迷人的动画。
开始吧!!
从这里下载启动项目,保存在一个合适的路径并用Xcode打开。
打开 HolderView.swift 。 在这个UIView 的子类中,你可以添加些子层(在Layers的下级目录中可以找到),使之像上面的动画一样生动
OvalLayer.swift: 这是第一层,它从零尺寸扩展,然后会有一小段时间的摇摆
TriangleLayer.swift: 接下来的这个层TriangleLayer会在OvalLayer 摇摆的时候出现,当此视图转动时,OvalLayer 会缩小到零尺寸,并在TriangleLayer 中消失。
RectangleLayer.swift: 这个层是TriangleLayer 用于分类的可视化容器
ArcLayer.swift: 这个层动画特效填充在RectangleLayer 中,这和杯子里填充了水(效果)非常相似
打开OvalLayer.swift, 启动项目已经包含了用于初始化这个层的代码和所有你会在动画里用到的Bezier path(对象)。你会看到expand(),wobble()和contract()方法都是空的。你可以通过参考这个指导书来填充这些方法。所有其他的 *Layer (以layer结尾)的文件都用了相似的方式构建。
注意:如果你想要学习更多的Bezier paths,那就检出我们系列指导书 Modern Core Graphics with Swift
最后,打开ViewController.swift 查看addHolderView()方法,这个方法添加了一个HolderView 作为一个子视图,放到viewcontroller 视图的中间。这个视图将会放置所有的动画。viewcontroller仅仅需要把它放到屏幕上,这个视图将会照管好现行的动画代码。
animateLabel() 是由类 HolderView 提供的代理回调函数,此类中你会用你完成的动画序列来填充。addButton()方法只是添加一个按钮到视图中,用于触摸和重启动画。
编译并运行你的应用;你会看到一个空白屏幕。一个空白的画布--这就是用于开始创建你的新动画的完美的载体。在指导书的最后,你的应用会看起来是这样的:
现在不需再费周折,我们开始吧
添加一个椭圆
这个动画从一个椭圆开始,椭圆是从屏幕中间扩展到视图,然后在周围有点摇摆。
打开 HolderView.swift ,在 HolderView 类的顶端附近声明如下的常量
let ovalLayer = OvalLayer()
现在在此类的底部添加如下方法
func addOval() { layer.addSublayer(ovalLayer) ovalLayer.expand() }
这段代码首先添加了你上面创建的 OverLayer 的实例作为一个子层到视图层,然后调用 expand() 方法,这是被切掉的需要你来填充的函数之一
来到 OvalLayer.swift 文件,添加如下代码到 expand() 中:
func expand() { var expandAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path") expandAnimation.fromValue = ovalPathSmall.CGPath expandAnimation.toValue = ovalPathLarge.CGPath expandAnimation.duration = animationDuration expandAnimation.fillMode = kCAFillModeForwards expandAnimation.removedOnCompletion = false addAnimation(expandAnimation, forKey: nil) }
这个函数创建了一个 CABasicAnimation 的实例,这个实例用于改变椭圆从 ovalPathLarge.到 ovalPathSmall 的路径。启动项目为你提供了两者的Bezier paths。
设置动画的 removedOnCompletion 的值为 false,fillMode 的值为 KCAFillModeForwards ,使得当动画结束的时候,椭圆保留它新的路径。
最后,打开 ViewController.swift ,在 view.addSubview(holderView) 下的 addHolderView() 方法中添加如下的线条
holderView.addOval()
将 holdview 添加到 ViewController 的视图中后,调用 addOval 方法来启动动画
构建并运行你的应用,你的动画现在就会看起来像下面(图例)
摇动椭圆
使用视图中扩张的椭圆,下一步就是在椭圆的步调中设置一些反弹,使之摇摆起来
打开 HolderView.swift,在此类的底部,添加下面的函数
func wobbleOval() { ovalLayer.wobble() }
在 OvalLayer 中调用被切掉的方法 wobble().
现在打开 OverLayer.swift,在 wobble() 中添加如下代码
func wobble() { // 1 var wobbleAnimation1: CABasicAnimation = CABasicAnimation(keyPath: "path") wobbleAnimation1.fromValue = ovalPathLarge.CGPath wobbleAnimation1.toValue = ovalPathSquishVertical.CGPath wobbleAnimation1.beginTime = 0.0 wobbleAnimation1.duration = animationDuration // 2 var wobbleAnimation2: CABasicAnimation = CABasicAnimation(keyPath: "path") wobbleAnimation2.fromValue = ovalPathSquishVertical.CGPath wobbleAnimation2.toValue = ovalPathSquishHorizontal.CGPath wobbleAnimation2.beginTime = wobbleAnimation1.beginTime + wobbleAnimation1.duration wobbleAnimation2.duration = animationDuration // 3 var wobbleAnimation3: CABasicAnimation = CABasicAnimation(keyPath: "path") wobbleAnimation3.fromValue = ovalPathSquishHorizontal.CGPath wobbleAnimation3.toValue = ovalPathSquishVertical.CGPath wobbleAnimation3.beginTime = wobbleAnimation2.beginTime + wobbleAnimation2.duration wobbleAnimation3.duration = animationDuration // 4 var wobbleAnimation4: CABasicAnimation = CABasicAnimation(keyPath: "path") wobbleAnimation4.fromValue = ovalPathSquishVertical.CGPath wobbleAnimation4.toValue = ovalPathLarge.CGPath wobbleAnimation4.beginTime = wobbleAnimation3.beginTime + wobbleAnimation3.duration wobbleAnimation4.duration = animationDuration // 5 var wobbleAnimationGroup: CAAnimationGroup = CAAnimationGroup() wobbleAnimationGroup.animations = [wobbleAnimation1, wobbleAnimation2, wobbleAnimation3, wobbleAnimation4] wobbleAnimationGroup.duration = wobbleAnimation4.beginTime + wobbleAnimation4.duration wobbleAnimationGroup.repeatCount = 2 addAnimation(wobbleAnimationGroup, forKey: nil) }
代码真够多的。但断句还是很讲究的。 接下来要做的是:
1. 从大路径下降到被垂直压扁的动画
2. 从垂直压扁变成水平和垂直都压扁
3. 和垂直挤压(动画)切换
4. 回到大路径结束动画
5. 把你所有的动画合并到CAAnimationGroup组,并把这个动画组添加到你的 OvalLayout 中。
每一个随后的动画的 beginTime 都是其前一个动画和动画持续时间的 beginTime 总和。你重复动画组两次就会给你一种摆动出稍微拉长的感觉
尽管你现在拥有产生摇摆动画的所有代码,你还是不能调用你的新动画
我们回到 HolderView.swift,在 addOval() 结尾处添加如下代码
NSTimer.scheduledTimerWithTimeInterval(0.3, target: self, selector: "wobbleOval", userInfo: nil, repeats: false)
在这里,你创建了一个timer定时器,它会在OvalLayer已经结束扩张后调用 wobbleOval()
编译并运行你的应用,检查下你的新动画。
这有点微妙,但那对一个真正的明快的动画是一个重要的因素。你不再需要那些满屏幕都是乱飞的东西了。
开始变身
是时候来电有趣的东西了。你将要把一个椭圆变身成为一个三角形。在用户眼里,这个转变应该看上去无缝连接的。要做到这些,你会用到两个相同颜色的分离的形状。
打开HolderView.swift,在HolderView类的顶端稍微靠近你早些时候添加的 OvalLayer 属性的下面添加如下代码
let triangleLayer = TriangleLayer()
这里声明了一个 TriangleLayer 类的常量,正如你在 OvalLayer 中做的一样
现在,让wobbleOval()方法看上去像这样:
func wobbleOval() { // 1 layer.addSublayer(triangleLayer) // Add this line ovalLayer.wobble() // 2 // Add the code below NSTimer.scheduledTimerWithTimeInterval(0.9, target: self, selector: "drawAnimatedTriangle", userInfo: nil, repeats: false) }
上面的代码做了如下这些事情:
-
这行(代码)添加了一个 TiangleLayer 实例,这个实例在稍早的时候作为HolderView层的子层已经被初始化过了。
-
正如你所知道的,因为这个摇摆动画在1.8s的总间隔时间内运行两次,所以在中间点启动变形过程会是一个非常好的地方。因此,你要添加一个定时器timer,它在延迟0.9s之后执行drawAnimatedTriangle()
注意:找到动画的正确的间隔或延迟需要反复实验,这也是一个好的动画和一个极好的动画区别。我鼓励你去修补你的动画,让它们看上去完美。这可能要花点时间,但确是值得的。
接下来,在此类的底部添加如下的函数。
func drawAnimatedTriangle() { triangleLayer.animate() }
这个方法会被你刚刚加入到 wobbleOval() 中的timer定时器调用。
现在打开 TriangleLayer.swift,添加如下代码到 animate()
func animate() { var triangleAnimationLeft: CABasicAnimation = CABasicAnimation(keyPath: "path") triangleAnimationLeft.fromValue = trianglePathSmall.CGPath triangleAnimationLeft.toValue = trianglePathLeftExtension.CGPath triangleAnimationLeft.beginTime = 0.0 triangleAnimationLeft.duration = 0.3 var triangleAnimationRight: CABasicAnimation = CABasicAnimation(keyPath: "path") triangleAnimationRight.fromValue = trianglePathLeftExtension.CGPath triangleAnimationRight.toValue = trianglePathRightExtension.CGPath triangleAnimationRight.beginTime = triangleAnimationLeft.beginTime + triangleAnimationLeft.duration triangleAnimationRight.duration = 0.25 var triangleAnimationTop: CABasicAnimation = CABasicAnimation(keyPath: "path") triangleAnimationTop.fromValue = trianglePathRightExtension.CGPath triangleAnimationTop.toValue = trianglePathTopExtension.CGPath triangleAnimationTop.beginTime = triangleAnimationRight.beginTime + triangleAnimationRight.duration triangleAnimationTop.duration = 0.20 var triangleAnimationGroup: CAAnimationGroup = CAAnimationGroup() triangleAnimationGroup.animations = [triangleAnimationLeft, triangleAnimationRight, triangleAnimationTop] triangleAnimationGroup.duration = triangleAnimationTop.beginTime + triangleAnimationTop.duration triangleAnimationGroup.fillMode = kCAFillModeForwards triangleAnimationGroup.removedOnCompletion = false addAnimation(triangleAnimationGroup, forKey: nil) }
这段代码使三角层TriangleLayer的角一个挨一个的被弹拉成为椭圆 OvalLayer 层的摆动。Bezier path已经作为启动工程的一部分被定义好。左边的角首先执行,接下来是右边的角,最后是上面的。你完成这个(动画)需要借助创建三个基于路径的CABasicAnimation类的实例, CABasicAnimation 类已经被你添加到 CAAnimationGroup 组中,而组则被放到了 TriangleLayer 中。
构建并运行你的应用,看看当前动画的状态.
完成变形
为了完成变形过程,你需要在缩小OvalLayer椭圆层的同时,对 HolderView 旋转360度,让 TriangleLayer 三角层单独隔离出来。
打开 HolderView.swift,在 drawAnimatedTriangle(): 尾部添加如下代码
NSTimer.scheduledTimerWithTimeInterval(0.9, target: self, selector: "spinAndTransform", userInfo: nil, repeats: false)
这里设置了一个定时器timer,用于在三角形动画结束后触发。0.9s的时间还次用反复实验来确定
现在在这个类的底部添加如下的函数。
func spinAndTransform() { // 1 layer.anchorPoint = CGPointMake(0.5, 0.6) // 2 var rotationAnimation: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") rotationAnimation.toValue = CGFloat(M_PI * 2.0) rotationAnimation.duration = 0.45 rotationAnimation.removedOnCompletion = true layer.addAnimation(rotationAnimation, forKey: nil) // 3 ovalLayer.contract() }
你之前创建的定时器添加了这段代码,定时器会在椭圆停止摆动并且三角行的角出现的时候调用这个函数。在这里我们看下这个函数更详细的(介绍)
-
更新层的锚点到略微靠近视图中间的下方。这提供了一个看上去更加自然的旋转。这是由于椭圆和三角形事实上比视图中心在垂直方向上略微偏移。因此,如果视图围绕中心旋转,椭圆和三角形可能会垂直方向移动
-
应用一个CABasicAnimation类来对层做360度旋转,或者2*pi的弧度。旋转是围绕着Z轴,Z轴就是穿过屏幕,垂直于屏幕平面的轴
-
在OvalLayer中调用contract()来展示动画,这个动画会削减椭圆的尺寸直到消失
现在打开 OvalLayer.swift,添加如下代码到 contract() 方法
func contract() { var contractAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path") contractAnimation.fromValue = ovalPathLarge.CGPath contractAnimation.toValue = ovalPathSmall.CGPath contractAnimation.duration = animationDuration contractAnimation.fillMode = kCAFillModeForwards contractAnimation.removedOnCompletion = false addAnimation(contractAnimation, forKey: nil) }
这段代码应用 CABasicAnimation 类,将 OvalLayer 设置它的初始路径 ovalPathSmall。
构建并运行你的应用程序,当动画完成的时候,只有三角形应该被留在屏幕上。
绘制容器
在下面这部分,你将要绘画一个矩形容器,用于创建一个闭合圈。你将会用到 RectangleLayer 的描边属性。你需要这样做两次,将红色和蓝色都作为描边色。
打开 HolderView.swift, 像下面这样声明两个 RectangularLayer 常量,(位置)就在你稍早时候 triangleLayer 属性的下面
let redRectangleLayer = RectangleLayer()let blueRectangleLayer = RectangleLayer()
接下来添加如下代码到 spinAndTransform(): 的尾部。
NSTimer.scheduledTimerWithTimeInterval(0.45, target: self, selector: "drawRedAnimatedRectangle", userInfo: nil, repeats: false) NSTimer.scheduledTimerWithTimeInterval(0.65, target: self, selector: "drawBlueAnimatedRectangle", userInfo: nil, repeats: false)
这里创建两个定时器timer分别调用 drawRedAnimatedRectangle() 和 drawBlueAnimatedRectangle() 。旋转动画结束后,首先需要画出矩形,当红色矩形描边绘画接近完成的时候,蓝色矩形描边开始。
添加下面两个方法头此类的底部
func drawRedAnimatedRectangle() { layer.addSublayer(redRectangleLayer) redRectangleLayer.animateStrokeWithColor(Colors.red) } func drawBlueAnimatedRectangle() { layer.addSublayer(blueRectangleLayer) blueRectangleLayer.animateStrokeWithColor(Colors.blue) }
一旦你添加矩形层 RectangleLayer 作为 HolderView 的子层,你就要调用 animateStrokeWithColor(color:) 并通过适当的颜色来绘画出边线。
现在打开 RectangleLayer.swift, 像下面这样填充 animateStrokeWithColor(color:)
func animateStrokeWithColor(color: UIColor) { strokeColor = color.CGColor var strokeAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd") strokeAnimation.fromValue = 0.0 strokeAnimation.toValue = 1.0 strokeAnimation.duration = 0.4 addAnimation(strokeAnimation, forKey: nil) }
这段代码通过添加一个 CABasicAnimation对象,在 RectangleLayer 矩形层周围绘画了一个描边。CAShapeLayer 的 strokeEnd 的 key(也就是keyPath)指示了在路径周围多远的距离停止描边。通过将这个属性值从0调到1,你会产生一种路径被从开始到结束都被绘画的错觉。 而从1到0,将会产生整个路径被抹去的错觉。
编译并运行你的应用,查看两个描边是如何看起来像他们构建的容器的。
填充容器
动画的下一步就是填充容器。你要寻找到的效果就像是水填充到玻璃杯中。这是个非常棒的视觉特效,使之为一个大的飞溅特效
打开 HolderView.swift,在 RectangleLayer 属性稍靠下添加如下的常量
let arcLayer = ArcLayer()
现在在drawBlueAnimatedRectangle():尾部添加如下的代码
NSTimer.scheduledTimerWithTimeInterval(0.40, target: self, selector: "drawArc", userInfo: nil, repeats: false)
这(段代码)创建了一个定时器,用于当蓝色 RectangleLayer 完成绘画后调用 drawArc()
在类的结尾添加如下的函数
func drawArc() { layer.addSublayer(arcLayer) arcLayer.animate() }
这段代码是在你动画填充之前,添加了上面已经创建ArcLayer 的实例对象到HolderView 层。
打开ArcLayer.swift 然后添加如下代码到animate():
func animate() { var arcAnimationPre: CABasicAnimation = CABasicAnimation(keyPath: "path") arcAnimationPre.fromValue = arcPathPre.CGPath arcAnimationPre.toValue = arcPathStarting.CGPath arcAnimationPre.beginTime = 0.0 arcAnimationPre.duration = animationDuration var arcAnimationLow: CABasicAnimation = CABasicAnimation(keyPath: "path") arcAnimationLow.fromValue = arcPathStarting.CGPath arcAnimationLow.toValue = arcPathLow.CGPath arcAnimationLow.beginTime = arcAnimationPre.beginTime + arcAnimationPre.duration arcAnimationLow.duration = animationDuration var arcAnimationMid: CABasicAnimation = CABasicAnimation(keyPath: "path") arcAnimationMid.fromValue = arcPathLow.CGPath arcAnimationMid.toValue = arcPathMid.CGPath arcAnimationMid.beginTime = arcAnimationLow.beginTime + arcAnimationLow.duration arcAnimationMid.duration = animationDuration var arcAnimationHigh: CABasicAnimation = CABasicAnimation(keyPath: "path") arcAnimationHigh.fromValue = arcPathMid.CGPath arcAnimationHigh.toValue = arcPathHigh.CGPath arcAnimationHigh.beginTime = arcAnimationMid.beginTime + arcAnimationMid.duration arcAnimationHigh.duration = animationDuration var arcAnimationComplete: CABasicAnimation = CABasicAnimation(keyPath: "path") arcAnimationComplete.fromValue = arcPathHigh.CGPath arcAnimationComplete.toValue = arcPathComplete.CGPath arcAnimationComplete.beginTime = arcAnimationHigh.beginTime + arcAnimationHigh.duration arcAnimationComplete.duration = animationDuration var arcAnimationGroup: CAAnimationGroup = CAAnimationGroup() arcAnimationGroup.animations = [arcAnimationPre, arcAnimationLow, arcAnimationMid, arcAnimationHigh, arcAnimationComplete] arcAnimationGroup.duration = arcAnimationComplete.beginTime + arcAnimationComplete.duration arcAnimationGroup.fillMode = kCAFillModeForwards arcAnimationGroup.removedOnCompletion = false addAnimation(arcAnimationGroup, forKey: nil) }
这个动画和之前的摇摆动画很相似。你创建了一个 CAAnimationGroup 动画组,动画组中包含五个基于路径的 CABasicAnimation 实例对象。
每个路径因高度递增而有了稍微不同的弧,这些路径也是启动项目的一部分。最后,将 CAAnimationGroup 动画组应用到层中,并使得动画组在完成的时候不会被移除,因而当动画完成的时候,它依然保留了自己的状态。
构建并运行你的应用,看看这个神奇的展开吧。
完成动画
剩下要做的就是扩展蓝色的HolderView视图来填充整个屏幕,并且添加一个UILabel作为一个logo添加到视图中
打开 HolderView.swift,在drawArc() 的结尾添加如下代码
NSTimer.scheduledTimerWithTimeInterval(0.90, target: self, selector: "expandView", userInfo: nil, repeats: false)
这(段代码)创建了一个定时器,用于在 ArcLayer 填充到容器后调用 expandView()
现在,添加下面的函数到同一个类的底部:
func expandView() { // 1 backgroundColor = Colors.blue // 2 frame = CGRectMake(frame.origin.x - blueRectangleLayer.lineWidth, frame.origin.y - blueRectangleLayer.lineWidth, frame.size.width + blueRectangleLayer.lineWidth * 2, frame.size.height + blueRectangleLayer.lineWidth * 2) // 3 layer.sublayers = nil // 4 UIView.animateWithDuration(0.3, delay: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { self.frame = self.parentFrame }, completion: { finished in self.addLabel() }) }
代码分析
-
HolderView视图的背景设置为蓝色,和你填充到矩形的颜色匹配
-
帧扩展到你稍早时候添加的RectangleLayer矩形层的描边宽度,
-
所有的子层都移除。现在没有了椭圆,没有了三角形,没有了矩形图层
-
添加动画,并扩张HolderView填充屏幕,当动画结束的时候,调用addLabel().
在类的底部,添加如下函数
func addLabel() { delegate?.animateLabel() }
这里只是简单的调用视图的代理函数,展示label标签。
现在打开ViewController.swift,添加如下代码到animateLabel():
func animateLabel() { // 1 holderView.removeFromSuperview() view.backgroundColor = Colors.blue // 2 var label: UILabel = UILabel(frame: view.frame) label.textColor = Colors.white label.font = UIFont(name: "HelveticaNeue-Thin", size: 170.0) label.textAlignment = NSTextAlignment.Center label.text = "S" label.transform = CGAffineTransformScale(label.transform, 0.25, 0.25) view.addSubview(label) // 3 UIView.animateWithDuration(0.4, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.1, options: UIViewAnimationOptions.CurveEaseInOut, animations: ({ label.transform = CGAffineTransformScale(label.transform, 4.0, 4.0) }), completion: { finished in self.addButton() }) }
依次带入各个注释段
-
从视图中移除HolderView ,并设置视图的背景颜色为蓝色。
-
创建一个文本为"S"的UIlabel标签对象,用于展示logo,并添加到视图。
-
标签对象使用一个弹性动画来使之伸缩。一旦动画结束,调用 addButton() 来添加一个按钮到视图中,当按钮按下的时候,重复动画。
构建并运行应用程序,给自己点个赞,花个时间来欣赏自己构建的动画吧。
下一步
你可以从这里 下载 最终完整的项目。
这个指导书包含了相当多的不一样的动画技术,当这些动画都堆叠在一起的时候,能够创造一个相当复杂的加载动画,这确实能够让你的应用在第一次被(用户)运行的时候就眼前一亮。
从这里,放松自由的玩玩不一样的(动画 的)定时和形状,看看你能组装成哪些很酷的动画
如果你想让你新发现的动画技术提升一个档次,那我建议你看下我们的(这本)书iOS Animations by Tutorials.
我