版本记录
| 版本号 | 时间 |
|---|---|
| V1.0 | 2018.08.26 |
前言
如果你细看了我前面写的有关动画的部分,就知道前面介绍了
CoreAnimation、序列帧以及LOTAnimation等很多动画方式,接下来几篇我们就以动画示例为线索,进行动画的讲解。相关代码已经上传至GitHub - 刀客传奇。感兴趣的可以看我写的前面几篇。
1. 动画示例(一) —— 一种外扩的简单动画
2. 动画示例(二) —— 一种抖动的简单动画
3. 动画示例(三) —— 仿头条一种LOTAnimation动画
4. 动画示例(四) —— QuartzCore之CAEmitterLayer下雪❄️动画
5. 动画示例(五) —— QuartzCore之CAEmitterLayer烟花动画
6. 动画示例(六) —— QuartzCore之CAEmitterLayer、CAReplicatorLayer和CAGradientLayer简单动画
7. 动画示例(七) —— 基于CAShapeLayer图像加载过程的简单动画(一)
8. 动画示例(八) —— UIViewController间转场动画的实现 (一)
9. 动画示例(九) —— 一种复杂加载动画的实现 (一)
开始
首先我们看一个效果

每个好一点的iOS应用程序都有自定义元素,自定义UI,自定义动画等。自定义,自定义,自定义!
如果您希望自己的应用能够脱颖而出,那么您必须花时间添加一些独特的功能,这些功能将为您的应用提供WOW因素。
在本教程中,您将构建一个自定义text field,在其被轻敲时可以生成一个可爱的小弹性反弹动画。
在此过程中,您将使用许多有趣的API:
- 1)
CAShapeLayer - 2)
CADisplayLink - 3)
UIView spring animations - 4)
IBInspectable
下面我们就开始了。
新建立一个工程,该项目基于Single View Application iOS\Application\Single View Application。 它目前在容器视图中有两个text field和一个按钮。

你的目标是在获得焦点时给予弹性弹跳。 你说,你是如何实现这一目标的?

技术很简单:您将使用四个控制点视图和一个CAShapeLayer,然后使用UIView spring animations为控制点设置动画。 当它们正在动画时,你会在它们的位置周围重新绘制形状。
如果这听起来有点复杂,请不要担心! 它比你想象的容易。
Create a Base Elastic View - 创建一个弹性视图
首先,您将创建基本弹性视图,你将它作为子视图嵌入到UITextfield中,你将为这个视图设置动画,让你的控件弹性反弹。
右键单击项目导航器中的ElasticUI组,然后选择New File ...,然后选择iOS / Source / Cocoa Touch Class模板。 点击Next。
调用类ElasticView,在子类的字段中输入UIView,并确保语言为Swift。 单击“下一步”,然后单击Create以选择存储与此新类关联的文件的默认位置。
首先,您需要创建四个控制点视图和一个CAShapeLayer。 添加以下代码,最终得到以下类定义:
import UIKit
class ElasticView: UIView {
private let topControlPointView = UIView()
private let leftControlPointView = UIView()
private let bottomControlPointView = UIView()
private let rightControlPointView = UIView()
private let elasticShape = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupComponents()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupComponents()
}
private func setupComponents() {
}
}
视图和图层可以立即创建。setUpComponents()是从所有初始化路径调用的设置方法。 你现在要实现它。
在setupComponents()中添加以下内容:
elasticShape.fillColor = backgroundColor?.CGColor
elasticShape.path = UIBezierPath(rect: self.bounds).CGPath
layer.addSublayer(elasticShape)
在这里,您要配置形状图层,将其填充颜色设置为与ElasticView的背景颜色相同,并将路径填充为与视图边界相同的大小。 最后,将其添加到图层层次结构中。
接下来,在`setupComponents()``的末尾添加以下代码:
for controlPoint in [topControlPointView, leftControlPointView,
bottomControlPointView, rightControlPointView] {
addSubview(controlPoint)
controlPoint.frame = CGRect(x: 0.0, y: 0.0, width: 5.0, height: 5.0)
controlPoint.backgroundColor = UIColor.blueColor()
}
这会将所有四个控制点添加到您的视图中。 为了帮助调试,这也将控制点的背景更改为蓝色,以便在模拟器中很容易看到。 您将在本教程结束时删除它。
您需要将控制点定位在顶部中心,底部中心,左中心和右侧中心。 这使得当您做远离视图的动画时,可以使用它们的定位在CAShapeLayer中绘制新路径。
你需要经常这样做,所以创建一个新功能来做到这一点。 将以下内容添加到ElasticView.swift:
private func positionControlPoints(){
topControlPointView.center = CGPoint(x: bounds.midX, y: 0.0)
leftControlPointView.center = CGPoint(x: 0.0, y: bounds.midY)
bottomControlPointView.center = CGPoint(x:bounds.midX, y: bounds.maxY)
rightControlPointView.center = CGPoint(x: bounds.maxX, y: bounds.midY)
}
该函数将每个控制点移动到视图边缘上的正确位置。
现在从setupComponents()的末尾调用new函数:
positionControlPoints()
在深入了解动画之前,您将添加一个视图,因此您可以看到ElasticView的工作原理。 为此,您将为sb添加新视图。
打开Main.storyboard,将新的UIView拖到视图控制器的视图中,并将其Custom Class设置为ElasticView。 不要担心它的位置,只要它在屏幕上,你就能看到发生了什么。

Build并运行你的项目

看那个! 四个小蓝方块 - 这些是您在setupComponents中添加的控制点视图。
现在,您将使用它们在CAShapeLayer上创建路径以获得弹性外观。
Drawing Shapes with UIBezierPath - 使用UIBezierPath绘制形状
在深入研究下一系列步骤之前,请考虑如何在2D中绘制内容 - 您依赖于绘制线,特别是直线和曲线。 在绘制任何内容之前,如果要绘制直线,则需要指定起始位置和结束位置;如果要绘制更复杂的内容,则需要指定多个位置。
这些点是CGPoints,您可以在当前坐标系中指定x和y。
当您想要绘制基于矢量的形状(如正方形,多边形和复杂的曲线形状)时,它会变得更复杂一些。
为了模拟弹性效果,您将绘制一个看起来像矩形的二次Bézier曲线(quadratic Bézier),但它将为矩形的每一边都有控制点,这样就可以创建一条曲线来创建弹性效果。
Bézier曲线以PierreBézier命名,他是法国工程师,曾在CAD / CAM系统中表示曲线。 看一下二次Bézier曲线的样子:

蓝色圆圈是您的控制点,它们是您之前创建的四个视图,红色圆点是矩形的角。
注意:Apple有一个针对UIBezierPath的深入的类参考文档。 如果您想深入了解如何创建路径,那么值得一试。
现在是时候将理论付诸实践了! 将以下方法添加到ElasticView.swift:
private func bezierPathForControlPoints()->CGPathRef {
// 1
let path = UIBezierPath()
// 2
let top = topControlPointView.layer.presentationLayer().position
let left = leftControlPointView.layer.presentationLayer().position
let bottom = bottomControlPointView.layer.presentationLayer().position
let right = rightControlPointView.layer.presentationLayer().position
let width = frame.size.width
let height = frame.size.height
// 3
path.moveToPoint(CGPointMake(0, 0))
path.addQuadCurveToPoint(CGPointMake(width, 0), controlPoint: top)
path.addQuadCurveToPoint(CGPointMake(width, height), controlPoint:right)
path.addQuadCurveToPoint(CGPointMake(0, height), controlPoint:bottom)
path.addQuadCurveToPoint(CGPointMake(0, 0), controlPoint: left)
// 4
return path.CGPath
}
这个方法做了很多事,这里进行详细拆解:
- 1)创建一个
UIBezierPath来保持你的形状。 - 2)将控制点位置提取为四个常量。 您使用
presentationLayer的原因是在动画期间获取视图的“实时”位置。 - 3)通过使用控制点从矩形的角到角添加曲线来创建路径
- 4)将路径作为
CGPathRef返回,因为这是形状层所期望的。
在为控制点设置动画时,需要调用此方法,因为它可以让您不断重新绘制新形状。 你是怎样做的?

CADisplayLink对象是一个计时器,允许您的应用程序将活动与显示器的刷新率同步。 您可以添加目标和在屏幕内容更新时调用的操作。
这是重新绘制路径和更新形状图层的绝佳机会。
首先,每次需要更新时添加一个方法来调用:
func updateLoop() {
elasticShape.path = bezierPathForControlPoints()
}
然后,通过将以下变量添加到ElasticView.swift来创建显示链接:
private lazy var displayLink : CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: Selector("updateLoop"))
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
return displayLink
}()
这是一个懒加载变量,意味着在访问它之前不会创建它。 每次屏幕更新时,它都会调用updateLoop()函数。
您将需要方法来启动和停止链接,因此添加以下内容:
private func startUpdateLoop() {
displayLink.paused = false
}
private func stopUpdateLoop() {
displayLink.paused = true
}
每当控制点移动时,你已准备好绘制一条新路径。 现在,你必须移动他们!
UIView Spring Animations - UIView弹性动画
Apple非常擅长在每个iOS版本中添加新功能,spring animations的添加可以轻松提升应用程序的WOW因素。
它允许您使用自定义阻尼和速度(amping and velocity)为元素设置动画,使其更加特殊和有弹性!
将以下方法添加到ElasticView.swift以使这些控制点移动:
func animateControlPoints() {
//1
let overshootAmount : CGFloat = 10.0
// 2
UIView.animateWithDuration(0.25, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 1.5,
options: nil, animations: {
// 3
self.topControlPointView.center.y -= overshootAmount
self.leftControlPointView.center.x -= overshootAmount
self.bottomControlPointView.center.y += overshootAmount
self.rightControlPointView.center.x += overshootAmount
},
completion: { _ in
// 4
UIView.animateWithDuration(0.45, delay: 0.0, usingSpringWithDamping: 0.15, initialSpringVelocity: 5.5,
options: nil, animations: {
// 5
self.positionControlPoints()
},
completion: { _ in
// 6
self.stopUpdateLoop()
})
})
}
以下是逐步细分:
- 1)
overshootAmount是控制点移动的量。 - 2)在
spring animation中包含即将到来的UI更改,持续时间为五分之一秒。如果您不熟悉spring animation但擅长物理,请查看UIView类参考,了解阻尼和速度变量的详细说明。对于我们其余的只要知道这些变量控制着动画如何反弹。使用数字来寻找感觉正确的配置是很正常的。 - 3)向上,向左,向下或向右移动控制点 - 这将是动画。
- 4)创建另一个弹簧动画以反弹所有内容。
- 5)重置控制点位置 - 这也将是动画。
- 6)一旦停止移动,停止
display link。
到目前为止,您尚未调用animateControlPoints。自定义控件的主要目的是在点击它时进行动画处理,因此调用上述方法的最佳位置是在touchBegan中。
添加以下内容:
override func touchesBegan(touches: Set, withEvent event: UIEvent) {
startUpdateLoop()
animateControlPoints()
}
Build并运行,然后点击View。

Refactoring and Polishing
现在你已经看到了很酷的动画,但你还有一些工作要做,以使你的ElasticView更抽象。
清除的第一个障碍是overshootAmount。 目前,它的硬编码值为10,但以编程方式和Interface Builder更改其值非常棒。

Xcode 6.0的一个新功能是@IBInspectable,这是一种通过界面构建器设置自定义属性的好方法。
您将通过添加overshootAmount作为@IBInspectable属性来利用这个非常棒的新功能,这样您创建的每个ElasticView都可以具有不同的值。
将以下变量添加到ElasticView:
@IBInspectable var overshootAmount : CGFloat = 10
通过替换此行来引用animateControlPoints()中的属性:
let overshootAmount : CGFloat = 10.0
以及下面这行
let overshootAmount = self.overshootAmount
转到Main.storyboard,单击ElasticView并选择Attributes Inspector选项卡。

您会注意到一个新选项卡,其中显示了您的视图名称以及一个名为Overshoot A的input field
对于使用@IBInspectable声明的每个变量,您将在Interface Builder中看到一个新的输入字段,您可以在其中编辑其值。
要查看此操作,请复制ElasticView,以便最终得到两个视图,并将新视图放在当前视图上方,就像这样。

在新视图中将原始视图中的Overshoot Amount值更改为20和40。
Build并运行。 点按两个视图即可查看差异。 如您所见,动画略有不同,并且取决于您在界面构建器中输入的数量。

尝试将值更改为-40而不是40,并查看会发生什么。 您可以看到控制点向内动画,但背景似乎没有变化。

你一定可以解决这个问题吧!
我会给你一个线索:你需要在setupComponents方法中改变一些东西。 自己尝试一下,但是如果你遇到困难,请看看下面的解决方案。
// You have to change the background color of your view after the elasticShape is created, otherwise the view and layer have the same color
backgroundColor = UIColor.clearColor()
clipsToBounds = false
干得好,你终于完成了你的ElasticView。
现在您已经拥有了ElasticView,您可以将其嵌入到不同的控件中,例如text fields或按钮。
Making an Elastic UITextfield - 做一个有弹性的UITextfield
现在您已经构建了弹性视图的核心功能,接下来的任务是将其嵌入到自定义text fields中。
右键单击项目导航器中的ElasticUI组,然后选择New File…。 选择iOS / Source / Cocoa Touch Class模板,然后单击Next。
调用类ElasticTextField,在子类字段中输入UITextfield并确保语言为Swift。 单击Next,然后单击Create。
打开ElasticTextField.swift并用以下内容替换其内容:
import UIKit
class ElasticTextField: UITextField {
// 1
var elasticView : ElasticView!
// 2
@IBInspectable var overshootAmount: CGFloat = 10 {
didSet {
elasticView.overshootAmount = overshootAmount
}
}
// 3
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
// 4
func setupView() {
// A
clipsToBounds = false
borderStyle = .None
// B
elasticView = ElasticView(frame: bounds)
elasticView.backgroundColor = backgroundColor
addSubview(elasticView)
// C
backgroundColor = UIColor.clearColor()
// D
elasticView.userInteractionEnabled = false
}
// 5
override func touchesBegan(touches: Set, withEvent event: UIEvent) {
elasticView.touchesBegan(touches, withEvent: event)
}
}
那里有很多事情发生!这是一步一步细分:
- 1)这是保存
ElasticView的A属性。 - 2)一个名为
overshootAmount的IBInspectable变量,因此您可以通过Interface Builder更改控件的弹性。它会覆盖didSet并只设置弹性视图的overshootAmount。 - 3)该类的标准初始值设定项,因此调用设置方法。
- 4)这是您设置
text field的位置。让我们进一步细分:- A)将
clipsToBounds更改为false。这使弹性视图超出其父级边界,并将UITextField的边框样式更改为.None以展平控件。 - B)创建并添加
ElasticView作为控件的子视图。 - C)更改控件的
backgroundColor以使其clear;你这样做是因为你希望ElasticView决定颜色。 - D)最后,这会将
ElasticView的userInteractionEnabled设置为false。否则,它会从您的控制中窃取。 - E)覆盖
touchesBegan并将其转发到您的ElasticView,以便它可以设置动画。
- A)将
转到Main.storyboard,选择UITextfield的两个实例,并在Identity Inspector中将它们的类从UITextField更改为ElasticTextField。
此外,请确保删除为测试目的而添加的两个ElasticView实例。

Build并运行,点按您的textfield,注意它实际上是如何工作的:

原因是当您在代码中创建ElasticView时,它会获得清晰的背景颜色,并将其传递给形状图层。
要更正此问题,只要在视图上设置新的背景颜色,就需要一种方式将颜色传递到shape layer。
Forwarding the Background Color - 传递背景色
因为要使用elasticShape作为视图的主要背景,所以必须覆盖ElasticView中的backgroundColor。
将以下代码添加到ElasticView.swift:
override var backgroundColor: UIColor? {
willSet {
if let newValue = newValue {
elasticShape.fillColor = newValue.CGColor
super.backgroundColor = UIColor.clearColor()
}
}
}
在设置值之前,将调用willSet。 检查是否已传递值,然后将elasticShape的fillColor设置为用户选择的颜色。 然后,您调用super并将其背景颜色设置为clear。
Build并运行,你应该有一个可爱的弹性控制。

Final Tweaks - 最后的调整
请注意UITextField的占位符文本与左边缘的接近程度。 它有点舒服,你不觉得吗? 你想自己解决这个问题吗?
看下面的解决方案。
// Add some padding to the text and editing bounds
override func textRectForBounds(bounds: CGRect) -> CGRect {
return CGRectInset(bounds, 10, 5)
}
override func editingRectForBounds(bounds: CGRect) -> CGRect {
return CGRectInset(bounds, 10, 5)
}
Removing Debug Information - 删除调试信息
打开ElasticView.swift并从setupComponents中删除以下内容。
controlPoint.backgroundColor = UIColor.blueColor()
到目前为止就全部完成了。
源码
1. ElasticView.swift
import UIKit
class ElasticView: UIView {
private let topControlPointView = UIView()
private let leftControlPointView = UIView()
private let bottomControlPointView = UIView()
private let rightControlPointView = UIView()
private let elasticShape = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupComponents()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupComponents()
}
private func setupComponents() {
elasticShape.fillColor = backgroundColor?.CGColor
elasticShape.path = UIBezierPath(rect: self.bounds).CGPath
layer.addSublayer(elasticShape)
for controlPoint in [topControlPointView, leftControlPointView,
bottomControlPointView, rightControlPointView] {
addSubview(controlPoint)
controlPoint.frame = CGRect(x: 0.0, y: 0.0, width: 5.0, height: 5.0)
}
positionControlPoints()
// You have to change the background color of your view after the elasticShape is created, otherwise the view and layer have the same color
backgroundColor = UIColor.clearColor()
clipsToBounds = false
}
private func positionControlPoints(){
topControlPointView.center = CGPoint(x: bounds.midX, y: 0.0)
leftControlPointView.center = CGPoint(x: 0.0, y: bounds.midY)
bottomControlPointView.center = CGPoint(x:bounds.midX, y: bounds.maxY)
rightControlPointView.center = CGPoint(x: bounds.maxX, y: bounds.midY)
}
private func bezierPathForControlPoints()->CGPathRef {
// 1
let path = UIBezierPath()
// 2
let top = topControlPointView.layer.presentationLayer().position
let left = leftControlPointView.layer.presentationLayer().position
let bottom = bottomControlPointView.layer.presentationLayer().position
let right = rightControlPointView.layer.presentationLayer().position
let width = frame.size.width
let height = frame.size.height
// 3
path.moveToPoint(CGPointMake(0, 0))
path.addQuadCurveToPoint(CGPointMake(width, 0), controlPoint: top)
path.addQuadCurveToPoint(CGPointMake(width, height), controlPoint:right)
path.addQuadCurveToPoint(CGPointMake(0, height), controlPoint:bottom)
path.addQuadCurveToPoint(CGPointMake(0, 0), controlPoint: left)
// 4
return path.CGPath
}
private lazy var displayLink : CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: Selector("updateLoop"))
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
return displayLink
}()
func updateLoop() {
elasticShape.path = bezierPathForControlPoints()
}
private func startUpdateLoop() {
displayLink.paused = false
}
private func stopUpdateLoop() {
displayLink.paused = true
}
@IBInspectable var overshootAmount : CGFloat = 10
func animateControlPoints() {
//1
let overshootAmount = self.overshootAmount
// 2
UIView.animateWithDuration(0.25, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 1.5,
options: nil, animations: {
// 3
self.topControlPointView.center.y -= overshootAmount
self.leftControlPointView.center.x -= overshootAmount
self.bottomControlPointView.center.y += overshootAmount
self.rightControlPointView.center.x += overshootAmount
},
completion: { _ in
// 4
UIView.animateWithDuration(0.45, delay: 0.0, usingSpringWithDamping: 0.15, initialSpringVelocity: 5.5,
options: nil, animations: {
// 5
self.positionControlPoints()
},
completion: { _ in
// 6
self.stopUpdateLoop()
})
})
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
startUpdateLoop()
animateControlPoints()
}
override var backgroundColor: UIColor? {
willSet {
if let newValue = newValue {
elasticShape.fillColor = newValue.CGColor
super.backgroundColor = UIColor.clearColor()
}
}
}
}
2. ElasticTextField.swift
import UIKit
class ElasticTextField: UITextField {
// 1
var elasticView : ElasticView!
// 2
@IBInspectable var overshootAmount: CGFloat = 10 {
didSet {
elasticView.overshootAmount = overshootAmount
}
}
// 3
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
// 4
func setupView() {
// A
clipsToBounds = false
borderStyle = .None
// B
elasticView = ElasticView(frame: bounds)
elasticView.backgroundColor = backgroundColor
addSubview(elasticView)
// C
backgroundColor = UIColor.clearColor()
// D
elasticView.userInteractionEnabled = false
}
// 5
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
elasticView.touchesBegan(touches, withEvent: event)
}
// Add some padding to the text and editing bounds
override func textRectForBounds(bounds: CGRect) -> CGRect {
return CGRectInset(bounds, 10, 5)
}
override func editingRectForBounds(bounds: CGRect) -> CGRect {
return CGRectInset(bounds, 10, 5)
}
}
后记
本篇主要讲述了一种弹性动画的实现,感兴趣的给个赞或者关注~~~
