本文是我学习《iOS Animations by Tutorials》 笔记中的一篇。 文中详细代码都放在个人Github上 andyRon/LearniOSAnimations。html
系统学习iOS动画之一:视图动画 学习了建立视图动画(View Animations),这一部分学习功能更强大、更偏底层的Core Animation(核心动画) APIs。核心动画的这个名字可能使人有点误解,暂时能够理解为本文的标题图层动画(Layer Animations)。ios
在本书的这一部分中,将学习动画层而不是视图以及如何使用特殊图层。git
图层是一个简单的模型类,它公开了许多属性来表示一些基于图像的内容。 每一个UIView
都有一个图层支持(都有一个layer
属性)。github
视图 vs 图层swift
因为如下缘由,图层(Layers)与视图(Views)(对于动画)不一样:数组
单个来讲,二者的优势。缓存
视图:bash
图层:闭包
视图和图层的选择技巧: 任什么时候候均可以选择视图动画; 当须要更高的性能时,就须要使用图层动画。架构
二者在架构中的位置:
预览:
本文比较长,图片比较多,预警⚠️😀。
8-图层动画入门 —— 从最简单的图层动画开始,了解调试动画错误的方法。
9-动画的Keys和代理 —— 怎么更好地控制当前运行的动画,并使用代理方法对动画事件作出响应。
10-动画组和时间控制 —— 组合许多简单的动画,并将它们做为一个组一块儿运行。
11-图层弹簧动画 —— 学习如何使用CASpringAnimation
建立强大而灵活的弹簧图层动画。
12-图层关键帧动画和结构属性 —— 学习图层关键帧动画, 动画结构属性的一些特殊处理。
接下来,学习几个专门的图层:
13-形状和蒙版 —— 经过CAShapeLayer
在屏幕上绘制形状,并为其特殊路径属性设置动画。
14-渐变更画 —— 了解如何使用CAGradientLayer
来绘制渐变和动画渐变。
15-Stroke和路径动画 —— 以交互方式绘制形状,并使用关键帧动画的一些强大功能。
16-复制动画 —— 学习如何建立图层内容的多个副本,而后利用副本制做动画。
图层动画的工做方式与视图动画很是类似; 只需在定义的时间段内为起始值和结束值之间的属性设置动画,而后让Core Animation处理二者之间的渲染。
可是,图层动画具备比视图动画更多的可动画属性; 在设计效果时,这会提供了不少选择和灵活性; 图层动画还有许多专门的CALayer子类(如CATextLayer
、 CAShapeLayer
、 CATransformLayer
、CAGradientLayer
、CAReplicatorLayer
、CAScrollLayer
、CAEmitterLayer
、AVPlayerLayer
等),这些子类有提供了许多其余属性。
本章介绍CALayer和Core Animation的基础知识。
可与视图动画的可动画属性对照着看。
bounds
、position
、transform
borderColor
、 borderWidth
、cornerRadius
shadowOffset
: 使阴影看起来更接近或更远离图层。 shadowOpacity
:使阴影淡入或淡出。 shadowPath
: 更改图层阴影的形状。 能够建立不一样的3D效果,使图层看起来像浮动在不一样的阴影形状和位置上。 shadowRadius
: 控制阴影的模糊; 当模拟视图朝向或远离投射阴影的表面移动时,这尤为有用。
contents
:修改此项以将原始TIFF或PNG数据指定为图层内容。
mask
:修改它将用于掩盖图层可见内容的形状或图像。 这个属性在13-形状和蒙版将详细介绍和使用。
opacity
开始项目使用 3-过渡动画完成的项目。
把本来head的视图动画替换为图层动画。
分别删除ViewController的viewWillAppear()
中:
heading.center.x -= view.bounds.width
复制代码
和viewDidAppear()
中:
UIView.animate(withDuration: 0.5) {
self.heading.center.x += self.view.bounds.width
}
复制代码
在viewWillAppear()
的开始(super
调用后)添加:
let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = -view.bounds.size.width/2
flyRight.toValue = view.bounds.size.width/2
flyRight.duration = 0.5
复制代码
核心动画中的动画对象只是简单的数据模型; 上面的代码建立了CABasicAnimation
的实例,并设置了一些数据属性。 这个实例描述了一个潜在的图层动画:能够选择当即运行,稍后运行,或者根本不运行。
因为动画未绑定到特定图层,所以能够在其余图层上重复使用动画,每一个图层将独立运行动画的副本。
在动画模型中,您能够将要设置为动画的属性指定为keypath
参数(好比上面设置是"position.x"
); 这很方便,由于动画老是在图层中设置。
接下来,为在keypath
上指定的属性设置fromValue
和toValue
。须要动画对象(此处我要处理的是heading)从屏幕左侧到屏幕中央。动画持续时间的概念没有改变; duration
设置为0.5秒。
动图已经设置完成,如今须要把它添加须要运行此动画的图层上。 在刚添加的代码下方添加,将动画添加到heading的图层:
heading.layer.add(flyRight, forKey: nil)
复制代码
add(_:forKey:)
会把动画作个一个拷贝给将要添加的图层。 若是以后须要更改或中止动画,能够添加forKey
参数用于识别动画。
此时的动画看上去和以前视图动画没有什么区别。
同同样的方法应用在Username Filed上,删除viewWillAppear()
和viewDidAppear()
中对应代码。再把以前的动画添加的Username Filed的layer上:
username.layer.add(flyRight, forKey: nil)
复制代码
此时运行项目,看上去会有点别扭,由于heading Label,Username Filed的动画是相同的,Username Filed没有以前的延迟效果。
在添加动画到Username Filed的layer上以前,添加:
flyRight.beginTime = CACurrentMediaTime() + 0.3
复制代码
动画的beginTime
属性设置动画应该开始的绝对时间; 在这种状况下,可使用CACurrentMediaTime()
获取当前时间(系统的一个绝对时间,机器开启时间,取自机器时间 mach_absolute_time()
),并以秒为单位添加所需的延迟。
此时,若是仔细观察会发现有个问题,Username Filed在开始动画以前已经出现了,这就涉及到另一个图层动画属性 fillMode
了。
fillMode
以Username Field的移动动画来看看fillMode
不一样值的区别,为了方便观察,我把beginTime
时间变大,代码相似于:
let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = -view.bounds.size.width/2
flyRight.toValue = view.bounds.size.width/2
flyRight.duration = 0.5
heading.layer.add(flyRight, forKey: nil)
flyRight.beginTime = CACurrentMediaTime() + 2.3
flyRight.fillMode = kCAFillModeRemoved
username.layer.add(flyRight, forKey: nil)
复制代码
kCAFillModeRemoved
是fillMode
的默认值
在定义的beginTime
处启动动画(若是未设置beginTime
,也就是beginTime
等于CACurrentMediaTime()
,则当即启动动画), 并在动画完成时删除动画期间所作的更改:
实际效果:
now 到 begin 这段时间动画没有开始,但Username Field直接显示了,而后到 begin时动画才开始,这就是以前遇到的状况。
kCAFillModeBackwards
不管动画的实际开始时间如何,kCAFillModeBackwards
都会当即在屏幕上显示动画的第一帧,并在之后启动动画:
实际效果:
第一帧在fromValue
处,也就是"position.x"
是负的在屏幕外,所以开始时没有看见Username Field,等待2.3s后动画开始。
kCAFillModeForwards
kCAFillModeForwards
像往常同样播放动画,但在屏幕上保留动画的最后一帧,直到您删除动画:
实际效果:
除了设置kCAFillModeForwards以外,还须要对图层进行一些其余更改以使最后一帧“粘贴”。 你将在本章后面稍后了解这一点。 和第一个有点相似,但仍是有区别的。
kCAFillModeBoth
kCAFillModeBoth
是kCAFillModeForwards
和kCAFillModeBackwards
的组合; 这会使动画的第一帧当即出如今屏幕上,并在动画结束时在屏幕上保留最终帧:
实际效果:
要解决以前发现的问题,将使用kCAFillModeBoth
。
一样对于Password Field,也删除其视图动画的代码,改换成相似Username Field的图层动画,不过beginTime
要晚一点,具体代码:
复制代码
flyRight.beginTime = CACurrentMediaTime() + 0.3 flyRight.fillMode = kCAFillModeBoth username.layer.add(flyRight, forKey: nil)
flyRight.beginTime = CACurrentMediaTime() + 0.4 password.layer.add(flyRight, forKey: nil)
到目前为止,您的动画刚好在表单元素最初位于Interface Builder中的确切位置结束。 可是,不少时候状况并不是如此。
### 调试动画
在上面的动画后继续添加:
```swift
username.layer.position.x -= view.bounds.width
password.layer.position.x -= view.bounds.width
复制代码
这就是把两个文本框的图层移动到屏幕外,相似于flyRight.fromValue = -view.bounds.size.width/2
(此时这段代码能够暂时注释掉),运行后发现问题,动画结束后两个文本框消失了,这是怎么回事呢?
继续在上面的代码后添加一个延迟函数:
delay(seconds: 5.0)
print("where are the fields?")
}
复制代码
并打断点后运行:
进入UI hierarchy 窗口:
UI hierarchy 模式下能够查看当前运行时的UI层次结构,包括已经隐藏或透明视图以及在屏幕外的视图。还能够3D查看。
固然还能够在右侧检测器中查看实时属性:
动画完成后,代码更改会致使字段跳回其初始位置。 但为何?
当你为Text Field设置动画时,你实际上并无看到Text Field自己是动画的; 相反,你会看到它的缓存版本,称为presentation layer(显示层)。动画完成后原始图层再次到本来位置,则从屏幕上移除presentation layer。 首先,请记住在viewWillAppear(_:)
中将Text Field设置在屏幕外:
动画开始时,Text Field暂时隐藏,预渲染的动画对象将替代它:
如今没法点击动画对象,输入任何文本或使用任何其余特定文本字段功能,由于它不是真正的文本字段,只是可见的“幻像”。 动画一旦完成,它就会从屏幕上消失,原始Text Field将被取消隐藏。但它此时的位置还在屏幕左侧!
要解决这个难题,您须要使用另外一个CABasicAnimation
属性:isRemovedOnCompletion
。
将fillMode
设置为kCAFillModeBoth
可以让动画在完成后保留在屏幕上,并在动画开始以前显示动画的第一帧。要完成效果,您须要相应地设置removedOnCompletion
,二者的组合将使动画在屏幕上可见。 在设置fillMode
以后,将如下行添加到viewWillAppear()
:
flyRight.isRemovedOnCompletion = false
复制代码
isRemovedOnCompletion
默认为true
,所以动画一完成就会消失。将其设置为false
并将其与正确的fillMode
组合可将动画保留在屏幕上 。
如今运行项目,应该能看到全部元素都按预期保留在屏幕上。
从屏幕上删除图层动画后,图层将回退到其当前位置和其余属性值。 这意味着您一般须要更新图层的属性以反映动画的最终值。
虽然前面已经说明过把isRemovedOnCompletion
设置成false
是如何工做的,但尽量避免使用它。 在屏幕上保留动画会影响性能,所以须要自动删除它们并更新原始图层的位置。
须要把原始图层设置到屏幕中间,在viewWillAppear
中天假:
username.layer.position.x = view.bounds.size.width/2
password.layer.position.x = view.bounds.size.width/2
复制代码
固然此时要注意把以前注释掉的flyRight.fromValue = -view.bounds.size.width/2
,去掉注释,也要把调试动画时的代码去掉。
删除viewWillAppear()
中把四个☁️透明度设为0.0的代码,和viewDidAppear()
的☁️的视图动画。
而后在viewDidAppear()
加入:
let cloudFade = CABasicAnimation(keyPath: "alpha")
cloudFade.duration = 0.5
cloudFade.fromValue = 0.0
cloudFade.toValue = 1.0
cloudFade.fillMode = kCAFillModeBackwards
cloudFade.beginTime = CACurrentMediaTime() + 0.5
cloud1.layer.add(cloudFade, forKey: nil)
cloudFade.beginTime = CACurrentMediaTime() + 0.7
cloud2.layer.add(cloudFade, forKey: nil)
cloudFade.beginTime = CACurrentMediaTime() + 0.9
cloud3.layer.add(cloudFade, forKey: nil)
cloudFade.beginTime = CACurrentMediaTime() + 1.1
cloud4.layer.add(cloudFade, forKey: nil)
复制代码
把原登陆按钮背景颜色变化的动画修改为图层动画。
删除logIn()
中的:
self.loginButton.backgroundColor = UIColor(red: 0.85, green: 0.83, blue: 0.45, alpha: 1.0)
复制代码
删除resetForm()
中的:
self.loginButton.backgroundColor = UIColor(red: 0.63, green: 0.84, blue: 0.35, alpha: 1.0)
复制代码
在ViewController.swift文件中建立一个全局的背景颜色变化动画函数:
func tintBackgroundColor(layer: CALayer, toColor: UIColor) {
let tint = CABasicAnimation(keyPath: "backgroundColor")
tint.fromValue = layer.backgroundColor
tint.toValue = toColor.cgColor
tint.duration = 0.5
layer.add(tint, forKey: nil)
layer.backgroundColor = toColor.cgColor
}
复制代码
在logIn()
中添加:
let tintColor = UIColor(red: 0.85, green: 0.83, blue: 0.45, alpha: 1.0)
tintBackgroundColor(layer: loginButton.layer, toColor: tintColor)
复制代码
在resetForm()
中登陆按钮动画方法的completion
闭包中添加:
completion: { _ in
let tintColor = UIColor(red: 0.63, green: 0.84, blue: 0.35, alpha: 1.0)
tintBackgroundColor(layer: self.loginButton.layer, toColor: tintColor)
})
复制代码
在ViewController.swift文件中建立一个全局的圆角变化动画函数:
func roundCorners(layer: CALayer, toRadius: CGFloat) {
let round = CABasicAnimation(keyPath: "cornerRadius")
round.fromValue = layer.cornerRadius
round.toValue = toRadius
round.duration = 0.33
layer.add(round, forKey: nil)
layer.cornerRadius = toRadius
}
复制代码
在logIn()
中添加:
roundCorners(layer: loginButton.layer, toRadius: 25.0)
复制代码
在resetForm()
中登陆按钮动画方法的completion
闭包中添加:
roundCorners(layer: self.loginButton.layer, toRadius: 10.0)
复制代码
两种状态的变化:
两个动画函数tintBackgroundColor
和roundCorners
最后都须要把动画最变化最终值赋值给动画的属性,这对应于前面的 [动画 vs 真实内容](#动画 vs 真实内容) 章节
本章节的最终效果:
关于视图动画和相应的闭包语法的一个棘手问题是,一旦您建立并运行视图动画,您就没法暂停,中止或以任何方式访问它。
可是,使用核心动画,您能够轻松检查在图层上运行的动画,并在须要时中止它们。 此外,您甚至能够在动画上设置委托对象并对动画事件作出反应。
本章的开始项目使用上一章完成的项目
CAAnimationDelegate
的两个代理方法:
func animationDidStart(_ anim: CAAnimation)
func animationDidStop(_ anim: CAAnimation, finished flag: Bool)
复制代码
作个小测试,在flyRight
初始化时,添加:
flyRight.delegate = self
复制代码
对ViewController
添加扩展,并实现一个代理方法:
extension ViewController: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
print(anim.description, "动画完成")
}
}
复制代码
运行,打印结果:
<CABasicAnimation: 0x6000032376e0> 动画完成
<CABasicAnimation: 0x600003237460> 动画完成
<CABasicAnimation: 0x600003237480> 动画完成
复制代码
会发现animationDidStop(_:finished:)
方法被调用三次,而且每次调用的动画都不一样,这由于当每一次调用layer.add(_:forKey:)
把动画添加给图层时,都会拷贝一份,这在前面的图层动画基础知识中说明过。
CAAnimation
类及其子类是用Objective-C编写的,而且符合键值编码(KVO),这意味着您能够将它们视为字典,并在运行时向它们添加新属性。(关于KVO,可查看个人小结文章 OC中的键/值编码(KVC))
使用此机制为flyRight
动画指定名称,以便以后能够从其余活动动画中识别它。
在viewWillAppear()
中的flyRight.delegate = self
后添加:
flyRight.setValue("form", forKey: "name")
flyRight.setValue(heading.layer, forKey: "layer")
复制代码
在上面的代码中,在flyRight
动画上建立键为"name"
,值为"form"
的键值对,能够从委托回调方法调用识别;
也建立了一个键为"layer"
,值为heading.layer
的键值对,以方便以后引用动画所属的图层。
一样的能够添加(以前已经说过每次动画都会拷贝一份,因此不会覆盖):
flyRight.setValue(username.layer, forKey: "layer")
// ...
flyRight.setValue(password.layer, forKey: "layer")
复制代码
在代理回调方法中验证上面的代码,上面的移动动画结束后再添加一个简单的脉动动画:
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
// print(anim.description, "动画完成")
guard let name = anim.value(forKey: "name") as? String else {
return
}
if name == "form" {
// `value(forKey:)`的结果老是`Any`,所以须要转换为所需类型
let layer = anim.value(forKey: "layer") as? CALayer
anim.setValue(nil, forKey: "layer")
// 简单的脉动动画
let pulse = CABasicAnimation(keyPath: "transform.scale")
pulse.fromValue = 1.25
pulse.toValue = 1.0
pulse.duration = 0.25
layer?.add(pulse, forKey: nil)
}
}
复制代码
注意:
layer?.add()
意味着若是动画中没有存储图层,则会跳过add(_:forKey:)
的调用。 这是Swift中的可选链式调用,可参考以撸代码的形式学习Swift-17:可选链式调用(Optional Chaining)
移动动画结束后有一个简单变大的脉动动画效果:
add(_:forKey:)
中的参数forKey
(注意不要和setValue(_:forKey:)
中的forKey
混淆),以前一直没使用。
在这部分中,将建立另外一个图层动画,学习如何一次运行多个动画,并了解如何使用动画Keys控制正在运行的动画。
添加一个新标签,新标签将从右到左缓慢动画,用来提示用户输入。 一旦用户开始输入他们的用户名或密码(Text Field得到焦点),该标签将中止移动并直接跳到其最终位置(居中位置)。 一旦用户知道该怎么作就没有必要继续动画。
在ViewController
中添加属性 let info = UILabel()
,并在viewDidLoad()
中配置:
info.frame = CGRect(x: 0.0, y: loginButton.center.y + 60.0, width: view.frame.size.width, height: 30)
info.backgroundColor = UIColor.clear
info.font = UIFont(name: "HelveticaNeue", size: 12.0)
info.textAlignment = .center
info.textColor = UIColor.white
info.text = "Tap on a field and enter username and password"
view.insertSubview(info, belowSubview: loginButton)
复制代码
为info
添加两个动画:
// 提示信息Label的两个动画
let flyLeft = CABasicAnimation(keyPath: "position.x")
flyLeft.fromValue = info.layer.position.x + view.frame.size.width
flyLeft.toValue = info.layer.position.x
flyLeft.duration = 5.0
info.layer.add(flyLeft, forKey: "infoappear")
let fadeLabelIn = CABasicAnimation(keyPath: "opacity")
fadeLabelIn.fromValue = 0.2
fadeLabelIn.toValue = 1.0
fadeLabelIn.duration = 4.5
info.layer.add(fadeLabelIn, forKey: "fadein")
复制代码
flyLeft
是从左到右移动的动画,fadeLabelIn
是透明度渐渐变大的动画。
此时的动画效果以下:
为Text Field添加代理。经过扩展,让ViewController
遵循UITextFieldDelegate
协议:
extension ViewController: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
guard let runningAnimations = info.layer.animationKeys() else {
return
}
print(runningAnimations)
}
}
复制代码
在viewDidAppear()
中添加:
username.delegate = self
password.delegate = self
复制代码
此时运行,info
动画还在进行时点击文本框,会打印动画key值:
["infoappear", "fadein"]
复制代码
在 textFieldDidBeginEditing(:)
里添加:
info.layer.removeAnimation(forKey: "infoappear")
复制代码
点击文本框后,删除从左向右移动的动画,info
当即到达终点,也就是屏幕中央:
固然也能够经过removeAllAnimations()
方法删除layer
上的全部动画。
**注意:**动画进行完了,会默认被从
layer
上删除,也就是animationKeys()
方法将得到不到动画keys了。
经过本章所学的动画代理和动画KVO修改☁️的动画
先在ViewController
中添加动画方法:
/// 云的图层动画
func animateCloud(layer: CALayer) {
let cloudSpeed = 60.0 / Double(view.layer.frame.size.width)
let duration: TimeInterval = Double(view.layer.frame.size.width - layer.frame.origin.x) * cloudSpeed
let cloudMove = CABasicAnimation(keyPath: "position.x")
cloudMove.duration = duration
cloudMove.toValue = self.view.bounds.width + layer.bounds.width/2
cloudMove.delegate = self
cloudMove.setValue("cloud", forKey: "name")
cloudMove.setValue(layer, forKey: "layer")
layer.add(cloudMove, forKey: nil)
}
复制代码
把viewDidAppear()
中的四个animateCloud
方法调用替代为:
animateCloud(layer: cloud1.layer)
animateCloud(layer: cloud2.layer)
animateCloud(layer: cloud3.layer)
animateCloud(layer: cloud4.layer)
复制代码
让☁️不停的移动,在动画代理方法animationDidStop
中添加:
if name == "cloud" {
if let layer = anim.value(forKey: "layer") as? CALayer {
anim.setValue(nil, forKey: "layer")
layer.position.x = -layer.bounds.width/2
delay(0.5) {
self.animateCloud(layer: layer)
}
}
}
复制代码
本章的效果:
在上一章中,学习了如何向单个图层添加多个独立动画。 可是,若是您但愿您的动画同步工做并保持彼此一致,该怎么办? 这就用到动画组(animation groups)。
本章介绍如何使用CAAnimationGroup
对动画进行分组,能够向组中添加多个动画并同时调整持续时间,委托和timingFunction
等属性。 对动画进行分组会产生简化的代码,并确保您的全部动画将做为一个实体单元同步。
本章的开始项目使用上一章完成的项目
删除viewWillAppear()
中的:
loginButton.center.y += 30.0
loginButton.alpha = 0.0
复制代码
删除viewDidAppear()
中登陆按钮的显示动画:
UIView.animate(withDuration: 0.5, delay: 0.5, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: [], animations: {
self.loginButton.center.y -= 30.0
self.loginButton.alpha = 1.0
}, completion: nil)
复制代码
在viewDidAppear()
中组动画添加:
let groupAnimation = CAAnimationGroup()
groupAnimation.beginTime = CACurrentMediaTime() + 0.5
groupAnimation.duration = 0.5
groupAnimation.fillMode = kCAFillModeBackwards
复制代码
CAAnimationGroup
继承于CAAnimation
,也有beginTime
, duration
, fillMode
, delegate
等属性。
继续三个动画,并把它们加入到上面的组动画中:
let scaleDown = CABasicAnimation(keyPath: "transform.scale")
scaleDown.fromValue = 3.5
scaleDown.toValue = 1.0
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.fromValue = .pi / 4.0
rotate.toValue = 0.0
let fade = CABasicAnimation(keyPath: "opacity")
fade.fromValue = 0.0
fade.toValue = 1.0
groupAnimation.animations = [scaleDown, rotate, fade]
loginButton.layer.add(groupAnimation, forKey: nil)
复制代码
登陆按钮的效果为:
图层动画中的动画缓动与1-视图动画入门中介绍的视图动画的动画选项的,在概念上是相同的, 只是语法有所不一样。
图层动画中的动画缓动经过类CAMediaTimingFunction
来表示 。用法以下:
groupAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
复制代码
name
参数有以下几种,和视图动画中的差很少:
kCAMediaTimingFunctionLinear
速度不变化
kCAMediaTimingFunctionEaseIn
开始时慢,结束时快
kCAMediaTimingFunctionEaseOut
开始时快,结束时慢
kCAMediaTimingFunctionEaseInEaseOut
开始结束都慢,中间快
能够试一下不一样的效果。
另外CAMediaTimingFunction
有个初始化方法init(controlPoints c1x: Float, _ c1y: Float, _ c2x: Float, _ c2y: Float)
,能够自定义缓动模式,具体可参考官方文档
repeatCount
可设置重复动画指定的次数。 为提示信息Label的动画添加剧复次数,在viewDidAppear()
中为flyLeft
动画设置属性:
flyLeft.repeatCount = 4
复制代码
另一个repeatDuration
可用来设置总重复时间。
和视图动画同样,也要设置autoreverses
,要否则不连贯:
flyLeft.autoreverses = true
复制代码
如今效果看着不错了,可是还有点问题,就是4次重复结束后,会直接跳到屏幕中心,以下(因为太长,gif已经省略了前几回滚动):
这也很好理解,最后一个循环以标签离开屏幕结束。解决办法就是半个动画周期:
flyLeft.repeatCount = 2.5
复制代码
能够经过设置速度属性来独立于持续时间来控制动画的速度。
flyLeft.speed = 2.0
复制代码
下面代码:
let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = -view.bounds.size.width/2
flyRight.toValue = view.bounds.size.width/2
flyRight.duration = 0.5
flyRight.fillMode = kCAFillModeBoth
flyRight.delegate = self
flyRight.setValue("form", forKey: "name")
flyRight.setValue(heading.layer, forKey: "layer")
heading.layer.add(flyRight, forKey: nil)
flyRight.setValue(username.layer, forKey: "layer")
flyRight.beginTime = CACurrentMediaTime() + 0.3
username.layer.add(flyRight, forKey: nil)
flyRight.setValue(password.layer, forKey: "layer")
flyRight.beginTime = CACurrentMediaTime() + 0.4
password.layer.add(flyRight, forKey: nil)
复制代码
修改成:
let formGroup = CAAnimationGroup()
formGroup.duration = 0.5
formGroup.fillMode = kCAFillModeBackwards
let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = -view.bounds.size.width/2
flyRight.toValue = view.bounds.size.width/2
let fadeFieldIn = CABasicAnimation(keyPath: "opacity")
fadeFieldIn.fromValue = 0.25
fadeFieldIn.toValue = 1.0
formGroup.animations = [flyRight, fadeFieldIn]
heading.layer.add(formGroup, forKey: nil)
formGroup.delegate = self
formGroup.setValue("form", forKey: "name")
formGroup.setValue(username.layer, forKey: "layer")
formGroup.beginTime = CACurrentMediaTime() + 0.3
username.layer.add(formGroup, forKey: nil)
formGroup.setValue(password.layer, forKey: "layer")
formGroup.beginTime = CACurrentMediaTime() + 0.4
password.layer.add(formGroup, forKey: nil)
复制代码
本章节的最终效果:
前面视图动画中的2-弹簧动画能够用于建立一些相对简单的弹簧式动画,而本章节学习的**图层弹簧动画(Layer Springs)**能够呈现一个看起来更天然的物理模拟。
本章的开始项目使用上一章完成的项目,添加一些新的图层弹簧动画,并说明两种弹簧动画之间的差别。
先说一些理论知识:
阻尼谐振子,Damped harmonic oscillators(直译就是,逐渐衰弱的振荡器),能够理解为逐渐衰减的振动。
UIKit API简化了弹簧动画的制做,不须要了解它们的原理就能够很方便的使用。 可是,因为您如今是核心动画专家,所以您须要深刻研究细节。
钟摆,理想情况下钟摆是不停的摆动,像下面的同样:
对应的运动轨迹图就像:
但现实中因为能量的损耗,钟摆的摇摆的幅度会逐渐减少:
对应的运动轨迹:
这就是一个阻尼谐振子 。
钟摆停下来所需的时间长度,以及最终振荡器图形的方式取决于振荡系统的如下参数:
阻尼(damping):因为空气摩擦、机械摩擦和其余做用在系统上的外部减速力。
质量(mass):摆锤越重,摆动的时间越长。
刚度(stiffness):振荡器的“弹簧”越硬(钟摆的“弹簧”是指地球的引力),钟摆摆动越困难,系统停下来也越快。想象一下,若是在月球或木星上使用这个钟摆;在低重力和高重力状况下的运动将是彻底不一样的。
初始速度(initial velocity):推一下钟摆。
“这一切都很是有趣,但与弹簧动画有什么关系呢?”
阻尼谐振子系统是推进iOS中弹簧动画的动力。 下一节将更详细地讨论这个问题。
UIKit以动态方式调整全部其余变量,使系统在给定的持续时间内稳定下来。 这就是为何UIKit弹簧动画有时有点被迫 停下来的感受。 若是仔细观察会发现UIKit动画有点不太天然。
幸运的是,核心容许经过CASpringAnimation
类为图层属性建立合适的弹簧动画。 CASpringAnimation
在幕后为UIKit建立弹簧动画,可是当咱们直接调用它时,能够设置系统的各类变量,让动画本身稳定下来。 这种方法的缺点是不能设置固定的持续时间(duration);持续时间取决于提供的其它变量,而后系统计算所得。
CASpringAnimation
的一些属性(对应以前振荡系统的参数):
damping
阻尼系数,阻止弹簧伸缩的系数,阻尼系数越大,中止越快
mass
质量,影响图层运动时的弹簧惯性,质量越大,弹簧拉伸和压缩的幅度越大
stiffness
刚度系数(劲度系数/弹性系数),刚度系数越大,形变产生的力就越大,运动越快
initialVelocity
初始速率,动画视图的初始速度大小。速率为正数时,速度方向与运动方向一致,速率为负数时,速度方向与运动方向相反
BahamaAirLoginScreen项目中两个文本框移动动画结束后有个脉动动画,让用户知道该字段处于活动状态并可使用。 然而,动画结束时有些忽然。 经过用CASpringAnimation
来让脉动动画更加天然一点。
把animationDidStop(_:finished:)
动画代码:
// 简单的脉动动画
let pulse = CABasicAnimation(keyPath: "transform.scale")
pulse.fromValue = 1.25
pulse.toValue = 1.0
pulse.duration = 0.25
layer?.add(pulse, forKey: nil)
复制代码
转变为:
let pulse = CASpringAnimation(keyPath: "transform.scale")
pulse.damping = 2.0
pulse.fromValue = 1.25
pulse.toValue = 1.0
pulse.duration = pulse.settlingDuration
layer?.add(pulse, forKey: nil)
复制代码
效果图先后对比:
这边要注意duration
。要使用系统根据当前参数估算的弹簧动画从开始到结束的时间pulse.settlingDuration
。
弹簧系统不能在0.25秒内稳定下来; 提供的变量意味着动画应该在它中止前再运行一段时间。 关于如何切断弹簧动画的视觉演示:
若是抖动时间太长,能够加大阻尼系数damping
,好比:pulse.damping = 7.5
。
CASpringAnimation
预约义的弹簧动画属性的默认值分别是:
damping: 10.0
mass: 1.0
stiffness: 100.0
initialVelocity: 0.0
复制代码
实现文本框的一个代理方法:
func textFieldDidEndEditing(_ textField: UITextField) {
guard let text = textField.text else {
return
}
if text.count < 5 {
let jump = CASpringAnimation(keyPath: "position.y")
jump.fromValue = textField.layer.position.y + 1.0
jump.toValue = textField.layer.position.y
jump.duration = jump.settlingDuration
textField.layer.add(jump, forKey: nil)
}
}
复制代码
上面代码,表示当用户在文本中输入结束后,若是输入字符数小于5,出现一个小幅度的抖动动画,提醒用户过短了。
initialVelocity
起始速度,默认值0。
在设置持续时间前添加,也就是在jump.duration = jump.settlingDuration
前添加:
jump.initialVelocity = 100.0
复制代码
效果:
因为开始时的额外推进,文本框弹的更高了。
mass
增长初始速度会使动画持续时间更长,若是增长质量会怎么样?
在jump.initialVelocity = 100.0
后添加:
jump.mass = 10.0
复制代码
效果:
额外质量使文本框的跳跃的要高了,而且稳定下来的持续时间更久了。
stiffness
刚度,默认是100。越大弹簧更“硬”。
在jump.mass = 10.0
后添加:
jump.stiffness = 1500.0
复制代码
效果:
如今跳跃的不是那么高了。
damping
动画看起来很棒,但彷佛确实有点太长了。 增长系统阻尼以使动画更快地稳定下来。
在jump.stiffness = 1500.0
后添加:
jump.damping = 50.0
复制代码
效果:
在文本框抖动时,添加有颜色的边框。
在textFieldDidEndEditing(_:)
中的textField.layer.add(jump, forKey: nil)
后添加:
textField.layer.borderWidth = 3.0
textField.layer.borderColor = UIColor.clear.cgColor
复制代码
此代码给文本框周围添加了透明边框。 在上面代码后添加:
let flash = CASpringAnimation(keyPath: "borderColor")
flash.damping = 7.0
flash.stiffness = 200.0
flash.fromValue = UIColor(red: 1.0, green: 0.27, blue: 0.0, alpha: 1.0).cgColor
flash.toValue = UIColor.white.cgColor
flash.duration = flash.settlingDuration
textField.layer.add(flash, forKey: nil)
复制代码
运行,放慢效果:
注意:在某些iOS版本中,图层动画会删除文本字段的圆角。此状况可在最后一段代码以后添加此行:
textField.layer.cornerRadius = 5
.
这个改变很方便,只要修改ViewController.swift
中两个函数:
// 背景颜色变化的图层动画
func tintBackgroundColor(layer: CALayer, toColor: UIColor) {
// let tint = CABasicAnimation(keyPath: "backgroundColor")
// tint.fromValue = layer.backgroundColor
// tint.toValue = toColor.cgColor
// tint.duration = 0.5
// layer.add(tint, forKey: nil)
// layer.backgroundColor = toColor.cgColor
let tint = CASpringAnimation(keyPath: "backgroundColor")
tint.damping = 5.0
tint.initialVelocity = -10.0
tint.fromValue = layer.backgroundColor
tint.toValue = toColor.cgColor
tint.duration = tint.settlingDuration
layer.add(tint, forKey: nil)
layer.backgroundColor = toColor.cgColor
}
// 圆角动画
func roundCorners(layer: CALayer, toRadius: CGFloat) {
// let round = CABasicAnimation(keyPath: "cornerRadius")
// round.fromValue = layer.cornerRadius
// round.toValue = toRadius
// round.duration = 0.33
// layer.add(round, forKey: nil)
// layer.cornerRadius = toRadius
let round = CASpringAnimation(keyPath: "cornerRadius")
round.damping = 5.0
round.fromValue = layer.cornerRadius
round.toValue = toRadius
round.duration = round.settlingDuration
layer.add(round, forKey: nil)
layer.cornerRadius = toRadius
}
复制代码
图层上的关键帧动画(Layer Keyframe Animations,CAKeyframeAnimation
)与UIView
上的关键帧动画略有不一样。 视图关键帧动画是将独立简单动画组合在一块儿,能够为不一样的视图和属性设置动画,动画二者之间能够重叠或存在间隙。
相比之下,CAKeyframeAnimation
容许咱们为给定图层上的单个属性设置动画。能够定义动画的不一样关键点,但动画中不能有任何间隙或重叠。 尽管听起来有些限制,但可使用CAKeyframeAnimation
建立一些很是引人注目的效果。
在本章中,将建立许多图层关键帧动画,从很是基本模拟真实世界碰撞到更高级的动画。 在15-Stroke和路径动画中,您将学习如何进一步获取图层动画,并沿给定路径为图层设置动画。
如今,您将在跑步以前走路,并为您的第一层关键帧动画建立一个时髦的摇摆效果。
想想基本动画是如何运做的? 使用fromValue
和toValue
,核心动画会在指定的持续时间内逐步修改这些值之间的特定图层属性。 例如,当在45°和-45°(或π/ 4和-π/ 4)之间旋转图层时,只须要指定这两个值,而后图层渲染全部中间值以完成动画:
CAKeyframeAnimation
使用一组值来完成动画,而不是fromValue
和toValue
。 另外,还须要提供动画应达到每一个值的关键点的时间。
在上面的动画中,图层从45°旋转到-45°,但此次它有两个独立的阶段:
首先,它在动画持续时间的前三分之二内从45°旋转到22°,而后它在剩余的时间内一直旋转到-45°。 实质上,使用关键帧设置动画,要求咱们为设置动画的属性提供关键值,以及在0.0和1.0之间进行相应数量的相对关键时间。
本章的开始项目使用上一章完成的项目
在resetForm()
中添加:
let wobble = CAKeyframeAnimation(keyPath: "transform.rotation")
wobble.duration = 0.25
wobble.repeatCount = 4
wobble.values = [0.0, -.pi/4.0, 0.0, .pi/4.0, 0.0]
wobble.keyTimes = [0.0, 0.25, 0.5, 0.75, 1.0]
heading.layer.add(wobble, forKey: nil)
复制代码
keyTimes
是从0.0
到1.0
的一系列值,而且与values
一一对应。在登陆按钮恢复原状后,heading有一个摇摆的效果:
眼睛敏锐的读者可能已经注意到我尚未介绍过结构属性的动画。 大多数状况下,你能够放弃动画结构的单个组件,例如CGPoint的x组件,或CATransformation3D的旋转组件,可是接下来你会发现动态结构值的动画比 你可能会先考虑一下。
结构体是Swift中的一等公民。 实际上,在使用类和结构之间语法上几乎没有区别。(关于类和结构体可查以撸代码的形式学习Swift-9:类和结构体(Classes and Structures)) 可是,核心动画是一个基于C构建的Objective-C框架,这意味着结构体的处理方式与Swift的结构体大相径庭。 Objective-C API喜欢处理对象,所以结构体须要一些特殊的处理。 这就是为何对图层属性(如颜色或数字)进行动画制做相对容易的缘由,可是为CGPoint
等结构体属性设置动画并不容易。 CALayer
有许多可动画属性,它们包含struct值,包括CGPoint
类型的位置,CATransform3D
类型的转换和CGRect
类型的边界。
为了解决这个问题,Cocoa使用NSValue
类,它可将一个struct
值“包装”为一个核心动画好处理的对象。
NSValue
附带了许多便利初始化程序:
init(cgPoint: CGPoint)
init(cgSize: CGSize)
init(cgRect rect: CGRect)
init(caTransform3D: CATransform3D)
复制代码
使用例子, 如下是使用CGPoint的示例位置动画:
let move = CABasicAnimation(keyPath: "position")
move.duration = 1.0
move.fromValue = NSValue(cgPoint: CGPoint(x: 100.0, y: 100.0))
move.toValue = NSValue(cgPoint: CGPoint(x: 200.0, y: 200.0))
复制代码
在把CGPoint
赋值给fromValue
或toValue
以前,须要把CGPoint
转化为NSValue
,不然动画没法工做。关键帧动画一样如此。
在logIn()
中添加:
let balloon = CALayer()
balloon.contents = UIImage(named: "balloon")!.cgImage
balloon.frame = CGRect(x: -50.0, y: 0.0, width: 50.0, height: 65.0)
view.layer.insertSublayer(balloon, below: username.layer)
复制代码
insertSublayer(_:below)
方法建立了一个图片图层做为view.layer
的子图层。
若是须要在屏幕上显示图像但不须要使用UIView
的全部好处(例如自动布局约束,附加手势识别器等),能够简单地使用上面的代码示例中的CALayer
。
在上面的代码后添加动画代码:
let flight = CAKeyframeAnimation(keyPath: "position")
flight.duration = 12.0
flight.values = [
CGPoint(x: -50.0, y: 0.0),
CGPoint(x: view.frame.width + 50.0, y: 160.0),
CGPoint(x: -50.0, y: loginButton.center.y)
].map { NSValue(cgPoint: $0) }
flight.keyTimes = [0.0, 0.5, 1.0]
复制代码
values
的三个对应点以下:
最后把动画添加到气球图层上,而且设置气球图层最终位置:
balloon.add(flight, forKey: nil)
balloon.position = CGPoint(x: -50.0, y: loginButton.center.y)
复制代码
运行,效果:
本章学习CALayer
的一个子类CAShapeLayer
,它能够在屏幕上绘制各类形状,从很是简单到很是复杂均可以。
本章的开始项目 MultiplayerSearch 模拟了正在搜索在线对手的战斗游戏的起始屏幕。其中一个视图控制器显示一个漂亮的背景图像,一些标签,一个”Search Again“按钮(默认是透明的),和两个头像图像,其中一个将是空的,直到应用程序”找到“一个对手。
两个头像都是AvatarView
类的一个实例。 下面开始完成一些头像视图的效果。 打开AvatarView.swift
,会发现有几个已定义的属性,它们分别表示:
photoLayer
:头像的图片图层。 circleLayer
:用于绘制圆的形状图层。 maskLayer
:另外一个用于绘制蒙版的形状图层。 label
:显示玩家姓名的标签。
上面的组件已经存在于项目中,但还没有添加到视图中,第一个任务就是把它们添加动视图中。 将如下代码添加到didMoveToWindow()
:
photoLayer.mask = maskLayer
复制代码
这简单地用maskLayer
中的圆形掩盖方形图像。
还能够经过@IBDesignable
(关于@IBDesignable
,可查看iOS tutorial 8:使用IBInspectable 和 IBDesignable定制UI)在storyboard中看到设置属性。
运行效果:
如今将圆形边框图层添加到头像视图图层,在didMoveToWindow()
中添加代码:
layer.addSublayer(circleLayer)
复制代码
这时的效果为:
添加名字标签:
addSubview(label)
复制代码
下面建立相似两个物体相撞,而后弹开的反弹(bounce-off)动画。
在ViewController
中建立searchForOpponent()
函数,并在viewDidAppear
中调用:
func searchForOpponent() {
let avatarSize = myAvatar.frame.size
let bounceXOffset: CGFloat = avatarSize.width/1.9
let morphSize = CGSize(width: avatarSize.width * 0.85, height: avatarSize.height * 1.1)
}
复制代码
bounceXOffset
是相互反弹时应移动的水平距离。
morphSize
是头像碰撞后的形变大小(宽度变小,长度变大)。
在searchForOpponent()
里继续添加:
let rightBouncePoint = CGPoint(x: view.frame.size.width/2.0 + bounceXOffset, y: myAvatar.center.y)
let leftBouncePoint = CGPoint(x: view.frame.size.width/2.0 - bounceXOffset, y: myAvatar.center.y)
myAvatar.bounceOff(point: rightBouncePoint, morphSize: morphSize)
opponentAvatar.bounceOff(point: leftBouncePoint, morphSize: morphSize)
复制代码
上面的bounceOff(point:morphSize:)
方法,两个参数分别表明头像移动的位置和变形的大小。在AvatarView
中添加:
func bounceOff(point: CGPoint, morphSize: CGSize) {
let originalCenter = center
UIView.animate(withDuration: animationDuration, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.0, animations: {
self.center = point
}, completion: {_ in
})
UIView.animate(withDuration: animationDuration, delay: animationDuration, usingSpringWithDamping: 0.7, initialSpringVelocity: 1.0, animations: {
self.center = originalCenter
}) { (_) in
delay(seconds: 0.1) {
self.bounceOff(point: point, morphSize: morphSize)
}
}
}
复制代码
上面的两个动画分别是,使用弹簧动画将头像移动到指定位置 和 使用弹簧动画将头像移动到原来位置。此时效果以下:
实际生活中,两个物体相撞时,有一个短期暂停,而且物体变形(”压扁“的效果)。下面就实现这种效果。
在bounceOff(point:morphSize:)
添加:
let morphedFrame = (originalCenter.x > point.x) ?
CGRect(x: 0.0, y: bounds.height - morphSize.height, width: morphSize.width, height: morphSize.height) :
CGRect(x: bounds.width - bounds.width, y: bounds.height - morphSize.height, width: morphSize.width, height: morphSize.height)
复制代码
经过originalCenter.x > point.x
来判断是左边头像仍是右边头像。
在bounceOff(point:morphSize:)
继续添加:
let morphAnimation = CABasicAnimation(keyPath: "path")
morphAnimation.duration = animationDuration
morphAnimation.toValue = UIBezierPath(ovalIn: morphedFrame).cgPath
morphAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
circleLayer.add(morphAnimation, forKey: nil)
复制代码
经过UIBezierPath
建立椭圆。
运行后,效果有点问题:
只有边框图层发生了变形,图片图层没有变化。
把morphAnimation
动画添加到蒙版图层:
maskLayer.add(morphAnimation, forKey: nil)
复制代码
这样的效果就好不少:
在searchForOppoent()
里最后添加delay(seconds: 4.0, completion: foundOppoent)
,而后在ViewController
中添加:
func foundOpponent() {
status.text = "Connecting..."
opponentAvatar.image = UIImage(named: "avatar-2")
opponentAvatar.name = "Andy"
}
复制代码
利用延迟来模拟在寻找对手。
在foundOpponent()
里添加delay(seconds: 4.0, completion: connectedToOpponent)
,而后而后在ViewController
中添加:
func connectedToOpponent() {
myAvatar.shouldTransitionToFinishedState = true
opponentAvatar.shouldTransitionToFinishedState = true
}
复制代码
shouldTransitionToFinishedState
是AvatarView
中自定义的属性,用于判断链接是否完成,在下面使用。
在connectedToOpponent()
里添加delay(seconds: 1.0, completion: completed)
,而后而后在ViewController
中添加:
func completed() {
status.text = "Ready to play"
UIView.animate(withDuration: 0.2) {
self.vs.alpha = 1.0
self.searchAgain.alpha = 1.0
}
}
复制代码
对手找到后,修改状态语,并显示从新搜索按钮。
效果:
在AvatarView
中添加一个属性var isSquare = false
,用于判断头像是否须要转换为正方形。
在bounceOff(point:morphSize:)
的第一个动画(头像移动到指定位置)的 completion
闭包中添加:
if self.shouldTransitionToFinishedState {
self.animateToSquare()
}
复制代码
其中animateToSquare()
为:
// 变换为正方形动画
func animateToSquare() {
isSquare = true
let squarePath = UIBezierPath(rect: bounds).cgPath
let morph = CABasicAnimation(keyPath: "path")
morph.duration = 0.25
morph.fromValue = circleLayer.path
morph.toValue = squarePath
circleLayer.add(morph, forKey: nil)
maskLayer.add(morph, forKey: nil)
circleLayer.path = squarePath
maskLayer.path = squarePath
}
复制代码
在bounceOff(point:morphSize:)
的第二个动画(头像移动到原来位置)的 completion
闭包添加判断:
if !self.isSquare {
self.bounceOff(point: point, morphSize: morphSize)
}
复制代码
这样的最终效果就是:
本章经过之前iOS的屏幕“滑动解锁”效果来学习渐变更画(Gradient Animations)。
开始项目 SlideToReveal是一个简单的单页面项目,只有一个显示时间的UILabel
,和一个以后用于渐变更画的自定义UIView
子类AnimateMaskLabel
。
CAGradientLayer
是CALayer
的另外一个子类,专门用于渐变的图层。
配置CAGradientLayer
,在属性gradientLayer
定义的函数块中添加:
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
复制代码
这定义了渐变的方向及其起点和终点。
let gradientLayer: CAGradientLayer = { let gradientLayer = CAGradientLayer() gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5) gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5) ... }() 复制代码
这种写法表示定义函数后直接调用,返回值直接给属性。这中写法在其它语言中也比较常见,好比JS。
继续添加:
let colors = [
UIColor.black.cgColor,
UIColor.white.cgColor,
UIColor.black.cgColor
]
gradientLayer.colors = colors
let locations: [NSNumber] = [0.25, 0.5, 0.75]
gradientLayer.locations = locations
复制代码
上面的定义方式和前面学习的图层关键帧动画 中的values
和keyTimes
有点相似。
结果就是渐变以黑色开始,中间白色,最后为黑色。经过locations
指定这些颜色应该出如今渐变过程当中的确切位置。固然也是能够不少个颜色点,和对应位置点的。
上面的效果就相似:
在layoutSubviews()
中定义渐变图层的frame
:
gradientLayer.frame = bounds
layer.addSublayer(gradientLayer)
复制代码
这就把渐变的图层定义在AnimateMaskLabel
。
在didMoveToWindow()
中添加:
let gradientAnimation = CABasicAnimation(keyPath: "locations")
gradientAnimation.fromValue = [0.0, 0.0, 0.25]
gradientAnimation.toValue = [0.75, 1.0, 1.0]
gradientAnimation.duration = 3.0
gradientAnimation.repeatCount = .infinity
gradientLayer.add(gradientAnimation, forKey: nil)
复制代码
repeatCount
设置为无穷大,动画持续3秒并将永远重复。效果以下:
上面的效果可能一时很差理解,若是把渐变图层的locations
分别设置成[0.0, 0.0, 0.25]
和[0.75, 1.0, 1.0]
,也就是动画开始点和结束点,状况分别是:
动画的效果就是前者的状态到后者的状态,这样就方便理解了。
这看起来很漂亮,但渐变宽度有点小。 只需放大渐变边界,就会获得更温和的渐变。 在layoutSubviews()
中找到gradientLayer.frame = bounds
行,替代为:
gradientLayer.frame = CGRect(x: -bounds.size.width, y: bounds.origin.y, width: 3 * bounds.size.width, height: bounds.size.height)
复制代码
这会将渐变框设置为可见区域宽度的三倍。 动画进入视图,直接穿过它,并从右侧退出:
效果:
在AnimateMaskLabel
中创造一个文本属性:
let textAttributes: [NSAttributedString.Key: Any] = {
let style = NSMutableParagraphStyle()
style.alignment = .center
return [
NSAttributedString.Key.font: UIFont(name: "HelveticaNeue-Thin", size: 28.0)!,
NSAttributedString.Key.paragraphStyle: style
]
}()
复制代码
接下来,须要将文本渲染为图像。 在text
属性的属性观察者中的setNeedsDisplay()
以后添加如下代码:
let image = UIGraphicsImageRenderer(size: bounds.size).image { (_) in
text.draw(in: bounds, withAttributes: textAttributes)
}
复制代码
在这里,使用图像渲染器来设置上下文。
使用该图像在渐变图层上建立蒙版,在上面代码后继续添加:
let maskLayer = CALayer()
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.frame = bounds.offsetBy(dx: bounds.size.width, dy: 0)
maskLayer.contents = image.cgImage
gradientLayer.mask = maskLayer
复制代码
如今效果:
在viewDidLoad()
中添加:
let swipe = UISwipeGestureRecognizer(target: self, action: #selector(ViewController.didSlide))
swipe.direction = .right
slideView.addGestureRecognizer(swipe)
复制代码
效果:
修改渐变的图层的colors
和locations
,然以前的黑白变成彩色:
let colors = [
UIColor.yellow.cgColor,
UIColor.green.cgColor,
UIColor.orange.cgColor,
UIColor.cyan.cgColor,
UIColor.red.cgColor,
UIColor.yellow.cgColor
]
复制代码
let locations: [NSNumber] = [0.0, 0.0, 0.0, 0.0, 0.0, 0.25]
复制代码
并修改动画的fromValue
和toValue
:
gradientAnimation.fromValue = [0.0, 0.0, 0.0, 0.0, 0.0, 0.25]
gradientAnimation.toValue = [0.65, 0.8, 0.85, 0.9, 0.95, 1.0]
复制代码
效果:
本章的最终效果:
注: stroke 可翻译成 笔画,但好像又不当恰当,就干脆不翻译😏。
开始项目 PullToRefresh
有一个TableView,下拉新视图保持可见状态四秒钟,而后缩回。本章就是在这个下拉视图中作一个相似菊花转的动画。
构建动画的第一步是建立一个圆形。 打开RefreshView.swift
并将如下代码添加到init(frame:scrollView:)
中:
// 飞机移动路线图层
ovalShapeLayer.strokeColor = UIColor.white.cgColor
ovalShapeLayer.fillColor = UIColor.clear.cgColor
ovalShapeLayer.lineWidth = 4.0
ovalShapeLayer.lineDashPattern = [2, 3]
let refreshRadius = frame.size.height/2 * 0.8
ovalShapeLayer.path = UIBezierPath(ovalIn: CGRect(x: frame.size.width/2 - refreshRadius, y: frame.size.height/2 - refreshRadius, width: 2 * refreshRadius, height: 2 * refreshRadius)).cgPath
layer.addSublayer(ovalShapeLayer)
复制代码
ovalShapeLayer
是一个类型为CAShapeLayer
的RefreshView
的属性。CAShapeLayer
以前已经学过了, 在这里,只需设置笔触和填充颜色,并将圆直径设置为视图高度的80%,这样可确保造成温馨的边距。
lineDashPattern
属性是设置虚线模式,它是一个数组,其中包含短划线的长度和间隙的长度(以像素为单位),固然还能够设置不少种虚线,详细的可查看官方文档。
在redrawFromProgress()
中添加:
ovalShapeLayer.strokeEnd = progress
复制代码
把飞机图片添加到飞机图层中,在init(frame:scrollView:)
中添加:
// 添加飞机
let airplaneImage = UIImage(named: "airplane.png")!
airplaneLayer.contents = airplaneImage.cgImage
airplaneLayer.bounds = CGRect(x: 0.0, y: 0.0, width: airplaneImage.size.width, height: airplaneImage.size.height)
airplaneLayer.position = CGPoint(x: frame.size.width/2 + frame.size.height/2 * 0.8, y: frame.size.height/2)
layer.addSublayer(airplaneLayer)
airplaneLayer.opacity = 0.0
复制代码
下拉时逐步更改飞机图层的不透明度,在redrawFromProgress()
添加:
airplaneLayer.opacity = Float(progress)
复制代码
在beginRefreshing()
中添加:
let strokeStartAnimation = CABasicAnimation(keyPath: "strokeStart")
strokeStartAnimation.fromValue = -0.5
strokeStartAnimation.toValue = 1.0
let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeEndAnimation.fromValue = 0.0
strokeEndAnimation.toValue = 1.0
复制代码
在beginRefreshing()
的末尾添加如下代码以同时运行两个动画:
let strokeAnimationGroup = CAAnimationGroup()
strokeAnimationGroup.duration = 1.5
strokeAnimationGroup.repeatDuration = 5.0
strokeAnimationGroup.animations = [strokeEndAnimation, strokeEndAnimation]
ovalShapeLayer.add(strokeAnimationGroup, forKey: nil)
复制代码
在上面的代码中,建立一个动画组并重复动画五次。 这应该足够长,以便在刷新视图可见时保持动画运行。 而后,将两个动画添加到组中,并将组添加到加载层。
运行效果:
在12-图层关键帧动画和结构属性 学习了使用values
属性来设置关键帧动画。下面学习另外一种方式使用关键帧动画。
在beginRefreshing()
的末尾添加飞机动画:
// 飞机动画
let flightAnimation = CAKeyframeAnimation(keyPath: "position")
flightAnimation.path = ovalShapeLayer.path
flightAnimation.calculationMode = CAAnimationCalculationMode.paced
let flightAnimationGroup = CAAnimationGroup()
flightAnimationGroup.duration = 1.5
flightAnimationGroup.repeatDuration = 5.0
flightAnimationGroup.animations = [flightAnimation]
airplaneLayer.add(flightAnimationGroup, forKey: nil)
复制代码
CAAnimationCalculationMode.paced
是另外一种控制动画时间的方法,这时核心动画会以恒定的速度设置动画,忽略设置的任何keyTimes
,这对于在任意路径上生成平滑动画很是有用。
CAAnimationCalculationMode
还有其余几种模式,详细可查看官方文档。
运行效果:
这比较奇怪了,✈️移动时,角度也有相应的变化。
在建立flightAnimationGrou
p的行上方插入如下新动画代码,来调整飞机移动时角度
let airplaneOrientationAnimation = CABasicAnimation(keyPath: "transform.rotation")
airplaneOrientationAnimation.fromValue = 0
airplaneOrientationAnimation.toValue = 2.0 * .pi
复制代码
最终效果
本章节学习复制动画(Replicating Animations)。
CAReplicatorLayer
是CALayer
的另外一个子类。它意思很简单,当建立了一些内容 —— 能够是一个形状,一个图像或任何能够用图层绘制的东西 —— 而CAReplicatorLayer
能够在屏幕上复制它,以下所示:
为何须要复制形状或图像?
CAReplicatorLayer
的超级强大之处,在于可让每一个复制体与母体略有不一样。 例如,能够逐步更改每一个副本的颜色。 原始图层多是洋红色,而在建立每一个副本时,将颜色向青色方向改变:
此外,还能够在副本之间应用转换(transform)。 例如,能够在每一个副本之间应用简单的旋转转换,将它们绘制成圆形,以下所示:
但最好的功能是每一个副本都可以设置动画延迟。 当原始内容的instanceDelay
设置0.2秒时,第一个副本将延迟0.2秒执行动画,第二个副本将延迟0.4秒执行动画,第三个副本将延迟0.6秒执行动画,依此类推。
可使用这种方式来建立引人入胜且复杂的动画。
在本章中,将建立一个模仿Siri,听到声音后,根据声音而产生波浪状的动画。这个开始项目 命名为Iris。
这个项目将建立两个不一样的复制。 首先,是在Iris会话时播放的视觉反馈动画,它看起来很像一个迷幻的正弦波:
而后是一个交互式麦克风驱动的音频波,当用户说话时,它将提供视觉反馈:
这两个动画覆盖了CAReplicatorLayer
的大部分功能。
打开Main.storyboard
:
只有一个视图控制器,它具备一个按钮和一个标签。 用户在按下按钮时询问问题; 当他们释放按钮时,Iris会作出回应。 标签用来显示麦克风输入和Iris的答案。
在ViewController.swift
中,按钮事件已链接到操做。当用户触摸按钮时,actionStartMonitoring()
会触发;当用户抬起手指时,actionEndMonitoring()
会触发。
另外还有两个超出本章范围的类:
Assistant
:人工智能助理。它预约义的有趣答案列表,并根据用户的问题说出来。 MicMonitor
:监控iPhone麦克风上的输入,并反复调用您提供的闭包表达式。这是您有机会更新显示的地方。
下面开始!
打开ViewController.swift
并添加如下两个属性:
let replicator = CAReplicatorLayer()
let dot = CALayer()
复制代码
dot
使用CALayer
,用来绘制基本的简单形状。replicator
做为复制器,用来以后复制dot
。
下面添加一些常量 属性:
let dotLength: CGFloat = 6.0
let dotOffset: CGFloat = 8.0
复制代码
doLength
用做点图层的宽度和高度,dotOffset
是每一个点复制体之间的偏移量。
将复制器层添加到视图控制器的视图中,在viewDidLoad()
中添加:
replicator.frame = view.bounds
view.layer.addSublayer(replicator)
复制代码
下一步是设置点图层。 在viewDidLoad()
中添加:
dot.frame = CGRect(x: replicator.frame.size.width - dotLength, y: replicator.position.y, width: dotLength, height: dotLength)
dot.backgroundColor = UIColor.lightGray.cgColor
dot.borderColor = UIColor(white: 1.0, alpha: 1.0).cgColor
dot.borderWidth = 0.5
dot.cornerRadius = 1.5
replicator.addSublayer(dot)
复制代码
先将点图层定位到复制器的右边缘,而后设置图层的背景颜色并添加边框等,最后将点图层加入复制器图层。运行结果:
在继续下面以前,先介绍CAReplicatorLayer
的三个属性: instanceCount
: 副本数 instanceTransform
: 副本之间的转换 instanceDelay
: 副本之间的动画延迟
在viewDidLoad()
中添加:
replicator.instanceCount = Int(view.frame.size.width / dotOffset)
replicator.instanceTransform = CATransform3DMakeTranslation(-dotOffset, 0.0, 0.0)
复制代码
屏幕宽度除以偏移量,根据不一样屏幕宽度设置副本数。好比5.5英寸(宽度为414)的instanceCount
是51,4.7英寸是46 。。。
每一个副本向左(-dotOffset
)移动8 。结果为:
添加一个小测试动画,来了解instanceDelay
的做用。 在viewDidLoad()
的末尾添加:
let move = CABasicAnimation(keyPath: "position.y")
move.fromValue = dot.position.y
move.toValue = dot.position.y - 50.0
move.duration = 1.0
move.repeatCount = 10
dot.add(move, forKey: nil)
复制代码
这个动画很简单,只是把点向上重复移动10次。
在上面代码的的末尾添加:
replicator.instanceDelay = 0.02
复制代码
效果:
在继续以前,须要删除上面的测试动画,除了instanceDelay
。
在本节中,您将学习在Iris讲话时播放的动画。 为此,您将结合使用具备不一样延迟的多个简单动画来产生最终效果。
首先,在startSpeaking()
中添加如下动画:
let scale = CABasicAnimation(keyPath: "transform")
scale.fromValue = NSValue(caTransform3D: CATransform3DIdentity)
scale.toValue = NSValue(caTransform3D: CATransform3DMakeScale(1.4, 15, 1.0))
scale.duration = 0.33
scale.repeatCount = .infinity
scale.autoreverses = true
scale.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
dot.add(scale, forKey: "dotScale")
复制代码
这是一个简单的层动画,重点在CATransform3DMakeScale
的几个参数选择。此处将点图层在垂直方向缩放15倍。
运行,并点击灰色按钮,分别前后调用actionStartMonitoring
,actionEndMonitoring()
,最后调用startSpeaking()
,效果:
能够尝试修改CATransform3DMakeScale
的几个参数和duration
来看看有什么不一样效果。
在startSpeaking()
添加淡出动画:
let fade = CABasicAnimation(keyPath: "opacity")
fade.fromValue = 1.0
fade.toValue = 0.2
fade.duration = 0.33
fade.beginTime = CACurrentMediaTime() + 0.33
fade.repeatCount = .infinity
fade.autoreverses = true
fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
dot.add(fade, forKey: "dotOpacity")
复制代码
与缩放动画的持续时间相同,但延迟0.33秒,透明度从1.0到0.2,当“波浪”充分移动后,开始淡出效果。
当两个动画同时运行时,效果会更好一点:
设置点背景颜色变化动画,在startSpeaking()
添加:
let tint = CABasicAnimation(keyPath: "backgroundColor")
tint.fromValue = UIColor.magenta.cgColor
tint.toValue = UIColor.cyan.cgColor
tint.duration = 0.66
tint.beginTime = CACurrentMediaTime() + 0.28
tint.fillMode = kCAFillModeBackwards
tint.repeatCount = .infinity
tint.autoreverses = true
tint.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
dot.add(tint, forKey: "dotColor")
复制代码
三种动画的效果:
前面已经经过复制器层制做了不少使人眼花缭乱的效果。 因为CAReplicatorLayer
自己就是一个图层,所以也能够为其自身的一些属性设置动画。
能够为CAReplicatorLayer
的基本属性(如position
,backgroundColor
或cornerRadius
)设置动画,也能够经过其特殊的属性设置很是酷的动画。
CAReplicatorLayer
特有的可动画属性包括(前面已经介绍过三个):
instanceDelay
: 副本之间的动画延迟 instanceTransform
:副本之间的转换 instanceColor
: 颜色 instanceRedOffset
,instanceGreenOffset
,instanceBlueOffset
:应用增量以应用于每一个实例颜色组件 instanceAlphaOffset
: 透明度增量
在startSpeaking()
的末尾添加一个动画:
let initialRotation = CABasicAnimation(keyPath: "instanceTransform.rotation")
initialRotation.fromValue = 0.0
initialRotation.toValue = 0.01
initialRotation.duration = 0.33
initialRotation.isRemovedOnCompletion = false
initialRotation.fillMode = kCAFillModeForwards
initialRotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
replicator.add(initialRotation, forKey: "initialRotation")
复制代码
上面只是有一个微小的旋转,效果:
再须要一个上下扭动的效果,添加下面的动画以完成效果:
let rotation = CABasicAnimation(keyPath: "instanceTransform.rotation")
rotation.fromValue = 0.01
rotation.toValue = -0.01
rotation.duration = 0.99
rotation.beginTime = CACurrentMediaTime() + 0.33
rotation.repeatCount = .infinity
rotation.autoreverses = true
rotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
replicator.add(rotation, forKey: "replicatorRotation")
复制代码
这是在instanceTransform.rotation
上运行第二个动画,它在以前第一个动画完成后启动。将旋转从0.01弧度(第一个动画的最终值)设置到-0.01弧度,这就有了扭到的效果(不一样方向的旋转)。 效果:
下面模拟语音助手,伪装回单。startSpeaking()
的开始处添加:
meterLabel.text = assistant.randomAnswer()
assistant.speak(meterLabel.text!, completion: endSpeaking)
speakButton.isHidden = true
复制代码
从Assistant
类中随机得到一个答案,而后在meterLabel
上显示,而且读处答案,读完后调用endSpeaking
方法。这是过程当中按钮须要隐藏。
以后,须要删除全部正在运行的动画,在endSpeaking()
中添加:
replicator.removeAllAnimations()
复制代码
接下来,须要将点图层“优雅”地设置为原始比例的动画, 在endSpeaking()
继续中添加:
let scale = CABasicAnimation(keyPath: "transform")
scale.toValue = NSValue(caTransform3D: CATransform3DIdentity)
scale.duration = 0.33
scale.isRemovedOnCompletion = false
scale.fillMode = kCAFillModeForwards
dot.add(scale, forKey: nil)
复制代码
上面的动画,没有指定fromValue
,会从当前值开始动画,变换为CATransform3DIdentiy
。
最后,删除dot
中当前正在运行的其他动画,并恢复说话按钮状态。 在endSpeaking()
继续中添加:
dot.removeAnimation(forKey: "dotColor")
dot.removeAnimation(forKey: "dotOpacity")
dot.backgroundColor = UIColor.lightGray.cgColor
speakButton.isHidden = false
复制代码
本节的效果:
前面这有Iris回答时,才会有对应波动动画。这一节要作的是,当用户按住按钮说话(问问题)时也就对应波动动画。
在actionStartMonitoring()
中添加:
dot.backgroundColor = UIColor.green.cgColor
monitor.startMonitoringWithHandler { (level) in
self.meterLabel.text = String(format: "%.2f db", level)
}
复制代码
当用户按下说话按钮时,触发actionStartMonitoring
。为了表示“正在收听”,将点图层颜色更改成绿色。
而后在监视器实例上调用startMonitoringWithHandler()
,它的参数是一个闭包块,会被重复执行,获取麦克风分贝数(db)。
这边的分贝数和咱们日常见到分贝数范围有点不一样, 它的值在-160.0 db到0.0 db的范围内,-160.0 db是最安静的,0.0 db意味着很是大的声音。
向上面的闭包中添加一段代码,添加完以下:
monitor.startMonitoringWithHandler { (level) in
self.meterLabel.text = String(format: "%.2f db", level)
let scaleFactor = max(0.2, CGFloat(level) + 50) / 2
}
复制代码
scaleFactor
将存储介于0.1和25.0之间的值。
在ViewController
新加一个属性:
var lastTransformScale: CGFloat = 0.0
复制代码
对于缩放动画,比例不断变化的,lastTransformScale
保存最后一个缩放值。
在上面的麦克风处理闭包中添加用户声音动画:
let scale = CABasicAnimation(keyPath: "transform.scale.y")
scale.fromValue = self.lastTransformScale
scale.toValue = scaleFactor
scale.duration = 0.1
scale.isRemovedOnCompletion = false
scale.fillMode = kCAFillModeForwards
self.dot.add(scale, forKey: nil)
复制代码
最后,保存lastTransformScale
,接着上面的代码添加:
self.lastTransformScale = scaleFactor
复制代码
当用户手指离开按钮时,须要重置动画并中止监听麦克风。 在actionEndMonitoring()
开始处添加:
monitor.stopMonitoring()
dot.removeAllAnimations()
复制代码
这个时候,效果:
仔细以前的效果,我发现用户麦克风输入动画和Iris动画之间是没有过渡,是直接跳过。这是actionEndMonitoring()
中的dot.removeAllAnimations()
形成的。
把dot.removeAllAnimations()
替代为:
// 麦克风输入和Iris动画之间的过渡
let scale = CABasicAnimation(keyPath: "transform.scale.y")
scale.fromValue = lastTransformScale
scale.toValue = 1.0
scale.duration = 0.2
scale.isRemovedOnCompletion = false
scale.fillMode = kCAFillModeForwards
dot.add(scale, forKey: nil)
dot.backgroundColor = UIColor.magenta.cgColor
let tint = CABasicAnimation(keyPath: "backgroundColor")
tint.fromValue = UIColor.green.cgColor
tint.toValue = UIColor.magenta.cgColor
tint.duration = 1.2
tint.fillMode = kCAFillModeBackwards
dot.add(tint, forKey: nil)
复制代码
本章最后的效果:
本文在个人我的博客中地址:系统学习iOS动画之三:图层动画