版本记录
版本号 | 时间 |
---|---|
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)
}
}
后记
本篇主要讲述了一种弹性动画的实现,感兴趣的给个赞或者关注~~~