怎么用Swift创建一个复杂的加载动画

如何用Swift创建一个复杂的加载动画

现在在苹果应用商店上有超过140万的App,想让你的app事件非常具有挑战的事情。你有这样一个机会,在你的应用的数据完全加载出来之前,你可以通过一个很小的窗口来捕获用户的关注。

怎么用Swift创建一个复杂的加载动画

没有比这个更好的地方让用户大为赞叹,当你的应用程序载入画面的时候,你可以添加一个愉快的动画,作为先导,以加载你的应用。

在本教程中,你将学习如何做出这样的动画。你将学习如何建立piece-by-piece,采用前卫的技术来创建一个流体和迷人的动画。


准备

下载本教程开始的工程,把它保存到一个方便的地方,并在Xcode中打开。

打开 HolderView.swift。在这个UIView的子类中,添加下面的子层在动画的上面展示:

  • OvalLayer.swift:这是第一层,从0慢慢变大,然后很短的时间抖动
  • TriangleLayer.swift:OvalLayer层正在抖动的时候出现,当这层旋转的时候,OvalLayer收缩到没有,只让TriangleLayer可视
  • RectangleLayer.swift:这层作为TriangleLayer可视的容器
  • ArcLayer.swift:这层填充RectangleLayer通过一个动画,类似于玻璃中充满水的样子

打开OvalLayer.swift,最初的项目已经包含了初始化这些层和所有在你的动画中将使用的贝塞尔路径的代码。你会看到expand(),wobble()contract()这些方法里面都是空的,通过这个教程你将填充这些方法。所有其他的*Layer 的文件都是类似的功能结构。

Note:如果你想学习关于贝塞尔路径的知识,可以点击这个教程 Modern Core Graphics with Swift

最后,打开ViewController.swift,看到addHolderView(),这个方法中添加了HolderView的实例对象,作为控制器视图的中心的子视图。这个视图将包含所有的动画。该视图控制器只需要将它放在屏幕上,然后该视图将执行实际的动画的代码。

animateLabel()方法是有HolderView类提供的代理回调,当你完成动画的次序时,你将会填充它。

addButton()仅仅是在视图上增加了一个按钮,以便您可以点击并重新启动动画。

构建并运行该项目,你应该看到一个空白的屏幕。这是件完美的事情在空白的画布上开始制作你新的动画!

在教程的最后,你的app将会是下面这样的:

怎么用Swift创建一个复杂的加载动画

因此,事不宜迟,让我们开始吧!


添加椭圆

动画开始有个红色的椭圆形,然后扩展到视图的中心,然后微微摇晃。

打开HolderView.swift,然后在HolderView上方附近定义下面的常量:

let ovalLayer = OvalLayer()

添加下面的方法:

func addOval() {
  layer.addSublayer(ovalLayer)
  ovalLayer.expand()
}

你第一次添加的OvalLayer的实例作为视图层的子视图。

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实例,来改变椭圆的路径从ovalPathSmallovalPathLarge。初始的项目中为你提供了这些贝塞尔路径。设置removedOnCompletionfalsefillMode的值为KCAFillModeForwards,在动画中一旦动画完成,让椭圆开辟一个新的路径。

最后,打开ViewController.swift,添加下面那一行到addHolderView(),就在view.addSubview(holderView)的下面:

holderView.addOval()

addOval在椭圆被添加到控制器的视图中后开始动画。

构建并运行你的app,你的动画将会显示下面这样:

怎么用Swift创建一个复杂的加载动画


晃动椭圆

在椭圆被添加到视图中并扩展后,下一步就是在步骤中加一些反弹,让其摆动。

打开HolderView.swift ,然后添加下面的方法:

func wobbleOval() {
  ovalLayer.wobble()
}

这个方法是让其在OvalLayer中摆动。

打开 OvalLayer.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)

这里创建了一个定时器,在OvalLayer完成扩展动画后,调用wobbleOval() 

构建并运行你的app,看下现在的动画:

怎么用Swift创建一个复杂的加载动画

这是非常微妙的,但这是一个非常重要的因素去创建一个真正令人愉快的动画。你不需要让其充满整个屏幕!


开始变形

即将看到神奇的事情!你即将把椭圆变成三角形。在用户的眼中,这个转变应该看上去是完全无缝的。那么你需要使用两个颜色相同的单独的形状来实现它。

打开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) 
}

这段代码做了下面这些事情:

1、该行添加了前面初始化的TriangleLayer实例,作为HolderView层里的一个子层

2、之前知道了摆动动画运行两次的总共时间是1.8,中间的地方将会是一个非常好的地方去开始变形处理。因此,添加一个计时器,在延迟0.9后,执行drawAnimatedTriangle()方法。

Note:在动画中寻找中一个正确的持续时间或者延迟时间需要一些实验和错误的尝试,然后才能发现一个好的动画和完美的动画的区别。我鼓励你去和你的动画去鼓捣,让它们变得更完美。这可能需要一些时间,但它是值得的。


接下来,添加下面的功能:

func drawAnimatedTriangle() {
  triangleLayer.animate()
}

这个方法是实现刚刚写的定时器的功能,它让三角形开始动。

现在,打开 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)
}

这段代码让三角形的角伴随着OvalLayer层的晃动一个一个出来;在最初的项目中贝塞尔路径已经定义了每个角。最左边的角第一个出来,接着是右边的,最后是最上面的那个角。通过创建三个基于CABasicAnimation的实例,添加到CAAnimationGroup中。

构建并运行你的app,将会看到目前的动画状态;当椭圆晃动的时候,三角形的每个角开始出来知道三个角全部显示出来,就像这样:

怎么用Swift创建一个复杂的加载动画


完成变形

要实现其变形,你需要让HolderView360度旋转当OvalLayer收缩的时候,最终仅留下TriangleLayer

打开HolderView.swift ,添加下面的代码在drawAnimatedTriangle()的最后:

NSTimer.scheduledTimerWithTimeInterval(0.9, target: self, selector: "spinAndTransform", 
                                       userInfo: nil, repeats: false)

在三角形动画完成后设置一个定时器,0.9秒的这个时间也是一次次通过尝试而得到的。

现在,添加下面的方法:

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()
}

刚刚创建的定时器一旦椭圆晃动停止并且三角形的每个角都出现时调用这个函数。下面看下这个函数的详细功能:

1、更新该层的锚点略低于视图的中心。这让旋转闲的更加自然。这是因为,椭圆和三角形事实上以垂直方向从视图的中心偏离。因此,如果视图绕着中心旋转,那么椭圆和三角似乎是垂直移动的。

2、CABasicAnimation让图层旋转360度或者2π弧度。旋转是绕着z轴,垂直于屏幕的表面进入和移出。

3、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)
}
这是让OvalLayer返回最开始的路径上ovalPathSmall,通过执行一个CABasicAnimation动画。这是跟之前调用的动画是相反的。

构建并运行你的app;一旦动画结束后,就只剩下三角形在屏幕的中间。

怎么用Swift创建一个复杂的加载动画


绘制容器

在接下来的部分中,你将会绘制一个长方形的容器来创建一个外壳。要做到这一点,你需要使用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)
这里你创建了两个定时器分别调用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去绘制一个笔。CAShapeLayer中的strokeEnd指示着画笔去描绘这个矩形框。

构建并运行你的app,将会看到下面的动画:

怎么用Swift创建一个复杂的加载动画


填充容器

现在有了容器,下一步就是将它填补起来。这个效果类似于水填充满玻璃,这是一个伟大的诗视觉效果!

打开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()
}

在执行动画之前,这里增加了一个在HolderView层上已经创建了的ArcLayer的实例。

打开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,包含5个基于路径的CABasicAnimation实例。每个路径随着高度的增加都有一个稍微不同的弧度。最后,将CAAnimationGroup应用到图层中,并指示着它不被移除,在动画完成之前将保持这个状态。

构建并运行你的app,将会看到神奇的一幕!

怎么用Swift创建一个复杂的加载动画


完成动画

剩下的事情就是将蓝色的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()
    })
}

该方法做了写什么事情呢:

1、holder view的背景被设置为蓝色,和你之前填充的矩形框的颜色一样;

2、这个frame扩大到之前设定的RectangleLayer画笔的宽度;

3、所有的子层都被移除。现在没有椭圆,没有三角形,没有矩形图层;

4、添加一个动画,让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()
    })
}

依次注释每个部分:

1、将HolderView从视图上移除,并设置视图的背景颜色为蓝色;

2、创建UILabel,内容为“S”,作为logo,添加到视图中;

3、执行一个弹簧动画显示那个Label。一旦动画执行完,调用addButton() 方法,在视图中添加一个按钮,当点击时,重复显示该动画过程。

构建并运行你的app,给自己鼓掌吧,然后花点时间来享受自己实现了什么!

怎么用Swift创建一个复杂的加载动画


你可以到这里下载完整的项目

由于第一次翻译外文,可能很多缺陷和不足,求各路大神指正!无比感谢!原文地址

另外,获取更多的iOS开发的相关资料、资讯、课程,可关注iOS开发者开发者公众平台!

怎么用Swift创建一个复杂的加载动画