Uber的启动画面是如何制作的
来源:互联网 发布:vb中loop是什么意思 编辑:程序博客网 时间:2024/05/23 21:18
启动画面(Splash Screen)——不但给开发者们提供了一个尽情发挥、创建有趣动画的机会,也填补了App启动时从终端慢吞吞地下载数据的时间。启动画面(动态的)对于App至关重要:它可以让用户不失兴趣地耐心等待应用完成加载。
尽管现在的启动画面多种多样,但很少有像Uber这般精美的。2016年第一季度,Uber的CEO发表了关于重塑品牌的策略,其中之一就是现在这个超酷的启动画面。
这篇教程的目的是尽可能真实地再现Uber的动画。我们会大量地使用到CALayers
、CAAnimations
,以及它们的子类。我不会从头介绍这些类的基本概念,而是把重点放在如何应用这些类,创建高质量的动画。如果你想要了解动画背后的基本原理,可以参考Marin Todorove的iOS动画中级教程。
入门
由于有非常多的动画要实现,我们不妨在这个初始项目的基础上进行修改。初始项目里已经为你创建好了所有需要的CALayer
,我们给它们添加动画即可。
译者注:为了保持教程简洁,删除了原文里一些与教学无关的文字。如有兴趣可通过文章最后的链接阅读原文相关内容。
先来看一眼最终效果:
打开初始项目看看里面的文件。
从控制器的角度分析,项目中的SplashViewController通过它的父视图控制器RootContainerViewController生成。SplashViewController会不停循环播放启动动画,直到App完全加载完成,即与终端API握手成功并获取了必要的数据。值得一提的是,在这个示例项目里,启动动画抽象成了一个单独的模块(译者注:可以直接集成到其他项目里)。
RootContainerViewController里有两个方法:showSplashViewController()
和showSplashViewControllerNoPing()
。我们主要使用第二个方法,它只会不停循环播放动画(不会进入主界面),便于我们把精力集中在SplashViewController的子视图上。当然,最后我们还是会切换回第一个方法,模拟API延迟并过渡到主界面。
启动画面的视图和层
SplashViewController包含了两个子视图。其一是“波纹格子”背景,我们把它叫做TileGridView, 由一系列TileView组成。另一个是带有动画效果的“U”字Logo,我们把它叫做AnimatedULogoView。
AnimatedULogoView里有4个CAShapeLayer:
circleLayer
:“U”型Logo的圆形白色背景lineLayer
:circleLayer
中心到边界的一条直线squareLayer
:circleLayer
中心位置的正方形maskLayer
:遮罩层,当它的边界随着动画改变的时会遮盖其他层
这些CAShapeLayer组合在一起,构成了Fuber标志性的“U”。
既然已经知道这些层的组合方式,那我们可以开始创建动画,让AnimatedULogoView动起来了。
圆形的动画
制作动画的时候,最好过滤掉其他视觉“噪音”,只关注当前实现的动画。打开AnimatedULogoView.swift,在init(frame:)
方法里,把除了cricleLayer
的其他层全都注释掉,实现完动画后我们会重新添加回来。注释后的代码应该像下面一样:
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)}
定位到generateCricleLayer()
方法,试着理解一下这里的圆是怎么绘制的。其实它只不过是用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
设置为圆的半径radius
,因为我们想让它动起来(画圆的过程)。
circleLayer
的动画是由三个CAAnimation组成的:一个描绘笔端动画的CAKeyframeAnimation,一个进行图形变换的CABasicAnimation,以及一个CAAnimationGroup把它们合成在一起。
定位到animateCricleLayer()
,添加下面代码:
// 笔画变化的动画let strokeEndAnimation = CAKeyframeAnimation(keyPath: "strokeEnd")strokeEndAnimation.timingFunction = strokeEndTimingFunctionstrokeEndAnimation.duration = kAnimationDuration - kAnimationDurationDelaystrokeEndAnimation.values = [0.0, 1.0]strokeEndAnimation.keyTimes = [0.0, 1.0]
通过把动画的values
设置为0.0和1.0,我们告诉Core Animation,从startAngle开始,到endAngle结束,创建像时钟一样的动画。随社storkeEnd的值变大,沿着周长的弧线长度也逐渐增加,整个圆逐渐被填满。对于这个例子,如果你把values
改为[0.0, 0.5],那么整个动画只会填满半个圆。
接着添加形变动画:
let transformAnimation = CABasicAnimation(keyPath: "transform")transformAnimation.timingFunction = strokeEndTimingFunctiontransformAnimation.duration = kAnimationDuration - kAnimationDurationDelay// 旋转放大的动画// 起始时:逆时针旋转45°,x、y为正常大小的0.25倍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轴的旋转。其结果是circleLayer
在顺时针旋转45°的同时逐渐放大。这里旋转的参数设置非常重要,因为和其它层的动画组合的时候,它需要和lineLayer
的位置及速度相匹配。
最后,在方法的末尾添加一个CAAnimationGroup,它负责把前面两个动画合成在一起,这样你只需给cricleLayer
添加一个动画即可。
// 把两个动画合成let groupAnimation = CAAnimationGroup()groupAnimation.animations = [strokeEndAnimation, transformAnimation]groupAnimation.repeatCount = Float.infinity // 无限重复动画groupAnimation.duration = kAnimationDurationgroupAnimation.beginTime = beginTimegroupAnimation.timeOffset = startTimeOffsetcircleLayer.addAnimation(groupAnimation, forKey: "looping")
CAAnimationGroup设定了两个重要的属性:beginTime
和timeOffset
,如果你对它们不熟悉的话可以参考这篇文章,里面有这两个属性的描述以及用法。
这里的groupAnimation
的beginTime
属性是根据父视图的时间设定的。timeOffset
在这里也需要设定,因为这个动画在第一次运行的时候,实际上是从中途开始的。当我们完成更多动画时,你可以回到这里,尝试改变startTimeOffset
的值并观察效果的差别。
把groupAnimation
添加给circleLayer
,编译运行一下看看目前的效果:
提示:试着删除
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()
方法,实现下一组动画效果:
// 线段宽度动画let lineWidthAnimation = CAKeyframeAnimation(keyPath: "lineWidth")lineWidthAnimation.values = [0.0, 5.0, 0.0]lineWidthAnimation.timingFunctions = [strokeEndTimingFunction, circleLayerTimingFunction]lineWidthAnimation.duration = kAnimationDurationlineWidthAnimation.keyTimes = [0.0, 1.0 - kAnimationDurationDelay/kAnimationDuration, 1.0]
这个动画会先增加lineLayer
的宽度,随后变回来。
添加下面代码实现下一个动画:
// 变形let transformAnimation = CAKeyframeAnimation(keyPath: "transform")transformAnimation.timingFunctions = [strokeEndTimingFunction, circleLayerTimingFunction]transformAnimation.duration = kAnimationDurationtransformAnimation.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
里:
// 合成动画let groupAnimation = CAAnimationGroup()groupAnimation.repeatCount = Float.infinitygroupAnimation.removedOnCompletion = falsegroupAnimation.duration = kAnimationDurationgroupAnimation.beginTime = beginTimegroupAnimation.animations = [lineWidthAnimation, transformAnimation]groupAnimation.timeOffset = startTimeOffsetlineLayer.addAnimation(groupAnimation, forKey: "looping")
编译运行,观察一下效果。
注意,在这里我们把线条的初始位置设置为了-M_PI_4
,同时把keyTimes
设置为了[0.0, 1.0 - kAnimationDurationDelay/kAnimationDuration, 1.0]
。数组的第一个和最后一个元素显而易见:0.0代表起始,1.0代表终止。为了得到中间时间点,我们需要计算出圆形动画完成、后半部分动画开始(缩小的动画)的时间。用kAnimationDurationDelay
除以kAnimationDuration
可以得到我们需要的结果。但因为它是个延迟动画,所以我们应该用1.0减去它,我们需要从末尾往前倒,减去延迟时间。
现在我们已经完成了circleLayer
和lineLayer
的动画了,接下来该处理中间的方形了。
方形的动画
现在你应该已经轻车熟路了。定位到startAnimation()
方法,注释掉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()
,开始解决下一个动画:
// 边框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)// 边框从原始长度的2/3开始放大,到原始大小后再逐渐缩小到0let boundsAnimation = CAKeyframeAnimation(keyPath: "bounds")boundsAnimation.values = [b1, b2, b3]boundsAnimation.timingFunctions = [fadeInSquareTimingFunction, squareLayerTimingFunction]boundsAnimation.duration = kAnimationDurationboundsAnimation.keyTimes = [0, 1.0-kAnimationDurationDelay/kAnimationDuration, 1.0]
上面的动画改变了CALayer
的边框。我们创建了一个关键帧动画,从边长的2/3开始,放大到完整尺寸,再缩小到0。
接下来是背景颜色的动画:
// 背景颜色的变化let backgroundColorAnimation = CABasicAnimation(keyPath: "backgroundColor")backgroundColorAnimation.fromValue = UIColor.whiteColor().CGColorbackgroundColorAnimation.toValue = UIColor.fuberBlue().CGColorbackgroundColorAnimation.timingFunction = squareLayerTimingFunctionbackgroundColorAnimation.fillMode = kCAFillModeBothbackgroundColorAnimation.beginTime = kAnimationDurationDelay * 2.0 / kAnimationDurationbackgroundColorAnimation.duration = kAnimationDuration / (kAnimationDuration - kAnimationDurationDelay)
注意这里的fillMode
属性。由于beginTime
不是零,动画会把开始和结束时的CGColor包含进去。因此,当我们把动画添加到父CAAnimationGroup里的时候不会闪现不同颜色。
说到CAAnimationGroup,该实现它了:
// 合成动画let groupAnimation = CAAnimationGroup()groupAnimation.animations = [boundsAnimation, backgroundColorAnimation]groupAnimation.repeatCount = Float.infinitygroupAnimation.duration = kAnimationDurationgroupAnimation.removedOnCompletion = falsegroupAnimation.beginTime = beginTimegroupAnimation.timeOffset = startTimeOffsetsquareLayer.addAnimation(groupAnimation, forKey: "looping")
编译运行,检查一下我们的进度,嗯看来方形动画已经顺利完成了。
是时候把之前的动画合并到一起看看效果了!
提示:在iOS模拟器里的动画可能会有些卡顿,因为我们需要在Mac上模拟平时由iOS的GPU完成的工作。如果你的电脑不能流畅运行动画,试着缩小模拟器的屏幕大小,或者在真机上测试。
遮罩层
首先取消init(frame:)
和startAnimating()
里所有注释。
把所有动画组合到一起,我们重新编译运行一下Fuber。
看起来好像还是差点意思?cricleLayer
的缩小消失太过突然。幸运的是,遮罩层动画可以修正这个问题,让它平滑地缩小。
定位到animateMaskLayer()
添加下面的代码:
// 边框缩小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 = kAnimationDurationDelayboundsAnimation.beginTime = kAnimationDuration - kAnimationDurationDelayboundsAnimation.timingFunction = circleLayerTimingFunction
上面的代码用于设定遮罩层边界动画。别忘了,当遮罩层的边界改变时,整个AnimatedULogoView都会被遮挡,因为它作用于所有子层。
现在我们来实现圆角动画,保持遮罩是圆形的:
// 边角弧度let cornerRadiusAnimation = CABasicAnimation(keyPath: "cornerRadius")cornerRadiusAnimation.beginTime = kAnimationDuration - kAnimationDurationDelaycornerRadiusAnimation.duration = kAnimationDurationDelaycornerRadiusAnimation.fromValue = radiuscornerRadiusAnimation.toValue = 2cornerRadiusAnimation.timingFunction = circleLayerTimingFunction
把这两个动画合成为一个CAAnimationGroup,这个层就完成了:
// 合成动画let groupAnimation = CAAnimationGroup()groupAnimation.removedOnCompletion = falsegroupAnimation.fillMode = kCAFillModeBothgroupAnimation.beginTime = beginTimegroupAnimation.repeatCount = Float.infinitygroupAnimation.duration = kAnimationDurationgroupAnimation.animations = [boundsAnimation, cornerRadiusAnimation]groupAnimation.timeOffset = startTimeOffsetmaskLayer.addAnimation(groupAnimation, forKey: "looping")
编译运行。
看起来不错!
教程的上半部分至此结束,关于背景网格的水波效果会在下一篇教程中介绍。
原文链接:How To Create an Uber Splash Screen
转自:http://www.cocoachina.com/ios/20160815/17356.html
- Uber的启动画面是如何制作的
- 如何制作一个类似Uber的溅落式启动屏
- 我的启动画面制作
- Java Application启动画面的制作
- 再谈程序启动画面的制作
- Java Application启动画面的制作
- Android程序的启动画面制作
- 制作和更换XP的启动画面
- 淡入淡出启动画面的制作
- 如何制作程序启动时与关于时的版权画面
- 如何制作winform得启动画面
- 如何制作winform得启动画面
- vc中为应用程序制作启动画面的简单方法
- 用 .NET制作程序启动画面的两种方法。
- 自已动手制作ubuntu的GRUB启动画面
- linux下面制作自己的启动画面bootsplash
- 自已动手制作ubuntu的GRUB启动画面
- iOS程序添加icon图标/启动画面的制作
- jq 轮播图
- 单片机知识预备
- java中volatile关键字的含义
- iOS 音量键事件
- HTML 5 canvas globalCompositeOperation 属性
- Uber的启动画面是如何制作的
- Centos中搭建光盘yum源
- solr suggest 词典配置
- iOS之静态库/动态库
- Java栈的使用简单介绍
- genymotion或者android studio中sdk tools 中 adb和emulator简介
- LeetCode 104 Maximum Depth of Binary Tree
- 好用的开源组件,收藏已备后用
- 绘图原理2