如何制作一个类似Uber的溅落式启动屏

如何制作一个类似Uber的溅落式启动屏

1.前言

本文翻译自 How To Create an Uber Splash Screen, 由 Derek Selander 发表于Raywenderlich。

受限于译者英语水平及翻译经验,译文容难免有词不达意,甚至翻译错误的地方,还望不吝赐教予以指正 。

一个好的溅落式启动页,使开发人员有机会再真是动画期间,从后端获取必要的数据.同时它在应用启动期间让用户始终保持高昂兴趣方面也发挥了重要作用.

虽然溅落式启动页已广泛存在,但是你很难找到一个如Uber这版出色的. 在2016年的首季,Uber释放出一个由CEO领导的品牌重塑战略,品牌重塑的成果之一,便是一个非常炫酷的溅落式启动页.

本文以仿制Uber启动动画为目标,其中运用了大量的CALayer和CAAnimation类,及其相应子类. 相对于概念介绍,本文更着重于如何运用这些类去实现一个产品级的动画效果. 如果需要了解动画背后的相关知识,请访问Marin Todorov的系列视频教程:这里写链接内容

2. 开始

鉴于本文设计的动画很多,这里提供一个已经为后续动画创建好所有CALayer的起始工程.

起始工程是一个叫做Fuber的应用,Fuber提供(Segway)驾乘共享服务,乘客通过向Segway驾驶员发出请求,来邀请其搭载自己抵达城市的任何地方。Fuber发展迅速,已在60多个国家为用户提供服务,但也面临众多国家的反对和工会要求其必须与司机签订合同的问题。:](原作者卖萌了)

最终,我们会创建一个如下的非常炫酷的溅落式启动页:
这里写图片描述

打开并运行起始工程,简单浏览一下工程结构。

首先从视图控制器开始,应用通过负责子视图(切入)切出任务的RootContainerViewController加载SplashViewController。父视图控制器从启动页开始运行,直至应用的所有准备工作全部完成。这期间应用会连接到后端,获取后续所需数据。需要指出的是,在这个简单的项目中启动页被设计成了一个独立的模块。

在RootContainerViewController中已经实现好了两个方法:showSplashViewController()和 showSplashViewControllerNoPing()。 由于教程中大部分时间,都在调用showSplashViewControllerNoPing()方法(调试启动动画),所以我们先将精力放在SplashViewController的子视图动画创建上,然后在通过showSplashViewController()模拟一个访问API的延迟效果,并随即跳转到主视图控制器。

3.溅落式启动页视图及其图层结构

SplashViewController的视图(view)包含两个子视图(subview)。 第一个子视图是用于构成波纹网格背景的TileGridview,它包含了一系列按网格排列的TileView实例。另一个子视图名为AnimatedULogoView,它构成了 U 字型的动画图标。

这里写图片描述

AnimatedULogoView包含4个CAShapeLayer:

  • circleLayer 用于实现字母 U 的白色背景
  • lineLayer 用于实现从circleLayer的中心到边缘的一条线段
  • squareLayer 用于实现位于circleLayer中心位置的方块
  • maskLayer 用作视图遮罩,通过改变其bounds的动画效果,来将其它所有图层的动画效果整齐划一地混合起来。

通过组合这几个CAShaperLayer动画,共同实现了Fuber中字母 U 的动画效果。
这里写图片描述

了解了图层的构成之后,接下来我们就来添加一些动画让AnimatedULogoView动起来吧。

4.让圆形动起来

创建复杂动画的关键,在于排除视觉干扰专注于我们正在实现的部分。 打开AnimatedULogoView.swift文件。找到init(frame:)方法,注释掉除circleLayer外其它向视图中添加子图层(sublayer)的方法,完成动画后会再将其全部添加回来。注释完成后的代码如下:

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 或使用默认值, 弧线会从右侧(3点钟位置)开始。传入 -M_PI_2 即 -90度, 则会从顶部开始,如果 endAngle 恰好是270度即 3 * M_PI_2,弧线则再次回到顶点(形成一个圆形)。注意为了绘制的动画效果,我们使用圆形的半径作为lineWidth。

circleLayer的动画需要三个CAAnimation子类来实现:一个作用于stokeEnd的CAKeyframeAnimation动画,一个作用于transform的CABasicAnimation动画,和一个负责将两部分动画组合起来的CAAnimationGroup。这将一次性同时创建所有动画。

在事先写好的animateCircleLayer()方法中添加如下代码:

  // 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,我们便透过Core Animation框架生成了一个从 startAngle 到 endAngle 沿顺时针旋转的动画。随着 strokeEnd 属性值的增加,弧线沿着圆周慢慢伸展,圆形也渐渐被”填满”。在这个例子中,如果我们将values属性的值设为[0.0, 0.5],则仅会画半个圆,这是因为 StrokeEnd 在动画结束时刚达好到圆周的一半。

译者注:“圆形也渐渐被‘填满’”一句的填满是引起来的,并不是真的被填满,而是描边的 lineWidth 与圆形半径相同,从而产生了填满的视觉效果。可参考generateCircleLayer()方法中layer.fillColor = UIColor.clear.cgColor这段代码,事实上填充色被设置为透明.

现在添加形变(transform)动画:

// 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)

该动画同时实现了放大和沿 Z 轴旋转的两个形变。这使得圆形在沿顺时针旋转45度的同时逐渐变大。这里的旋转很重要,因为圆形的旋转要与lineLayer和其它图层一块动起来时的位置和速度保持一致。

最后在animateCircleLayer()方法的最下面添加一个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 设置为与父视图相同。

对timeOffeset的设置是必要的,因为动画首次运行时实际上是从一半开始的。当完成更多动画效果后,你可以试着改变startTimeOffset的值,并观察动画在视觉效果上的不同。

将动画组添加到circleLayer之后,编译并运行应用,检查下动画效果.

这里写图片描述

注意: 试着删除groupAnimation.animations数组中的strokeEndAnimation或transformAnimation,以确认每个动画具体实现了哪些视觉效果. 可以按该方法再去验证一下文中的其它动画,你会惊讶于,仅仅改变动画的组合方式就可以产生如此令人难以预料的独特视觉效果.

5. 让线段动起来

完成了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
  // Swift 3.0 keyTimes是一个NSNumber数组
  lineWidthAnimation.keyTimes = [0.0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]

该动画会使lineLayer的宽度(width)呈现出先增后减的效果。

再为接下来的动画添加如下代码:

  // 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")

编译并运行,注意观察变化.

这里写图片描述

注意我们设置了相同的初始形变值-M_PI_4,以便线段(line)和圆形(circle)在绘制时能对齐。为此我们还将keyTimes 设置为[0.0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]。 数组中首尾两个元素是确定的: 0 代表动画开始那一刻,1.0 代表动画结束那一刻,然后通过计算来获取圆形绘制刚刚结束、第二部分的动画即将开始时的那一刻。由于它是一个延迟的动画效果,所以我们还需要从 1.0 中减去通过kAnimationDurationDelay除以kAnimationDuration而得到的确切百分比,这是因为我们想让动画在结束后的延迟过程中再返回到起点。(译者:形成一个循环动画,否则会出现跳跃,致使动画不连贯)

circleLayer和lineLayer动画都已完成,接下来我们该完成中间的方块动画了。

6.让方块动起来

与之前类似。 在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的大小(bounds)。创建一个先将其边长缩小到2/3,再恢复,最终在缩小到零的关键帧动画。

接下来,为背景色添加动画效果:

// 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不为零时, 动画就会在起始点和结束点保持住当前颜色(CGColor)。这避免了动画在被添加到父CAAnimationGroup时出现闪烁。(译者:这里译的不好:(。请试着改变该属性的设置,看看视觉效果上有什么不同,以加深理解。)

了解了这些,我们就动手来实现一下吧:

 // 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")

编译并运行检查动画效果。注意观察方块的变化。

这里写图片描述

现在将所有的动画组合起来看看效果如何!

注意: 在电脑的GPU完成对iOS设备的模拟任务前,模拟器上的动画可能会有那么一点小抽。如果你的电脑带不动动画,可以试着将模拟器窗口调小或者转到真机开发。

7. 遮罩

首先,取消init(frame:)方法中对所有添加图层方法的注释,以及startAnimating()方法中对所有动画调用的注释.

组合好所有动画后,再次编译并运行。

这里写图片描述

看上去还是有点怪怪的,是不是?圆形在缩小时,它的边缘会有一个小跳跃。幸运地是, 遮罩动画可以解决该问题,让所有子图动画平滑整齐划一.

转到`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

这是一个边界(bounds)动画。记住,由于这是一个应用于所有子图层的遮罩,当边界发生变化时, 整个AnimatedULogoView都将消失,直至遮罩被应用到所有子图层。

现在在添加一个让方块变圆的圆角动画:

 // cornerRadius
  let cornerRadiusAnimation = CABasicAnimation(keyPath: "cornerRadius")
  cornerRadiusAnimation.beginTime = kAnimationDuration - kAnimationDurationDelay
  cornerRadiusAnimation.duration = kAnimationDurationDelay
  cornerRadiusAnimation.fromValue = radius
  cornerRadiusAnimation.toValue = 2
  cornerRadiusAnimation.timingFunction = circleLayerTimingFunction

将这两个动画添加到一个CAAnimationGroup中,以完成这个图层(的所有动画):

// 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")

编译并运行。

这里写图片描述

看起来好多了!

8.网格

试想一下有一系列以TileGridView实例的方式来移动的 UIView。 它们看起来会是什么样呢?呃。。。这里就不引用创并展开说明了!(译者:《创》是一部科幻电影。这里翻译的不好,见谅!)

网格背景由一些列附加到TileGridView类的TileView组成。为了便于从视觉上理解这个概念, 我们打开TileView.swift文件,找到init(frame:)方法,在方法的最后添加如下代码:

layer.borderWidth = 2.0

编译并运行应用。

这里写图片描述

如果你所见,TileView被整齐地排成一张网格。整个创建逻辑都集中在TileGridView.swift文件的renderTileViews()方法内。幸运的是,我们所需的布局逻辑(起始工程)已经实现好。接下来要做的就是让它动起来!

9.让瓦片视图(TileView)动起来

TileGridView仅有一个直接的子视图(subview)containerView。它负责添加所有的TileView。 此外,还有一个名为tileViewRows的属性, 它是一个二维数组,包含所有添加到containerView中的TileView。

回到TileView中的init(frame:)方法. 删除我们刚才添加的用于显示边界的代码,并取消向图层中添加chimeSplashImage方法的注释。完成后的方法如下:

override init(frame: CGRect) {
  super.init(frame: frame)
  layer.contents = TileView.chimesSplashImage.CGImage
  layer.shouldRasterize = true
}

这里写图片描述

酷。。。。我们就要大功告成了。

然而,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]()

这段代码设置了一系列我们即将用到的时间函数。继续添加下面的代码:

 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是个布尔值,用于控制何时将形变动画和位置动画添加到我们刚刚创建的数组中。在通过renderTileViews()方法创建时,所有未处在TileGridView外围边缘的TileView,就已将shouldEnableRipple设为true。

添加一个不透明动画:

 // 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")

这会将groupAnimation添加到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)
    }
  }
}

编译并运行。

这里写图片描述

嗯。。。看上去已经好多了,但AnimatedULogoView的跳动应该通过TileView向外产生一个类似水波的涟漪效果。这就意味还需要创建一个,基于从中央视图(view)到外围视图之间距离的,用于与一个常数相乘的延迟系数。

紧挨着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)中心点之间的距离。

回到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应该像水波一样,向四周逐渐扩散开来。

解决该问的最好方法就是拿出自己的高中数学知识,然后根据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)
    }
  }
}

这会通过 rippleOffset 计算位于每个顶点(vector)的 TileView 的偏移量。

编译运行一下.

这里写图片描述

太棒了! 接下来是点睛之笔:添加一个放大的效果,这个放大的动画效果要刚好在遮罩边界(bounds)发生改变之前。

在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")

再次编译并运行。

这里写图片描述

漂亮,我们已经创建了一个产品级的动画效果,会有大一批的Fuber用户在微博(Twitter)上为此点赞的!:](作者又卖了个萌!)

注意:试着改变kRippleMagnitudeMultiplier和kRippleDelayMultiplier的值,看看会有什么有趣的事发生。

接下收尾,在RootContainerViewController.swift文件中,将viewDidLoad()最后一行代码showSplashViewControllerNoPing()改为showSplashViewController()。

最后在编译运行一次,欣赏下自己的工作成果吧

这里写图片描述

给自己点个赞吧,这是一个非常炫酷的溅落式启动页。

10.接下来

原文链接,我只是勤劳的搬运工而已

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值