iOS中的动画可以分为很多种类型,根据图层显示可以分为隐式动画和显示动画。根据动画在iOS中实现的“位置”可以分为显示层( UIView)动画和内容层(Layer)动画。
隐式动画
所谓的隐式动画,之所以叫隐式是因为我们并没有指定任何动画的类型。我们仅仅改变了一个属性,然后 Core Animation 来决定如何并且何时去做动画。
但当你改变一个属性,Core Animation 是如何判断动画类型和持续时间的呢?实际上动画执行的时间取决于当前事务的设置,动画类型取决于图层行为。
事务实际上是 Core Animation 用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。
事务是通过 CATransaction 类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。
Core Animation在每个run loop周期中自动开始一次新的事务(run loop是iOS负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的东西),即使你不显式的用 [CATransaction begin] 开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。
我们来对颜色渐变的例子使用一个不同的行为,通过给 colorLayer 设置一个自定义的 actions 字典。我们也可以使用委托来实现,但是 actions 字典可以写更少的代码。那么到底改如何创建一个合适的行为对象呢?
这里我们使用的是一个实现了 CATransaction 的实例,叫做推进过渡。
class ViewController: UIViewController {
@IBOutlet weak var layerView: UIView!
var colorLayer: CALayer?
override func viewDidLoad() {
super.viewDidLoad()
self.colorLayer = CALayer.init()
self.colorLayer?.frame = CGRect.init(x: 50, y: 50, width: 100, height: 100)
let red = CGFloat((arc4random_uniform(255) + 1)) / 255.0
let green = CGFloat((arc4random_uniform(255) + 1)) / 255.0
let blue = CGFloat((arc4random_uniform(255) + 1)) / 255.0
self.colorLayer?.backgroundColor = UIColor.init(red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: 1.0).cgColor.cgColor
let transtion = CATransition.init()
transtion.type = .push
transtion.subtype = .fromLeft
self.colorLayer?.actions = ["backgroundColor": transtion]
self.layerView.layer.addSublayer(self.colorLayer!)
}
@IBAction func changeLayerColor(_ sender: Any) {
let red = CGFloat((arc4random_uniform(255) + 1)) / 255.0
let green = CGFloat((arc4random_uniform(255) + 1)) / 255.0
let blue = CGFloat((arc4random_uniform(255) + 1)) / 255.0
self.colorLayer?.backgroundColor = UIColor.init(red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: 1.0).cgColor
}
}
效果如图所示:
显式动画
定义显示动画的时候,我们不必定义CALayer的变化,也不必执行它,而是通过CABasicAnimation逐个定义动画,其中每个动画都含有各自的属性,然后通过addAnimation:方法添加到图层中
CAAnimationDelegate在任何头文件中都找不到,但是可以在CAAnimation头文件或者苹果开发者文档中找到相关函数。在这个例子中,我们用- animationDidStop: finished: 方法在动画结束之后来更新图层backgroundColor的。
当更新属性的时候,我们需要设置一个新的事务,并且禁用图层行为。否则动画会发生两次,一个是因为显式的 CABasicAnimation ,另一次是因为隐式动画,具体实现代码如下。
class ViewController: UIViewController {
@IBOutlet weak var layerView: UIView!
var colorLayer: CALayer?
override func viewDidLoad() {
super.viewDidLoad()
self.colorLayer = CALayer.init()
self.colorLayer?.frame = CGRect.init(x: (self.view.bounds.size.width - 100) * 0.5, y: (self.view.bounds.size.height - 100) * 0.5, width: 100, height: 100)
self.colorLayer?.backgroundColor = UIColor.red.cgColor
self.layerView.layer.addSublayer(self.colorLayer!)
}
@IBAction func changeLayerColor(_ sender: Any) {
let color = UIColor.init(red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: 1.0)
let baseAnimation = CABasicAnimation.init()
baseAnimation.keyPath = "backgroundColor"
baseAnimation.toValue = color.cgColor
baseAnimation.duration = 1
baseAnimation.delegate = self
self.colorLayer?.add(baseAnimation, forKey: nil)
}
}
extension ViewController: CAAnimationDelegate {
func animationDidStop(_ anim: CABasicAnimation, finished flag: Bool) {
CATransaction.begin()
CATransaction.setDisableActions(true) // 去隐式动画
self.colorLayer?.backgroundColor = anim.toValue as! CGColor
CATransaction.commit()
}
}
效果如图:
一、显示层动画
1. UIView 动画
iOS 在 UIView 的显示层已经帮我们把动画效果的功能完成了,在 UIView 图层中不仅集成了动画的线性渐变方法,而且动画的加速、减速以及复杂的动画变化时间函数、运动路径函数也已经为我们集成好了。
1.1 UIView 代码块调用
open class func animate(withDuration duration: TimeInterval, animations: @escaping () -> Void)
1.2 UIView animation动画(iOS 13 中已弃用,需要改成基于代码块的动画方式)
override func viewWillAppear(_ animated: Bool) {
UIView.beginAnimations(nil, context: nil) // 动画开始
UIView.setAnimationDuration(1.0) // 动画周期设置
self.funView.frame = CGRect.init(x: 20, y: 60, width: self.funView.frame.size.width, height: self.funView.frame.size.height) // 动画位置设置
UIView.commitAnimations() // 动画提交
}
1.3 CoreGraphics
UIView有一个非常重要的动画属性transform, 该属性继承于CGAffineTransform,“CG" 实际上是CoreGraphics框架的缩写。可见transform属性是核心绘图框架与UIView之间的桥梁,借助于这一属性可以制作很多高级的动画效果。
transform最常用的三种动画分别是缩放、旋转和位移。下面介绍了UIView的缩放效果,即几何尺寸的变化,下 面将采用transform 属性完成这一动画效果。下面是具体的代码实现。
override func viewWillAppear(_ animated: Bool) {
// CGAffineTransform: 缩放
UIView.beginAnimations(nil, context: nil) // 动画开始
UIView.setAnimationDuration(1) // 动画周期设置
loginButton!.transform.CGAffineTransform(scalex: 0.7, y: 1.2)
UIView.commitAnimations() // 动画提交
}
frame和transform
- frame是一个复合属性,由center、bounds和transform共同计算而来。
- transform改变,frame会受到影响,但是center和bounds不会受到影响。也就是你使用transform来缩放,bounds是不会变的。那么由center和bounds计算得到的frame是永远保持transform为identity时的状态。这也是为什么把transform设为identity后,view会回归到最初状态。
1.4 显示层关键帧动画
关键方法
@available(iOS 7.0, *)
open class func animateKeyframes(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.KeyframeAnimationOptions = [], animations: @escaping () -> Void) async -> Bool
添加起始帧和结束帧可以基本完成飞机降落的动画效果,但是要想实现更为精确的控制就必须再多添加几帧。比如飞机在0.5s时,控制飞机运动到(150,175)的位置处。可以在动画的0.5s时添加一一个 关键帧,关键帧的内容描述当前飞机运动到(150, 175) 处。
class ViewController: UIViewController {
var imageViewPlane: UIImageView = UIImageView.init()
override func viewDidLoad() {
self.imageViewPlane.image = UIImage.init(named: "飞机")
self.imageViewPlane.frame = CGRect.init(x: 0, y: 100, width: 50, height: 50)
self.imageViewPlane.contentMode = .scaleAspectFit
self.view.addSubview(self.imageViewPlane)
}
func addKeyFrames() -> () {
// 飞机降落
UIView.animateKeyframes(withDuration: 2, delay: 0, options: UIView.KeyframeAnimationOptions.calculationModeCubic) {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1/2) {
self.imageViewPlane.frame = CGRect.init(x: self.view.bounds.width - 50, y: 300, width: 30, height: 30)
}
UIView.addKeyframe(withRelativeStartTime: 1/2, relativeDuration: 1/2) {
self.imageViewPlane.frame = CGRect.init(x: self.view.bounds.width - 100, y: 300, width: 100, height: 100)
}
} completion: { finish in
print("done")
}
}
}
显示层逐帧动画
根据字面意思不难理解,逐帧动画实现的动画效果就是将图片一帧帧地逐帧渲染,所以首先需要准备逐帧动画实现的素材。将图片序列按照飞机飞行的连续时间状态在很短的时间内依次展示出来,即可实现动画的逐帧展现效果。
如何让图片按照连续的顺序和一定的时间间隔显示图片呢?这里为大家介绍两种逐帧刷新方法,一种是基于定时器的逐帧刷新,这种方法经常使用在动画帧率不高,且帧率之间的时间间隔并不十分严格的情况下。另一种是基于 CADisplayLink 的帧刷新效果,该方法刷帧频率高达每秒 60帧,且非常精确。这里先为大家介绍基于定时器的逐帧动画效果。
二、内容层动画
Core Animation
CoreAnimation 为 iOS 核心动画,它提供了一组丰富的 API 可以用于制作各种高级炫酷的动画效果。Core Animation 来自 iOS 的 QuartzCore.framework 框架,它还具有以下特点。
-
直接作用于
CALayer图层上,而非UIView上。 -
Core Animation的执行过程在后台执行,不阻塞主线程。 -
可以利用
CALayer绝大多数属性制作高级动画效果。
下面来看看 Core Animation 下各种常用动画类的继承关系,如图所示。
@protocol CAMediaTiming有很多动画公共的属性,比如常见的duration(动画执行周期)、speed(速度)、repeatCount(重复次数)等一些公共的属性都放在CAMediaTiming中。CAAnimation主要用于实现动画的委托代理方法,比如动画开始事件和动画结束事件都是通过CAAnimation类来实现的。CAPropertyAnimation为属性动画,分为基础动画和关键帧动画。一会儿为大家介绍的CALayer内容层动画合集都是通过CABasicAnimation来实现的。CAKeframeAnimation为关键帧动画,与UIView中的关键帧动画实现原理类似。CAAnimationGroup组合动画,顾名思义,利用这个类可以把其他常用动画组合在一起实现。CATransition专场动画,主要用于视图控制器或者多View之间的视图切换场景。
1. CABasicAnimation
1.1 CALayer层动画合集
1.1.1 位置动画
// 位置
let animation:CABasicAnimation = CABasicAnimation()
animation.keyPath = "position"
let positionX: CGFloat = self.loginBtn!.frame.origin.x + 0.5 * self.loginBtn!.frame.size.width;
let positionY: CGFloat = self.loginBtn!.frame.origin.y + 0.5 * self.loginBtn!.frame.size.height + 100;
animation.toValue = NSValue(cgPoint: CGPoint (x: positionX, y: positionY))
animation.duration = 2.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
self.loginBtn?.layer.add(animation, forKey: nil)
代码第1行初始化一个 CABasicAnimation 动画实例对象。第2行设置动画实例对象的效果。比如这里设置的是position,表明当前是为了修改登录按钮的位置信息。
1.1.2 缩放动画
// 缩放
let animation: CABasicAnimation = CABasicAnimation()
animation.keyPath = "transform.scale.x"
animation.fromValue = 1.0
animation.toValue = 0.8
animation.duration = 2.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
self.loginBtn?.layer.add (animation, forKey: nil)
代码整体结构与第1.1.1节类似,这里就不再重复。重点来看几个不一样的地方。代码第2行设置UI控件宽度缩放属性名称 transform.scale.x.第3行指明当前缩放系数原始值。原始系数设置为1.0。第4行设置最终缩放系数。
1.1.3 旋转动画
// 旋转
let animation:CABasicAnimation = CABasicAnimation()
animation.keyPath = "transform. rotation"
animation.toValue = 3.14/2
animation.duration = 2.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
self.loginBtn?.layer.add(animation, forKey: nil)
代码的第2行将动画实例 keyPath 属性修改为 transform.rotation,表明当前想实现旋转动画效果。第3行设置旋转最终角度,代码中采用弧度来表示,所以90转换为弧度为3.14/2。
1.1.4 位移动画
// 位移
let animation: CABasicAnimation = CABasicAnimation ()
animation.keyPath = "transform.translation.y"
animation.toValue = 100
animation.duration = 2.0
animation.fillMode = .forwards
animation.isRemovedonCompletion = false
self.loginBtn?.layer.add(animation, forKey: nil)
本节将采用 transform 的 translation 来实现这一动画。 Translation 还有x、y两个属性分别表示在x、y方向上移动
1.1.5 圆角动画
// 圆角
let animation:CABasicAnimation = CABasicAnimation()
animation.keyPath = "cornerRadius"
animation.toValue = 15
animation.duration = 2.0
animation.fillMode = .forwards
animation.isRemovedonCompletion = false
self.loginBtn?.layer.add(animation, forKey: nil)
代码第2行设置 Layer 图层圆角属性 cormnerRadius,第3行设置圆角半径为15。
1.1.6 边框动画
// 边框
self.loginBtn?.layer.borderColor = UIColor.gray.cgColor
self.loginBtn?.layer.cornerRadius = 10.0
let animation: CABasicAnimation = CABasicAnimation()
animation.keyPath = "borderWidth"
animation.toValue = 10
animation.duration = 2.0
animation.fillMode = .forwards
animation.isRemovedonCompletion = false
self.loginBtn?.layer.add(animation, forKey: nil)
代码第1行设置登录按钮边框颜色为灰色,第2行设置按钮圆角效果。第4行设置动画属性为 borderWidth,表明当前动画改变的是边框宽度效果。第5行设置边框最终宽度为10。
1.1.7 颜色渐变动画
// 颜色
let animation: CABasicAnimation = CABasicAnimation()
animation.keyPath = "backgroundColor"
animation.fromValue = UIColor.green.cgColor
animation.toValue = UIColor.red.cgColor
animation.duration = 2.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
self.loginBtn?.layer.add(animation, forKey: nil)
代码第3行表示初始状态登录按钮颜色,第4行表明最终登录按钮的背景颜色。
1.1.8 淡入淡出动画
// 淡入
let animation:CABasicAnimation = CABasicAnimation()
animation.keyPath = "opacity"
animation.fromValue = UIColor.green.cgColor
animation.toValue = 1.0
animation.duration = 2.0
animation.fillMode = .forwards
animation.isRemoved0nCompletion = false
self.loginBtn?.layer.add(animation, forKey: nil)
Opacity 属性和 alpha 属性类似,通过设置 0~ 1.0 的浮点数可以实现透明效果。代码第2行设置动画属性 opacity。第4行设置 opacity 最终属性为1.0,默认情况下登录按钮的 opacity 属性为0,表明按钮初始状态为隐藏,之后慢慢显现,最终出现在界面上。
1.1.9 阴影渐变动画
self.loginBtn?.layer.shadowColor = UIColor.red.cgColor
self.loginBtn?.layer.shadowOpacity = 0.5
let animation:CABasicAnimation = CABasicAnimation()
animation.keyPath = "shadowOffset"
animation.toValue = NSValue(cgSize: CGSize (width: 10, height:10) )
animation.duration = 2.0
animation.fillMode = .forwards
代码第1行设置阴影背景颜色为红色。第2行设置阴影透明度为半透明0.5。第4行设置阴影动画效果属性shadowOffset。第5行设置阴影投影角度,分别向 x、y 轴方向渐变。
2. 关键帧动画
CAKeyframeAnimation的使用很简单,只需在合适的位置设置相应的关键帧即可。而选取合适的位置、设置合适的关键帧都离不开 CAKeyframeAnimation 的各种属性。下面就对 CAKeyframeAnimation 的各种常用属性做一一个解析。
values: 该属性是一个数组类型,数组中的每个元素都描绘了一一个关键帧的相关属性。比如描述关键帧位置的动画时,values描述的是位置信息。描述关键帧淡入淡出动画时,values描述的是透明度渐变信息。keyTimes: 默认情况下,关键帧在动画的展示周期内是均匀播放的,但是如果设置了这个属性,就可以精确控制每个关键帧显示的周期。这个属性的取值范围在0~1之间。所以每个关键帧显示的周期为keyTimes*duration.path: 如果通过values属性可以对动画进行比较细腻的控制,那么通过path属性则可以对动画的细节部分控制得更为精确。因为通过设置CGPathRef\CGMutablePathRef可以让动画按照自己绘制的路径随心所欲地运行。
class ViewController: UIViewController {
@IBOutlet weak var layerView: UIView!
var colorLayer: CALayer?
override func viewDidLoad() {
super.viewDidLoad()
self.colorLayer = CALayer.init()
self.colorLayer?.frame = CGRect.init(x: (self.view.bounds.size.width - 100) * 0.5, y: (self.view.bounds.size.height - 100) * 0.5, width: 100, height: 100)
self.colorLayer?.backgroundColor = UIColor.red.cgColor
self.layerView.layer.addSublayer(self.colorLayer!)
}
@IBAction func changeLayerColor(_ sender: Any) {
let animation = CAKeyframeAnimation.init(keyPath: "backgroundColor")
animation.duration = 2
animation.values = [UIColor.red.cgColor, UIColor.blue.cgColor, UIColor.green.cgColor, UIColor.yellow.cgColor]
self.colorLayer?.add(animation, forKey: nil)
}
}
效果如图:
3. CAEmitterCell 粒子动画
iOS除了前面为大家介绍的动画效果之外,还有一些非常炫酷的粒子特性,通过这些粒子特性可以设计出各种各样炫酷的动画效果。在本节将结合 CAEmitterLayer 和 CAEmitterCell 这两个类为大家实现火焰燃烧粒子动画效果。
在iOS系统中,粒子系统由两部分组成: CAEmitterLayer 和 CAEmitterCell。
CAEmitterLayer为粒子发射图层。该图层主要用于控制粒子展现范围、粒子发射位置、粒子发射形状、渲染模式等属性。通过CAEmitterCell构建的发射单元都受到CAEmitterLayer图层节制,可以说粒子展现必须在CAEmitterLayer图层上来实现。CAEmitterCell粒子发射单元,用于对粒子系统中的单个粒子做更加精细的控制。比如控制粒子的移动速度、方向、范围。在CAEmitterCell类中提供了几十种粒子属性参数设置,所以结合这些属性可以制作各种炫酷的粒子特效动画。
如何在 iOS 中使用这两个类呢?首先这里使用的 CAEmitterLayer 属于 CALayer 图层,所以实例化CAEmitterLayer 图层之后将其添加到相应 View 的 layer 图层上即可。而 CAEmitterLayer 实例对象有一个emitterCells 属性,该属性描绘了所有的粒子实例和相应特性,因此将 CAEmittrClll 实例对象传递给CAEmitterLayer、emitterCells 属性就可以实现粒子动画效果。
class ViewController: UIViewController {
override func viewDidLoad() {
self.view.backgroundColor = UIColor.black
let emitterCell = CAEmitterCell()
emitterCell.name = "fire"
emitterCell.emissionLongitude = CGFloat(M_PI)
emitterCell.velocity = -1 // 粒子速度负数表明向上燃烧
emitterCell.velocityRange = 50 // 粒子速度 范围
emitterCell.emissionRange = 1.1 // 周围发射角度
emitterCell.yAcceleration = -200 // 粒子y方向的加速度分量
emitterCell.scaleSpeed = 0.3 // 缩放比例超大火苗
emitterCell.color = UIColor(red: 0.8, green:0.4, blue:0.2, alpha:0.1).cgColor
emitterCell.contents = UIImage(named: "火焰")!.cgImage
let emitterLayer = CAEmitterLayer()
emitterLayer.position = self.view.center // 粒子发射位置
emitterLayer.emitterSize = CGSize(width: 50, height: 10) // 控制火苗大小
emitterLayer.renderMode = .additive
emitterLayer.emitterMode = .outline
emitterLayer.emitterShape = .line
emitterLayer.emitterCells = [emitterCell]
self.view.layer.addSublayer(emitterLayer)
emitterLayer.setValue(500, forKeyPath: "emitterCells.fire.birthRate")
emitterLayer.setValue(1, forKeyPath: "emitterCells.fire.lifetime")
}
}
代码第2行设置当前视图背景颜色为黑色。第3行定义粒子单元cell第4行定义该粒子单元名称为fire火焰。第5行定义该粒子系统在xy平面的发射角度。第6行设置粒子速度,当前设置为一1,表明沿y轴向反方向运动。第7行设置粒子速度范围为50。第8行设置发射角度为1.1。第9行设置粒子在y轴方向上的加速度分量。第10行描绘粒子的缩放系数。第11行设置当前火焰颜色为红色。既然要实现火焰燃烧的粒子效果,这里必须要设置粒子的“种子”内容。第12行设置粒子效果的内容。第13行定义粒子的发射图层。第14行确定粒子的发射位置。第15行控制当前火苗的大小。第16行到第18行确定当前火苗渲染模式以及发射源模式。第19行将当前的粒子单元部署到粒子发射图层中。第20行将粒子发射图层添加到当前视图控制器的Layer图层上。最后两行设置粒子的生成速度以及粒子生命周期。如图所示为最终实现的“鬼火”燃烧效果。
4. CAGradientLayer光波扫描动画效果
// CAGradientLayer光波扫描动画效果
// part1:设置指纹扫描背景图片
let image: UIImage = UIImage.init(named: "二维码")!
let imageView: UIImageView = UIImageView.init(image: image)
imageView.contentMode = UIView.ContentMode.scaleAspectFit
imageView.frame = self.view.bounds
imageView.center = self.view.center
self.view.backgroundColor = UIColor.black
self.view.addSubview(imageView)
// part2:设置Layer图层属性
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.frame = CGRect.init(x: 100, y: 100, width: 300, height: 300)
gradientLayer.position = imageView.center
imageView.layer.addSublayer(gradientLayer)
gradientLayer.startPoint = CGPoint (x:0, y:0)
gradientLayer.endPoint = CGPoint (x:0, y:1)
let color = UIColor(red: 142.0/255.0, green: 229.0/255.0, blue: 238.0/255.0, alpha: 0.5) //
let color1 = UIColor(red: 61.0/255.0, green: 226.0/255.0, blue: 210.0/255.0, alpha: 0.5) // 青色
// 设置光波颜色梯度
gradientLayer.colors = [UIColor.clear.cgColor, color1.cgColor, color.cgColor]
gradientLayer.locations = [0.0,0.5,0.6]
let gradientAnimation: CABasicAnimation = CABasicAnimation()
gradientAnimation.keyPath = "locations" // 动画属性
gradientAnimation.fromValue = [0.0, 0.1, 0.2]; //动画属性变化起始状态值
gradientAnimation.toValue = [0.8, 0.9, 1.0]; // 动画属性变化终止状态值
gradientAnimation.duration = 3.0; //动画执行周期
gradientAnimation.repeatCount = 10; //动画执行重复次数
gradientLayer.add(gradientAnimation, forKey: nil)
代码第一部分: 添加指纹背景图片,并设置图片拉伸方式和 frame 等属性添加到 self.view 上。
代码第二部分: 初始化 Layer 属性,并将 Layer 图层添加到 imageView 的 Layer 图层上。代码第11行到第14行设置Layer图层的相关属性。
代码第三部分设置 CABasicAnimation 动画。我们需要修改动画的 locations 属性,并设置动画的起始位置为[0.0, 0.1, 0.2],终止位置为[0.8,0.9, 1.0] 。设置动画执行周期3s,执行次数10次。最后一行将动画添加到 Layer 图层上,启动当前动画。如图所示为最终实现的光波扫描动画效果
5. CAShapeLayer
CAShapeLayer 是 QuartzCore 框架下非常重要的一个类,利用它可以实现各种图形绘制类动画效果。CAShapeLayer 来自 iOS 框架下的核心动画部分,Shape 为形状的意思,描述了当前动画的特点,可以实现各类形状的绘制。Layer 表明当前动画并非直接作用于 UIView 显示层上,而是作用在 Layer 内容层上。
iOS 中有一个 UIBezierPath(贝塞尔曲线) 类,使用 UIBezierPath 类可以创建基于矢量的路径. CAShapeLayer 需要和贝塞尔曲线配合使用才有意义。贝塞尔曲线可以为其提供形状,而单独使用CAShapeLayer 是没有任何意义的。
柱状图的代码结构与折线图类似,整体也分为两部分:一部分动态图表初始化,另一部分绘制代码部分。
import UIKit
let PNGreen:UIColor = UIColor (red: 77.0/255.0, green: 186.0/255.0, blue: 122.0/255.0, alpha: 1.0)
class BarChartView: UIView {
var chartLine: CAShapeLayer = CAShapeLayer()
var pathAnimation: CABasicAnimation = CABasicAnimation()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.white
self.clipsToBounds = true
chartLine.lineCap = .square
chartLine.lineJoin = .round
chartLine.fillColor = UIColor.gray.cgColor
chartLine.lineWidth = 30.0
chartLine.strokeEnd = 0.0
self.layer.addSublayer(chartLine)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
动态柱状图初始化代码部分与折线图非常类似,这里就不再重复,它们的核心思想都是通过 CAShapeLayer 图层和 CABasicAnimation 基础动画来实现的。
下面是柱状图绘制代码。代码第1行定义一一个方法,该方法设置柱状图绘制的 strokeEnd 属性为1,并将动画添加到 chartLine 上。代码第5行定义一条贝济埃曲线。第6行到第8行设置曲线的宽度,以及拐角和曲线之间的连接属性。第9行到第13行在视图中合适的位置绘制五根柱状图。第14行将绘制的五根柱状图路径添加到 CAShapeLayer 图层上。第15行设置柱状图颜色。第16行设置动画属性为 strokeEnd .第17 行到第19行设置动画起始终止阶段的动画速度效果以及动画的 value 值变化情况。第20行设置动画执行完毕之后不按照原路返回。最后一行设置动画周期为1s。
func drawLineChart() {
chartLine.strokeEnd = 1.0
chartLine.add(pathAnimation, forKey: nil)
}
override func draw(_ rect: CGRect) {
let line: UIBezierPath = UIBezierPath()
line.lineWidth = 30.0
line.lineCapStyle = .square
line.lineJoinStyle = .round
for i in 0...4 {
let x: CGFloat = CGFloat(60+70*i)
let y:CGFloat = CGFloat(100+20*i)
line.move(to: CGPoint (x:x,y:215))
line.addLine(to: CGPoint (x:x,y:y))
}
chartLine.path = line.cgPath
chartLine.strokeColor = PNGreen.cgColor
pathAnimation.keyPath = "strokeEnd"
pathAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
pathAnimation.fromValue = 0.0
pathAnimation.toValue = 1.0
pathAnimation.autoreverses = false
pathAnimation.duration = 1.0
}
动画调用非常简单,该代码主要分为两个部分: 第一部初始化并绘制相应坐标系,另一部分开始调用图标绘制方法。初始化代码如下。
class ViewController: UIViewController {
var barChartView1: BarChartView?
override func viewDidLoad() {
super.viewDidLoad()
// CAShapeLayer
barChartView1 = BarChartView (frame: CGRect(x: 0, y: 150, width: self.view.bounds.width, height: self.view.bounds.height))
self.view.addSubview(barChartView1!)
self.addDrawChartButton()
self.addAxes()
}
// 绘制坐标系
func addAxes() {
// BarChart
for i in 1...5 {
let xAxesTitle: String = "SEP"+"\(i)"
let xAxesLabel: UILabel = UILabel(frame: CGRect.init(x: 50 + (CGFloat(i)-1) * 70 - 8, y: 400, width: 50, height: 20))
xAxesLabel.text = xAxesTitle
self.view.addSubview(xAxesLabel)
}
}
// 动画调用代码
func addDrawChartButton() {
let bt_line: UIButton = UIButton()
bt_line.frame = CGRect.init(x: (self.view.frame.size.width - 100)/2, y: 100, width: 100, height: 50)
bt_line.setTitle("Bar Chart", for: UIControl.State.normal)
bt_line.setTitleColor(PNGreen, for: UIControl.State.normal)
bt_line.addTarget(self, action: #selector(drawChart), for: .touchUpInside)
self.view.addSubview (bt_line)
}
@objc func drawChart(){
barChartView1!.drawLineChart()
}
}
如图所示为最终效果图。
6. CAReplicatorLayer:图层复制效果
CAReplicatorLayer 主要由三个部分组成: CA、Replicator、 Layer。 CA 为 CoreAnimation 的缩写,表明我们当前的动画使用的是 iOS 框架下的核心动画框架。Replicator 表示该图层可以用于图层的快速复制。Layer 表明当前动画并非直接作用于 UIView 显示层上,而是作用在 Layer 内容层上。本章就是使用 CAReplicatorLayer 图层的快速复制效果实现恒星公转效果。
恒星旋转动画从整体结构.上分为三个部分。
- UllmageView:背景图片、恒星、地球。
- CAKeyframeAnimation: 关键帧动画及相关属性设置。
- CAReplicatorLayer: Replicator 图层关键属性设置。
第一部分实现代码如下所示。
let UISCREEN_WIDTH = UIScreen.main.bounds.size.width
let UISCREEN_HEIGHT = UIScreen.main.bounds.size.height
var replicatorLayer: CAReplicatorLayer = CAReplicatorLayer()
var iv_earth: UIImageView?
override func viewDidLoad() {
super.viewDidLoad()
let background: UIImageView = UIImageView.init(frame: CGRect.init(x: 0, y: 0, width: UISCREEN_WIDTH, height: UISCREEN_HEIGHT))
background.image = UIImage(named: "background.jpg")
self.view.addSubview(background)
iv_earth = UIImageView.init(frame: CGRect.init(x: (UISCREEN_WIDTH - 50)/2 + 150, y: (UISCREEN_HEIGHT - 50)/2, width: 50, height: 50))
iv_earth?.image = UIImage.init(named: "earth.png")
let iv_sun = UIImageView.init(frame: CGRect.init(x: 0, y: 0, width: 50, height: 50))
iv_sun.center = self.view.center
iv_sun.image = UIImage.init(named: "sun.png")
replicatorLayer.addSublayer(iv_earth!.layer)
replicatorLayer.addSublayer (iv_sun.layer)
}
代码第1行和第2行定义当前屏幕宽高信息。第3行和第4行实例化 CAReplicatorLayer 图层,并定义地球 UIImageView。第7行定义 UIImageView 背景图片,第8行为 UIImageView 背景添加 background.jpg 图片。第9行将背景图片添加到 self.view上。第10行实例化地球 UIImageView 并将其定位在屏幕的指定位置。第11行添加 earth.png 图片。第12行到第14行实例化恒星 UIImageView 实例对象,添加 sun.png 背景图片并添加到屏幕的中心位置。最后两行分别将地球 UIImageView 和恒星 UIImageView 添加到 Layer 图层上。
动画实现部分代码在 viewWillAppear 方法中实现,因此可以达到应用启动之后动画即开始展现的效果。下面是动画实现核心代码。
override func viewWillAppear(_ animated: Bool) {
// 图层复制
let path: UIBezierPath = UIBezierPath()
path.addArc(withCenter: CGPoint.init(x: self.view.center.x, y: self.view.center.y), radius: 150, startAngle: 0, endAngle: CGFloat(M_PI*2), clockwise: true)
path.close ()
let animation: CAKeyframeAnimation = CAKeyframeAnimation(keyPath:"position")
animation.path = path.cgPath
animation.duration = 10
animation.repeatCount = MAXFLOAT
replicatorLayer.instanceCount = 100
replicatorLayer.instanceDelay = 0.2
self.view.layer.addSublayer(replicatorLayer)
iv_earth?.layer.add(animation, forKey: nil)
}
代码第1行实例化一个贝塞尔曲线的路径。第2行绘制一个圆,该圆的路径就是最终恒星公转时的路径。第3行关闭path,保证贝塞尔曲线是一个闭合的曲线。第4行实例化关键帧动画实例对象。第5行到第7行设置动画的路径属性、执行周期、重复次数。第8行表明有100个重复的 Layer 图层,即100个地球 UIImageView 的图层内容。第9行动画每0.2s 复制一个Layer图层。第.10行将 replicatorLayer 图层添加到 self.view.layer 图层上。最后一行在地球 Layer 图层上启动该动画。最终动画效果如图所示。
三、 3D 动画
在第一部分和第二部分中已经为大家详细介绍了 iOS 中常见的2D动画效果,本章将为大家介绍一些常用的3D动画效果。通过修改x、y轴可以实现动画2D效果,如移动、缩放等。3D则是在原本的基础上修改z轴,实现投影、拉伸等3D效果。iOS 中提供了一个3D变幻矩阵(CATransform3D),通过这个矩阵可以实现各种透视、拉伸、移动缩放效果。
1. 锚点:anchorPoint
默认来说, anchorPoint 位于图层的中点,所以图层的将会以这个点为中心放置。 anchorPoint 属性并没有被 UIView 接口暴露出来,这也是视图的position属性被叫做“center”的原因。但是图层的 anchorPoint 可以被移动,比如你可以把它置于图层 frame 的左上角,于是图层的内容将会向右下角的 position 方向移动(如图),而不是居中了。
2. 矩阵变换的基本原理
iOS 中利用 CATransform3D 实现3D变换效果。CATransform3D 其实质是定义了一个三维变换(4x4 CGFloat值的矩阵),利用该矩阵可以实现图层的旋转、缩放、偏移、歪斜和视图透视等效果。
先来看看 CATransform3D 所描述的矩阵都有哪些功能。
struct CATransform3D {
CGFloat m11(x缩放 , m12(y切变), m13() , m14();
CGFloat m21(x切变) , m22(y缩放), m23() , m24();
CGFloat m31() , m32() , m33() , m34(透视);
CGFloat m41(x平移), m42(y平移) , m43(z平移), m44();
}
从动画中可以看出,该动画效果围绕图片x轴方向中线,实现在z轴方向旋转。下面是具体实现代码。
var imageView: UIImageView?
override func viewDidLoad() {
imageView = UIImageView()
imageView?.frame = CGRect(x: 0, y: 0, width: 400, height: 300)
imageView?.center = self.view.center
imageView?.image = UIImage (named: "iPhone.jpg")
imageView?.contentMode = UIView.ContentMode.scaleAspectFit
imageView!.layer.anchorPoint = CGPoint.init(x: 0.5, y: 0.5)
self.view.addSubview (imageView!)
UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDuration(4)
imageView!.layer.transform = CATransform3DMakeRotation(CGFloat (M_PI/2.0), 1, 0, 0) ;
UIView.commitAnimations()
}
将动画部分代码修改为以下形式:
UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDuration(4)
var transform: CATransform3D = CATransform3DIdentity
transform.m22 = 0.5
imageView!.layer.transform = CATransform3DScale(transform, 1, 1, 1)
UIView.commitAnimations()
代码第3行创建一个单位矩阵,该矩阵没有任何缩放、旋转等效果,为图层最原始的形态。第4行手动修改矩阵的(2, 2)位置处的值为0.5。通过动图矩阵旋转效果可以看出,该值负责图层的y轴方向缩放,将m22修改为0.5之后,表明当前动画为y轴方向图片缩放。如动图所示为最终实现效果。
四、CoreAnimation: CATransition 转场动画
转场动画主要用于不同视图场景之间的切换。比如我们经常使用的PPT,每一页PPT都可以作为一个独立的场景,在这个单独的场景中可以添加各种各样的UI。但是当这一页展示完毕,需要进入到下一页时,添加一个合适的过度动画会使得转场效果比较平滑。在 iOS 中经常使用 CoreAnimation 核心动画中的 CATransition 实现这个功能。
CATransition 同 CoreAnimation 核心动画中 CABasicAnimation 等相关类的使用方法类似。主要分为以下三个步骤。
- 实例化
CATransition,并设置相应的转场动画key。 - 设置合适的转场动画属性,比如动画周期、过度方向、动画保持状态等。
- 将动画效果添加到相应视图的
Layer图层中。 在第一步设置动画效果时需要注意,iOS提供了大量的炫酷动画效果。不过总体上来说可以分为公有API和私有API,公有API制作的APP可以直接上线,私有API制作的APP有被拒的风险,所以在使用的时候需要尤为注意。 公有API: - fade, 淡入淡出效果,可以使用常量kCATransitionFade表示。
- push, 推送效果,可以使用常量kCATransitionMoveIn表示。
- reveal, 揭开效果,可以使用常量kCATransitionReveal表示。
- movein, 移动效果,可以使用常量kCATransitionMoveln表示。
私有API:
- pageCurl,向上翻页效果,只能用字符串表示。
- pageUnCurl,向下翻页效果,只能用字符串表示。
- cube,立方体翻转效果,只能用字符串表示。
- ogIFlip, 翻转效果,只能用字符串表示。
- stuckEffect,收缩效果,只能用字符串表示。
- rippleEffect, 水滴波纹效果,只能用字符串表示。
- cameralrisHollowOpen, 相机打开效果,只能用字符串表示。
- cameralrisHollowClose, 相机关闭效果,只能用字符串表示。
在第二个步骤中,设置动画的周期、最终状态等属性和 CoreAnimation 核心动画中的 CABasicAnimation 类使用方法相同,这里主要介绍转场动画的方向属性设置。转场动画支持以下四种方向。
- kCATransitionFromRight:从右侧转场。
- kCATransitionFromLeft: 从左侧转场。
- kCATransitionFromTop:从顶部转场。
- kCATransitionFromBottom: 从底部转场。
第三步实现将动画添加到指定的图层上。如果想让整个视图控制器进行转场,那么可以添加到当前的 self.view 上。如果想对某个特定的图层进行转场,那么可以直接添加到相应图层上。
@IBAction func animationBegin(_ sender: Any) {
imageView?.image = UIImage.init(named: "iPhoneX")
let animation: CATransition = CATransition()
animation.duration = 2;
animation.type = .fade
self.view.layer.add(animation, forKey: nil)
}
代码第1行加载一张新的图片到 UIImageView 上。第2行实例化 CATransition 实例对象。第3行设置当前动画周期为2s。第4行设置动画类型为翻转效果。最后将动画添加到 self.view 的 Layer 图层.上。如动图所示为最终实现效果。
参考:
《iOS 动画核心技术与案例实战》
《核心动画高级技巧》