iOS启动动画--Uber启动动画

作者: simuty | 来源:发表于2016-08-03 10:24 被阅读878次

原文链接
How To Create an Uber Splash Screen

近日Uber与滴滴合并了,作为开发者表示真心喜欢Uber的这个启动动画!
Paste_Image.png

因为新闻上看到Uber的消息,更新过后, 真心喜欢这个动画效果, 之前学习iOS Core Animation时也做了一些笔记,最近在梳理知识点时看到一篇文章介绍Uber动画的, 借此机会再对** CALayers **和 CAAnimations,温习一下.


先放个重点,以防没耐心的童鞋看不完导致收获不到任何东西

贝塞尔曲线与CAShapeLayer的关系

1. CAShapeLayer中shape代表形状的意思,所以需要形状才能生效 
2. 贝塞尔曲线可以创建基于矢量的路径 
3. 贝塞尔曲线给CAShapeLayer提供路径,CAShapeLayer在提供的路径中进行渲染。路径会闭环,所以绘制出了Shape 
4. 用于CAShapeLayer的贝塞尔曲线作为Path,其path是一个首尾相接的闭环的曲线,即使该贝塞尔曲线不是一个闭环的曲线


整体分析

从UIViewController的角度出发,APP的炫酷动画(启动动画)应该是父视图,由此来转换到用户的使用界面---地图页面, 直到APP请求完必要的API与加载完成必要的数据从而结束循环动画.

RootContainerViewController中有两个方法:showSplashViewController()showSplashViewControllerNoPing(). 像大多数教程一样, 你会调用showSplashViewControllerNoPing()循环展示动画, 所以你可以将动画集中到子类--SplashViewController中, 最后你可以通过调用showSplashViewController()来模拟延迟实现转场到主视图.

层级分析

SplashViewController包含两个视图, 一个主要负责显示文字, 另外一个负责动画AnimatedULogoView, 如下图, 图片来源:

RiderIconView.gif

主要的功能在AnimatedULogoView包含了四个CAShapeLayers:

1. circleLayer: 描绘背景**U**的循环;
2. lineLayer: 这个直线负责显示循环最后留有的边缘;
3. squareLayer: 是circleLayer中间的那个缩小时的方块'
4. maskLayer: 遮盖其他的View, 使其他layer层实现波浪效果.

总的来说CAShaperLayers实现的效果的原型如下图示:

Fuber-Animation (1).gif

第一部分

对于贝塞尔曲线和CA动画的实现, 如果需要工程初期项目---Download the starter project here.

第一步: 画圆

在实现特效动画时,应该抛开特效,分步骤的来分析实现. 接下来在AnimatedULogoView.swift 中一步一步实现圆的效果:

1.gif
//画圆的前期配置
func generateCircleLayer() -> CAShapeLayer {
        //初始化layer层
        let layer = CAShapeLayer()
        let width:CGFloat = 40
        //在此使用半径为宽度
        layer.lineWidth = width//4
        
        /**
         *  center:弧线中心点的坐标 radius:弧线所在圆的半径 startAngle:弧线开始的角度值 endAngle:弧线结束的角度值 clockwise:是否顺时针画弧线
         */
        
        //开始和结束的角度之和需要达到360°
        layer.path = UIBezierPath(arcCenter: CGPointZero, radius: width/2, startAngle: CGFloat(-2*M_PI_2), endAngle: CGFloat(2*M_PI_2), clockwise: true).CGPath
        //填充的颜色
        layer.strokeColor = UIColor.redColor().CGColor
        //如果不将此设置为指定颜色,将显示黑色
        layer.fillColor = UIColor.clearColor().CGColor
        return layer
        
    }

第二步

圆形动画还需要三个CAAnimations:

1. CAKeyframeAnimation: 关键帧动画;
2. CABasicAnimation: 基本动画实现转换;
3. CAAnimationGroup: 包含并实现以上两种动画.

1.CAKeyframeAnimation: 关键帧动画;


//执行动画, 画圆
    func animationCircleLayer(){
       // 关键帧(keyframe)使我们能够定义动画中任意的一个点,然后让 Core Animation 填充所谓的中间帧。
        let strokeEndAnimation = CAKeyframeAnimation(keyPath: "strokeEnd")
        
        //设定动画的速度变化 x - y - x - y
        strokeEndAnimation.timingFunction = CAMediaTimingFunction(controlPoints: 1.00, 0.0, 0.35, 1.00)
        
        strokeEndAnimation.duration = kAnimationDuration - kAnimationDurationDelay
        //确保是一个完整的圆
        strokeEndAnimation.values = [0.0, 1.0]
        //keyTimes是确保开始到结束的时从0.0-1.0分别设置,避免其中的动画产生跳转。
        strokeEndAnimation.keyTimes = [0.0, 1.0]
}

2.CABasicAnimation: 基本动画实现转换

// 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°后恢复至原始半径.

3.CAAnimationGroup: 包含并实现以上两种动画.可以试着只添加一种动画, 看看效果.

let groupAnimation = CAAnimationGroup()
        //添加动画, 可以在此删除任一种动画, 会看到另类效果
        groupAnimation.animations = [strokeEndAnimation, transform]
        //重复次数
        groupAnimation.repeatCount = Float.infinity
        groupAnimation.duration = kAnimationDuration
        groupAnimation.beginTime = beginTime
        groupAnimation.timeOffset = 0.7 * kAnimationDuration
        circleLayer.addAnimation(groupAnimation, forKey: "looping")

第三步: 划线

画好了圆, 把接下来的那个留白的线整了, 做了这一块, 你会发现动画重在分析分步. 眼见不一定为实

2.gif
func generateLineLayer() -> CAShapeLayer {
        let layer = CAShapeLayer()
        layer.position = CGPointZero
        layer.frame = CGRectZero
        layer.allowsGroupOpacity = true
        layer.lineWidth = 5.0
        layer.strokeColor = UIColor(red: 15/255, green: 78/255, blue: 101/255, alpha: 1).CGColor
        
        let bezierPath = UIBezierPath()
        //设置起点
        bezierPath.moveToPoint(CGPointZero)
    //划线
        bezierPath.addLineToPoint(CGPointMake(-40, 0.0))
        layer.path = bezierPath.CGPath
        return layer
    }



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


第四步: 添加方形

添加animateSquareLayer()函数

// 2/3
  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))
  //0
  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]

==============
// 背景色
  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)


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


以上几个步骤拆分整个icon的动画,接下来在进行修饰一下


//整体修饰
    private func 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
   
        // 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")
    
    }



如下图所示

3.gif

第二部分: 背景网格

第一步:创建网格

试着想象集群的ui视图通过TileGridView实例。他们看起来像什么?嗯…时间停止然后产生隆隆声, 接着看一看效果吧!

背景网格包含一系列的TileViews组成的TileGridView.打开TileView.swift找到init(frame:), 添加如下方法:

layer.borderWidth = 2.0

运行后的效果:

Paste_Image.png

正如你所看到的, 这个TileViews已经被安排为网格状, 这样被创建其实是调用了TileGridView.swift中的renderTileViews()方法, 这个逻辑前期已经准备好了.你只需要实现它! 当然, 还是建议学学的

第二步:动态实现TileView

TileGridView有个containerView子类, 它负责添加所有的TileViews. 这个类中有个二维数组tileViewRows, 包含所有的添加当前的视图上的TileViews.

回到TileView‘s init(frame:).删除之前添加的代码, 添加如下代码, 将TileView中的图片填充layer层上.

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

如图所示:

Grid-Starting.gif

coooooooooool!!

然而, TileGridView和它的子视图需要一些动态效果, 打开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]()

以上代码定义了一系列的timing函数, 接下来开始使用, 添加如下代码:

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是个布尔类型,来控制变换/位置动画是否添加到动画刚刚创建数组中,它的默认值是true, 所有的TileViews不在TileGridView视图上, 这个逻辑之前已经在TileGridView中的renderTileViews()实现了.

添加蒙版的动画:

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

然后将这些动画放进动画组中:

// 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之前那个数组.

是时候调用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)
    }
  }
}

如图所示:

Grid-1.gif

这下好多了, 但是在这个网状中的冲击波还是不够火候, 这意味着延迟补偿需要创建并基于视图的中心距离乘以一个常数来创建.
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)
}

回到startAnimatingWithBeginTime(_:), 用下面的代码替换原来的代码:


 for view in tileRows {
      let distance = self.distanceFromCenterViewWithView(view)
 
      view.startAnimatingWithDuration(kAnimationDuration, beginTime: beginTime, rippleDelay: kRippleDelayMultiplier * NSTimeInterval(distance), rippleOffset: CGPointZero)
    }
  }

在此调用延迟方法, 运行如下图:

Grid-2.gif

更好了!现在这个动画开始看起来更加体面了,但仍有一些缺失。TileViews移动应该基于冲击波的方向和大小的.

更好的方法是使用高中时学习的知识, 基于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)
    }
  }
}

运行如下图:

Grid-3.gif

非常酷!已经有“放大”的感觉了,但规模动画发生之前需要有一个改变面具的界限。

回到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")

运行如下如:

FuberFinal.gif

漂亮!


注释: 试着去改变`kRippleMagnitudeMultiplier`和`kRippleDelayMultiplier`的值, 看看会有什么样的效果

完成了所有的事情后, 回到RootContainerViewController.swift. 在viewDidLoad()中注释showSplashViewControllerNoPing(), 添加showSplashViewController().

最后的效果如下

Fuber-Animation (1).gif

参考
1.CAShapeLayer和贝塞尔曲线
2.Stackoverflow

更多精彩内容请关注“IT实战联盟”哦~~~


IT实战联盟.jpg

相关文章

网友评论

    本文标题:iOS启动动画--Uber启动动画

    本文链接:https://www.haomeiwen.com/subject/bqeqsttx.html