实现Uber的启动动画
此篇为译文,若存在纰漏,请见谅。
原文:How To Create an Uber Splash Screen
一个完美的启动动画—通过有趣的动画让开发者不会再为app启动时依赖API返回核心数据而产生的延时问题抓狂。有趣的启动动画(启动画面不再是静态的,无动画的启动画面)会在app中起到十分重要的作用:让用户有耐心等待app的启动。
尽管我们能在很多app中见到启动动画,但是你很难找到一个比Uber漂亮的。在2016年的第一个季度,Uber推出了新版major rebranding strare gy led by its CEO。其中的一个变化就是带来了一个十分酷炫的启动画面。
此篇教程的目的是尽可能地还原Uber的启动动画。其中重度使用 CALayer 及 CAAnimation 类,包括他们的子类。除了介绍这些这些类的概念,本教程会更注重如何使用这些类来构造高质量的动画。想要深入学习这些动画,看这里:Marin Todorov’s Intermediate iOS Animation video series
开始
因为在教程中需要实现大量的动画,所以你将从一个初始工程开始学习,我们已经在这个工程中创建了所有与动画相关的CALayer类的实例。
下载工程
初始项目为一个名为Fuber的app。(译者注:接下来这段话是原文作者卖萌)Fuber提供呼叫Segway(一种独轮电动自行车)司机来接载乘客到城市中的任意一个角落。Fuber发展迅速,现在已经在60多个国家为Segway乘客服务,但是遭到了许多国家政府的反对就像Segway工会反对用户使用Fuber联系Segway司机。:]
教程结束的时候,你会创建出一个如下图的启动动画:
打开并运行Fuber项目,看一看。
从UIViewController角度,app启动 SplashViewController 从父视图控制器-RootContainerViewController:负责控制它的子视图控制器。SplashViewController负责循环启动动画直到app准备好加载。一般这段时间内会去请求API以获取app启动所必须的数据。值得一提的是,在这个简单的示例项目中,启动动画拥有自己的模块。
这里有两个方法在 RootContainerViewController: showSplashViewController() 和 ShowSplashViewControllerNoPing()。此教程的大部分时间,你只需要调用 ShowSplashViewControllerNoPing(),它只会循环启动动画,这样你可以专注于在 SplashViewController中的动画,之后你再会调用 showSplashViewController() 用来模拟请求API的延迟并转场进入主视图控制器。
启动动画的Views与Layers组成
SplashViewController视图中包含两个subview,第一个subview是 TileGridView,它有一个名为“ripple grid”的背景图,它包含了一个格子布局的子视图实例 TileView。另外一个subview由动画视图 ‘U’ icon组成,名为 AnimatedULogoView
AnimatedULogoView包含了4个CAShapeLayer:
- circleLayer 表示“U”的圆形白色背景。
- lineLayer 是一条直线从 circleLayer 的中心延伸到它的边缘。
- squareLayer 是 circleLayer 中心的正方形。
- maskLayer 当其他图层的边界改变时,在一个简单的动画中它被用来统一控制这些图层。
组合起来,这些 CAShaperLayer 创建了Fuber的“U”。
现在你知道了这些图层是怎么组合起来的,是时候去写动画代码让 AnimatedULogoView动起来。
白色圆形背景动画
在实现这些动画的过程中,最好排除外界干扰专注于正在实现的动画,点开 AnimatedULogoView.swift。在 init(frame:) 中,注释掉添加这些 sublayer 除了 circleLayer 的代码。当完成所有动画之后,便会取消这些注释。代码现在应该是这个样子:
override init(frame: CGRect) {
super.init(frame: frame)
circleLayer = generateCircleLayer()
lineLayer = generateLineLayer()
squareLayer = generateSquareLayer()
maskLayer = generateMaskLayer()
// layer.mask = maskLayer
layer.addSublayer(circleLayer)
// layer.addSublayer(lineLayer)
// layer.addSublayer(squareLayer)
}
找到 generateCircleLayer() 去看看这个圆是怎么创建的。它是用 UIBezierPath 创建出来的 CAShapeLayer 图层。注意这一行代码:
layer.path = UIBezierPath(arcCenter: CGPointZero, radius: radius/2, startAngle: -CGFloat(M_PI_2), endAngle: CGFloat(3*M_PI_2), clockwise: true).CGPath
默认情况下,也就是 startAngle 参数为0,贝尔塞曲线(bezier)的路径会从右边开始(3点钟的位置)。当设置为 -M_PI_2 也就是-90°,这个曲线会从圆的正上方开始绘制,因为 endAngle 参数设置为270°及 3M_PI_2*,曲线会在圆的正上方结束绘制。因为你要动画展示这个绘制圆的过程,所以圆的半径 radius 与曲线的线宽 lineWidth 相同。
circleLayer 的动画需要3个 CAAnimation组合起来:一个关键帧动画 CAkeyframeAnimation 绘制圆键值为 strokeEnd,一个转换基础关键帧动画 CABasicAnimation 使圆的形态转换,最后一个为动画组 CAAnimationGroup 用来将前面两个动画组合起来。接下来让我们创建它们。
找到 animateCirleLayer() 添加以下代码:
// strokeEnd
let strokeEndAnimation = CAKeyframeAnimation(keyPath: "strokeEnd")
strokeEndAnimation.timingFunction = strokeEndTimingFunction
strokeEndAnimation.duration = kAnimationDuration - kAnimationDurationDelay
strokeEndAnimation.values = [0.0, 1.0]
strokeEndAnimation.keyTimes = [0.0, 1.0]
通过设置这个动画的 values 为 [0.0,1.0],你会看到一个很cool的类似时钟的动画。当 strokeEnd 的值增加的时候,贝塞尔曲线的长度也跟着圆的周长增加,最后这个圆就被“填满”了。举个特定的例子,假如你将 values 的值设置为 [0.0,0.5],这个动画将只会绘制到一个半圆便结束了,因为 strokeEnd 停止在圆的周长一半的位置。
(译者注:想要看到这一个小动画的效果,可以将这个动画加入 circleLayer 中,添加这一行代码:circleLayer.addAnimation(strokeEndAnimation, forKey: "looping") 后运行工程。)
现在来添加形态转换动画:
// transform
let transformAnimation = CABasicAnimation(keyPath: "transform")
transformAnimation.timingFunction = strokeEndTimingFunction
transformAnimation.duration = kAnimationDuration - kAnimationDurationDelay
var startingTransform = CATransform3DMakeRotation(-CGFloat(M_PI_4), 0, 0, 1)
startingTransform = CATransform3DScale(startingTransform, 0.25, 0.25, 1)
transformAnimation.fromValue = NSValue(CATransform3D: startingTransform)
transformAnimation.toValue = NSValue(CATransform3D: CATransform3DIdentity)
这个动画包含两个部分,一部分是比例(scale)变化,另一部分为z轴上旋转变化。这样 circleLayer 再绘制圆的过程中还会顺时针旋转45°。旋转动画十分重要,它需要配合 lineLayer 图层动画的位置与速度。
最后,添加一个动画组 CAAnimationGroup,这个动画组包含了之前的两个动画,所以你只需要将这个动画组加入到 circleLayer图层即可。
// Group
let groupAnimation = CAAnimationGroup()
groupAnimation.animations = [strokeEndAnimation, transformAnimation]
groupAnimation.repeatCount = Float.infinity
groupAnimation.duration = kAnimationDuration
groupAnimation.beginTime = beginTime
groupAnimation.timeOffset = startTimeOffset
circleLayer.addAnimation(groupAnimation, forKey: "looping")
这个动画组 CAAnimationGroup 有两个值得关注的属性被设置:beginTime 和 timeOffset。如果你对它们都不熟悉,这里有一篇很赞的文章介绍它们以及它们的用途。
这个动画组 groupAnimation的 beginTime 设置参照于它的父视图。
timeOffset是必须要设置的,因为这个动画不是从动画循环的起点开始的。当你完成了更多的动画之后,尝试去修改 startTimeOffset的值并观察动画发生了什么变化。(译者注:关于timeOffset可以这么理解,假如一段动画是一个环,持续时间为5秒,设置timeOffset的值为2秒,那么这个动画循环将从2秒开始到5秒,然后再从0秒到2秒,这样的一个流程)
将这个动画组加到 circleLayer 图层后,运行工程,动画的效果应该如图:
注意:尝试从 groupAnimation.animations 数组中移除 strokeEndAnimation 或者 transformAnimation,来看看每个动画究竟是什么样子的。尽量在本教程中对每一个你创建的动画采用这个方式来预览,你会惊讶于这些动画组合出了你意想不到的效果。
直线动画
已经完成了 circleLayer 动画,接下来我们来解决 lineLayer动画。还是在 AnimatedULogoView.swift,找到 startAnimating() 注释掉调用动画的代码除了 animateLineLayer()。代码看起来应该是如下的样子:
public func startAnimating() {
beginTime = CACurrentMediaTime()
layer.anchorPoint = CGPointZero
// animateMaskLayer()
// animateCircleLayer()
animateLineLayer()
// animateSquareLayer()
}
除此之外,改变 init(frame:) 中的内容,这样我们只添加了 circleLayer 和 lineLayer :
override init(frame: CGRect) {
super.init(frame: frame)
circleLayer = generateCircleLayer()
lineLayer = generateLineLayer()
squareLayer = generateSquareLayer()
maskLayer = generateMaskLayer()
// layer.mask = maskLayer
layer.addSublayer(circleLayer)
layer.addSublayer(lineLayer)
// layer.addSublayer(squareLayer)
}
接下来找到 animateLineLayer() 在实现中添加下一组动画:
// lineWidth
let lineWidthAnimation = CAKeyframeAnimation(keyPath: "lineWidth")
lineWidthAnimation.values = [0.0, 5.0, 0.0]
lineWidthAnimation.timingFunctions = [strokeEndTimingFunction, circleLayerTimingFunction]
lineWidthAnimation.duration = kAnimationDuration
lineWidthAnimation.keyTimes = [0.0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]
这个动画用来控制直线线宽由细到粗再到细的过程。
下一个转换动画,添加:
// transform
let transformAnimation = CAKeyframeAnimation(keyPath: "transform")
transformAnimation.timingFunctions = [strokeEndTimingFunction, circleLayerTimingFunction]
transformAnimation.duration = kAnimationDuration
transformAnimation.keyTimes = [0.0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]
var transform = CATransform3DMakeRotation(-CGFloat(M_PI_4), 0.0, 0.0, 1.0)
transform = CATransform3DScale(transform, 0.25, 0.25, 1.0)
transformAnimation.values = [NSValue(CATransform3D: transform),
NSValue(CATransform3D: CATransform3DIdentity),
NSValue(CATransform3D: CATransform3DMakeScale(0.15, 0.15, 1.0))]
跟 circleLayer 转换动画很像,在这里你定义一个绕着z轴顺时针旋转。对直线而言,首先执行25%的比例变换,紧接着变换成15%(百分比相对于直线原始尺寸而言)。
将上面的两个动画使用一个 CAAnimationGroup 组合起来,并将这个组合动画添加到 lineLayer:
// Group
let groupAnimation = CAAnimationGroup()
groupAnimation.repeatCount = Float.infinity
groupAnimation.removedOnCompletion = false
groupAnimation.duration = kAnimationDuration
groupAnimation.beginTime = beginTime
groupAnimation.animations = [lineWidthAnimation, transformAnimation]
groupAnimation.timeOffset = startTimeOffset
lineLayer.addAnimation(groupAnimation, forKey: "looping")
运行工程,看到这个prettiness(可爱?!)的动画:
请注意你使用 -M_PI_4 初始转换值与画圆动画配合起来。你还需要设置 keyTimes 为 [0.0, 1.0 -kAnimationDurationDelay/kAnimationDuration, 1.0]。这个数组的第一个和最后一个元素的含义很明显:0表示开始,1.0表示结束,中间的元素需要去计算画圆完成的时间紧接着开始缩小动画。用 kAnimationDurationDelay 除 kAnimationDuration 获得准确的百分比,因为这是个延时动画,所以需要用1.0减去这个百分比才是延时时间。
你现在已经完成了 circleLayer 和 lineLayer 动画,是时候实现圆中心的方块动画。
方块动画
接下来你应该很熟悉了,找到 startAnimating() 注释掉调用动画的方法除了 animateSquareLayer()。除此之外,修改 init(frame:) 如下:
override init(frame: CGRect) {
super.init(frame: frame)
circleLayer = generateCircleLayer()
lineLayer = generateLineLayer()
squareLayer = generateSquareLayer()
maskLayer = generateMaskLayer()
// layer.mask = maskLayer
layer.addSublayer(circleLayer)
// layer.addSublayer(lineLayer)
layer.addSublayer(squareLayer)
}
完成之后,找到 animateSquareLayer() 然后开始实现下一个动画:
// bounds
let b1 = NSValue(CGRect: CGRect(x: 0.0, y: 0.0, width: 2.0/3.0 * squareLayerLength, height: 2.0/3.0 * squareLayerLength))
let b2 = NSValue(CGRect: CGRect(x: 0.0, y: 0.0, width: squareLayerLength, height: squareLayerLength))
let b3 = NSValue(CGRect: CGRectZero)
let boundsAnimation = CAKeyframeAnimation(keyPath: "bounds")
boundsAnimation.values = [b1, b2, b3]
boundsAnimation.timingFunctions = [fadeInSquareTimingFunction, squareLayerTimingFunction]
boundsAnimation.duration = kAnimationDuration
boundsAnimation.keyTimes = [0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]
这个特别的动画改变了 CALayer 的边界(bound)。让这个方形的边长从3分之2的长度开始变化到原长最后变为0。
接下来,改变背景颜色的动画:
// backgroundColor
let backgroundColorAnimation = CABasicAnimation(keyPath: "backgroundColor")
backgroundColorAnimation.fromValue = UIColor.whiteColor().CGColor
backgroundColorAnimation.toValue = UIColor.fuberBlue().CGColor
backgroundColorAnimation.timingFunction = squareLayerTimingFunction
backgroundColorAnimation.fillMode = kCAFillModeBoth
backgroundColorAnimation.beginTime = kAnimationDurationDelay * 2.0 / kAnimationDuration
backgroundColorAnimation.duration = kAnimationDuration / (kAnimationDuration - kAnimationDurationDelay)
注意 fillMode 属性,因为 beginTime 不为0,这个动画会固定住开始与结束的颜色,这样添加这个动画进入动画组的时候就不会出现闪烁。
说到这,是时候实现这个动画组了:
// Group
let groupAnimation = CAAnimationGroup()
groupAnimation.animations = [boundsAnimation, backgroundColorAnimation]
groupAnimation.repeatCount = Float.infinity
groupAnimation.duration = kAnimationDuration
groupAnimation.removedOnCompletion = false
groupAnimation.beginTime = beginTime
groupAnimation.timeOffset = startTimeOffset
squareLayer.addAnimation(groupAnimation, forKey: "looping")
运行工程,你现在可以看到如下方块动画的效果:
是时候组合以上实现的动画,看看这些动画组合起来的效果吧!
注意:这些动画在模拟器上显示可能会出现锯齿状边缘,因为是电脑模拟iOS设备的GPU。如果电脑无法实现这些动画效果,尝试切换到一个更小屏幕尺寸的模拟器或者在真机上运行程序。
MaskLayer
首先,取消 init(frame:) 以及 starAnimating() 中被注释的代码。
所有的动画都被添加之后,运行工程:
看起来还是差一点,是吧?有一个突然的闪烁当 circleLayer 的边界(bounds)缩小的时候。幸运的是,mask动画可以去掉这个闪烁,让边界的收缩更加平滑。
找到 animateMaskLayer() 添加以下代码:
// bounds
let boundsAnimation = CABasicAnimation(keyPath: "bounds")
boundsAnimation.fromValue = NSValue(CGRect: CGRect(x: 0.0, y: 0.0, width: radius * 2.0, height: radius * 2))
boundsAnimation.toValue = NSValue(CGRect: CGRect(x: 0.0, y: 0.0, width: 2.0/3.0 * squareLayerLength, height: 2.0/3.0 * squareLayerLength))
boundsAnimation.duration = kAnimationDurationDelay
boundsAnimation.beginTime = kAnimationDuration - kAnimationDurationDelay
boundsAnimation.timingFunction = circleLayerTimingFunction
这个是改变边界的动画。请记住当 maskLayer 的边界改变的时候,整个 AnimateULogoView 都会消失,因为 maskLayer 是最底层的图层。
现在来实现一个 cornerRadius 动画,来保持 maskLayer 边界是一个圆:
// cornerRadius
let cornerRadiusAnimation = CABasicAnimation(keyPath: "cornerRadius")
cornerRadiusAnimation.beginTime = kAnimationDuration - kAnimationDurationDelay
cornerRadiusAnimation.duration = kAnimationDurationDelay
cornerRadiusAnimation.fromValue = radius
cornerRadiusAnimation.toValue = 2
cornerRadiusAnimation.timingFunction = circleLayerTimingFunction
将这两个动画加入动画组中,并将动画组添加到这个图层:
// Group
let groupAnimation = CAAnimationGroup()
groupAnimation.removedOnCompletion = false
groupAnimation.fillMode = kCAFillModeBoth
groupAnimation.beginTime = beginTime
groupAnimation.repeatCount = Float.infinity
groupAnimation.duration = kAnimationDuration
groupAnimation.animations = [boundsAnimation, cornerRadiusAnimation]
groupAnimation.timeOffset = startTimeOffset
maskLayer.addAnimation(groupAnimation, forKey: "looping")
运行工程:
看起来非常好!
网格
一个虚拟边界,想象一连串的 UIView 快速穿过 TileGridView 的画面,好了...是时候停止引用Tron,接着往下看。(译者注:此处可去看看原文。)
这个背景网格包含这一系列的 TileView 并贴在它的父视图 TileGridView 上。想要更直接的理解这句话,打开 TileView.swift 找到 init(frame:) 。加入以下代码:
layer.borderWidth = 2.0
运行工程:
就像你所看到的,TileView们在网格中排列很整齐。实现他们的逻辑在 TileGridView.swift 的 renderTileViews() 中。接下来你需要做的就是让它们动起来。
TileView的动画
TileGridView 只有一个子视图 containerView。它添加了所有子视图 TileView。除此之外,它有一个属性 tileViewRows ,它是个二维数组包含了所有的被加入到 container View 的 tileView。
找到 TileView 的 init(frame:) 方法。删除那行用来显示边界的代码并取消注释添加 chimeSplashImage 到图层的代码。这个方法现在看起来是这样:
override init(frame: CGRect) {
super.init(frame: frame)
layer.contents = TileView.chimesSplashImage.CGImage
layer.shouldRasterize = true
}
运行程序:
Coooooool...We're getting there!
无论如何,TileGridView(包括所有的 TileView) 需要一些动画。点开 TileView.swift,找到 startAnimatingWithDuration(_:beginTime:rippleDelay:rippleOffset:) 添加下一个动画:
let timingFunction = CAMediaTimingFunction(controlPoints: 0.25, 0, 0.2, 1)
let linearFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
let easeOutFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
let easeInOutTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let zeroPointValue = NSValue(CGPoint: CGPointZero)
var animations = [CAAnimation]()
这一段代码声明了几个你即将用到的 TimingFunction变量。添加以下代码:
if shouldEnableRipple {
// Transform.scale
let scaleAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
scaleAnimation.values = [1, 1, 1.05, 1, 1]
scaleAnimation.keyTimes = TileView.rippleAnimationKeyTimes
scaleAnimation.timingFunctions = [linearFunction, timingFunction, timingFunction, linearFunction]
scaleAnimation.beginTime = 0.0
scaleAnimation.duration = duration
animations.append(scaleAnimation)
// Position
let positionAnimation = CAKeyframeAnimation(keyPath: "position")
positionAnimation.duration = duration
positionAnimation.timingFunctions = [linearFunction, timingFunction, timingFunction, linearFunction]
positionAnimation.keyTimes = TileView.rippleAnimationKeyTimes
positionAnimation.values = [zeroPointValue, zeroPointValue, NSValue(CGPoint:rippleOffset), zeroPointValue, zeroPointValue]
positionAnimation.additive = true
animations.append(positionAnimation)
}
shouldEnableRipple 是一个布尔值,它决定着什么时候添加转换与位移动画进入你刚刚创建的 animations 数组中。当 TileView 在 TileGridView 边界以内它的值为 true(译者注:具体逻辑可以看 renderTileViews())。这个逻辑已经被实现,在 TileGridView.swift 的 renderTileViews() 方法中。
接着添加一个 opacity(透明) 动画:
// Opacity
let opacityAnimation = CAKeyframeAnimation(keyPath: "opacity")
opacityAnimation.duration = duration
opacityAnimation.timingFunctions = [easeInOutTimingFunction, timingFunction, timingFunction, easeOutFunction, linearFunction]
opacityAnimation.keyTimes = [0.0, 0.61, 0.7, 0.767, 0.95, 1.0]
opacityAnimation.values = [0.0, 1.0, 0.45, 0.6, 0.0, 0.0]
animations.append(opacityAnimation)
通过 keyTimes 可以很明确的知道这个动画是如何变换透明度的。
现在将上面的动画加入动画组中:
// Group
let groupAnimation = CAAnimationGroup()
groupAnimation.repeatCount = Float.infinity
groupAnimation.fillMode = kCAFillModeBackwards
groupAnimation.duration = duration
groupAnimation.beginTime = beginTime + rippleDelay
groupAnimation.removedOnCompletion = false
groupAnimation.animations = animations
groupAnimation.timeOffset = kAnimationTimeOffset
layer.addAnimation(groupAnimation, forKey: "ripple")
这里将这个动画组加入到 TileView 中,注意到这个动画组可能有一个或三个动画组成,这取决于 shouldEnableRipple 的值。
现在你已经实现了每一个 TileView 的动画,是时候在 TileGridView 中调用它。回过头来看 TileGridView.swift 把下面的代码加入到 startAnimatingWithBeginTime(_:) :
private func startAnimatingWithBeginTime(beginTime: NSTimeInterval) {
for tileRows in tileViewRows {
for view in tileRows {
view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, rippleDelay: 0, rippleOffset: CGPointZero)
}
}
}
运行工程:
Hum...现在看起来更赞了,但是缺一点东西,那就是 AnimatedULogView 放大的时候 TileGridView 要有一种波纹往外阔的效果(译者注:就像扔一个石头进入水中激起的波纹效果)。这意味着每一个 TileView 的动画需要有个延迟,延迟的大小由它与屏幕中心的距离决定。(译者注:这里是译者关于采用延时策略实现简单波浪效果的文章:])
找到 startAnimatingWithBeginTime(_:) ,加入下面这个方法:
private func distanceFromCenterViewWithView(view: UIView)->CGFloat {
guard let centerTileView = centerTileView else { return 0.0 }
let normalizedX = (view.center.x - centerTileView.center.x)
let normalizedY = (view.center.y - centerTileView.center.y)
return sqrt(normalizedX * normalizedX + normalizedY * normalizedY)
}
这个工具方法用来获取 TileView 与中心那个 TileView 中心的距离。
回到 startAnimatingWithBeginTime(_:) ,用以下代码替换掉原来的代码:
for tileRows in tileViewRows {
for view in tileRows {
let distance = self.distanceFromCenterViewWithView(view)
view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, rippleDelay: kRippleDelayMultiplier * NSTimeInterval(distance), rippleOffset: CGPointZero)
}
}
这里使用 distanceFromCenterViewWithView(_:) 方法来计算每个小动画的延时时间。
运行工程:
更赞了!这个启动动画看起有那么一回事了,但是还是存在一些瑕疵。这个波浪动画看起来不是那么波浪,很僵硬不够自然。
现在最好重新拿起你的高中数学(不用担心,很简单的内容),用向量来表示 TileView 与中心的位置关系。
在 distanceFromCenterViewWithView(_:) 加入另外一个方法:
private func normalizedVectorFromCenterViewToView(view: UIView)->CGPoint {
let length = self.distanceFromCenterViewWithView(view)
guard let centerTileView = centerTileView where length != 0 else { return CGPointZero }
let deltaX = view.center.x - centerTileView.center.x
let deltaY = view.center.y - centerTileView.center.y
return CGPoint(x: deltaX / length, y: deltaY / length)
}
回到 startAnimatingWithBeginTime(_:),修改代码如下:
private func startAnimatingWithBeginTime(beginTime: NSTimeInterval) {
for tileRows in tileViewRows {
for view in tileRows {
let distance = self.distanceFromCenterViewWithView(view)
var vector = self.normalizedVectorFromCenterViewToView(view)
vector = CGPoint(x: vector.x * kRippleMagnitudeMultiplier * distance, y: vector.y * kRippleMagnitudeMultiplier * distance)
view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, rippleDelay: kRippleDelayMultiplier * NSTimeInterval(distance), rippleOffset: vector)
}
}
}
这里计算每一个 TileView 与中心的向量,并赋值给 rippleOffset 参数。
运行工程:
Very cool!现在只剩最后一步啦:实现背景似乎要冲出屏幕的动感画面(如下图),一个放大动画需要在 mask 的边界发生变化之前启动。
在 startAnimatingWithBeginTime(_:) 最上方插入以下代码:
let linearTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
let keyframe = CAKeyframeAnimation(keyPath: "transform.scale")
keyframe.timingFunctions = [linearTimingFunction, CAMediaTimingFunction(controlPoints: 0.6, 0.0, 0.15, 1.0), linearTimingFunction]
keyframe.repeatCount = Float.infinity;
keyframe.duration = kAnimationDuration
keyframe.removedOnCompletion = false
keyframe.keyTimes = [0.0, 0.45, 0.887, 1.0]
keyframe.values = [0.75, 0.75, 1.0, 1.0]
keyframe.beginTime = beginTime
keyframe.timeOffset = kAnimationTimeOffset
containerView.layer.addAnimation(keyframe, forKey: "scale")
运行程序:
Beautiful! You have now created a production-quality animation that many Fuber users will complain about on Twitter. Great job! :](翻译略.....)
提示:去尝试修改 kRippleMagnitudeMultiplier 和 kRippleDelayMultiplier 的值并看看会有哪些变化。
为了完成整个启动流程,点开 RootContainerViewController.swift。在 viewDidLoad() 中,将最后一行代码 showSplashViewControllerNoPing() 改为 showSplashViewController()。
再次运行工程,欣赏下你的成果吧:
是不是很cool...一个完美的启动动画!
后话
你可以在这里下载完整的工程。
如果你想要学习更多关于动画的知识,看iOS Animations by Tutorials
译者注:整个教程还是比较清晰易懂的,有什么纰漏及疑惑的地方可以撩下我哈!