原文链接
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.giffunc 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.gifcoooooooooool!!
然而, 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
网友评论