猫咪在上一篇文章中简单的介绍了UIView动画的入门和实现,那这篇文章,就来和大家一起来看一看更复杂的动画效果是怎么实现的呢?先来看一张图:
正如我们看到的那样,越往上封装度越高,动画的实现越简洁,但是自由度低。
核心动画有以下几类:
- 图层类(CALayer)
- 动画和计时类 (CAAnimation 和 CAMediaTiming)
- 布局和约束类(CAConstraint)
- 事务类 (CATransaction)
一、各个类的概念介绍
1.图层类 - CALayer
图层类是核心动画的基础,它是所有核心动画图层类的父类。和UIView一样,同样有自己的视图集合,同样有layer、subLayer...也同样拥有backgroundColor、frame等相似的属性,我们可以将UIView看做是一个特殊的CALayer。
它们之间的区别就是UIView可以响应事件,layer最常用的就是设置圆角、阴影、边框等参数和实现动画。因此对view做动画实际上是对layer进行操作。
2.动画类 - CAAnimation
核心动画的动画类使用基本的动画和关键帧动画把图层的内容和选取的属性动 画的显示出来。所有核心动画的动画类都是从 CAAnimation 类继承而来。CAAnimation 实现了 CAMediaTiming 协议,提供了动画的持续时间,速度,和重复计数。 CAAnimation 也实现了 CAAction 协议。该协议为图层触发一个动画动作提供了提供 标准化响应。
推荐文章:iOS开发CoreAnimation解读之一——初识CoreAnimation核心动画编程
3.布局和约束类 - CAConstraint
核心动画的 CAConstraint 类 是一个布局管理器,它可以指定子图层类限制于你指定的约束集合。每个约束 (CAConstraint 类的实例封装)描述层的几何属性(左,右,顶部或底部的边缘或水 平或垂直中心)的关系,关系到其同级之一的几何属性层或 superlayer。
4.事务类 - CATransaction
CATransition 提供了一个图层变化的过渡效果,它能影响图层的整个内容。 动画进行的时候淡入淡出(fade)、推(push)、显露(reveal)图层的内容。这些过渡效 果可以扩展到你自己定制的 Core Image 滤镜。
二、核心动画渲染框架
虽然核心动画的图层和 Cocoa 的视图在很大程度上没有一定的相似性,但是他们 两者最大的区别是,图层不会直接渲染到屏幕上。
在我们常用的模型-视图-控制器(model-view-controller)概念里面 NSView 和 UIView 是典 型的视图部分,但是在核心动画里面图层是模型部分。图层封装了几何、时间、可视 化属性,同时它提供了图层现实的内容,但是实际显示的过程则不是由它来完成。
三、CoreGraphics框架
介绍CoreGraphics的资料很多,推荐给大家一篇我觉得比较好的: CoreGraphics入门
这是猫咪实现的两个小动画,上面hamburger的动画原址:用 Swift 制作一个漂亮的汉堡按钮过渡动画,给大家简单介绍一下下面播放按钮的实现原理:
import CoreGraphics
import QuartzCore
import UIKit
class PlayButton: UIControl {
// MARK: - enum
// 按钮的几种动画效果
enum PlayAnimation {
case rotateAndGrad
case grad
case breathe
}
// 播放状态
enum PlayStatus {
case buffering // 缓冲
case play // 播放
case pause // 暂停
case stopped // 停止
}
// MARK: - private property
// 左右两条线
private var top: CAShapeLayer! = CAShapeLayer()
private var bottom: CAShapeLayer! = CAShapeLayer()
private var rotate: CAShapeLayer! = CAShapeLayer()
// 菜单的起点和终点
private let menuStrokeStart: CGFloat = 0.325
private let menuStrokeEnd: CGFloat = 0.9
// 中线的起点和重点
private let playStrokeStart: CGFloat = 0.028
private let playStrokeEnd: CGFloat = 0.111
// MARK: - life cycle
override init(frame: CGRect) {
super.init(frame: frame)
// 绘制左右两条线
self.top.path = leftStroke
self.bottom.path = rightStroke
self.rotate.path = outline
// 设置layer的相关属性
for layer in [self.top, self.bottom, self.rotate] {
// 填充颜色
layer?.fillColor = nil
// 线条的颜色
layer?.strokeColor = UIColor.white.cgColor
// 线条宽度
layer?.lineWidth = 4
// 两条线段相交时锐角斜面长度
layer?.miterLimit = 4
// 线条首尾的外观
layer?.lineCap = kCALineCapRound
// 设置layer的bounds
let strokingPath = CGPath(__byStroking: (layer?.path!)!, transform: nil, lineWidth: 4, lineCap: .round, lineJoin: .miter, miterLimit: 4)
layer?.bounds = (strokingPath?.boundingBoxOfPath)!
// 设置行为
layer?.actions = [
"strokeStart": NSNull(),
"strokeEnd": NSNull(),
"transform": NSNull()
]
self.layer.addSublayer(layer!)
}
// 设置左线的锚点和位置
self.top.anchorPoint = CGPoint(x: 1, y: 1)
self.top.position = CGPoint(x: 40, y: 19.25)
// 设置右线的锚点和位置
self.bottom.anchorPoint = CGPoint(x: 1, y: 0)
self.bottom.position = CGPoint(x: 40, y: 18)
// 设置中线的位置和起点和终点
self.rotate.position = CGPoint(x: 29, y: 18)
self.rotate.strokeStart = playStrokeStart
self.rotate.strokeEnd = playStrokeEnd
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - setter and getter
// 画短直线 -> play
let leftStroke: CGPath = {
let path = CGMutablePath()
path.move(to: CGPoint(x: 2, y: 2))
path.addLine(to: CGPoint(x: 2, y: 15))
return path
}()
private let rightStroke: CGPath = {
let path = CGMutablePath()
path.move(to: CGPoint(x: 2, y: 12))
path.addLine(to: CGPoint(x: 2, y: 25))
return path
}()
// 外边框圆 -> 中间
private let outline: CGPath = {
let path = CGMutablePath()
path.move(to: CGPoint(x: 29, y: 0))
// 添加曲线
path.addCurve(to: CGPoint(x: 27, y: 53), control1: CGPoint(x: 27, y: 31), control2: CGPoint(x: 27, y: 43.75))
path.addCurve(to: CGPoint(x: 41.5, y: 3), control1: CGPoint(x: 75.92, y: 24.75), control2: CGPoint(x: 62.97, y: 3))
path.addCurve(to: CGPoint(x: 16.5, y: 27), control1: CGPoint(x: 28.66, y: 3), control2: CGPoint(x: 16.5, y: 13.16))
path.addCurve(to: CGPoint(x: 41.5, y: 52), control1: CGPoint(x: 16.5, y: 40.84), control2: CGPoint(x: 27.66, y: 52))
path.addCurve(to: CGPoint(x: 66.5, y: 27), control1: CGPoint(x: 55.34, y: 52), control2: CGPoint(x: 66.5, y: 40.84))
path.addCurve(to: CGPoint(x: 41.5, y: 3), control1: CGPoint(x: 66.5, y: 13.16), control2: CGPoint(x: 56.89, y: 3))
path.addCurve(to: CGPoint(x: 16.5, y: 27), control1: CGPoint(x: 27.66, y: 3), control2: CGPoint(x: 16.5, y: 13.16))
return path
}()
var playStatus: PlayStatus = .pause {
didSet {
let strokeStart = CABasicAnimation(keyPath: "strokeStart")
let strokeEnd = CABasicAnimation(keyPath: "strokeEnd")
// 动画
if self.playStatus == .play {
strokeStart.toValue = menuStrokeStart
strokeStart.duration = 0.2
strokeStart.timingFunction = CAMediaTimingFunction(controlPoints: 0.25, -0.4, 0.5, 1)
strokeEnd.toValue = menuStrokeEnd
strokeEnd.duration = 0.3
strokeEnd.timingFunction = CAMediaTimingFunction(controlPoints: 0.25, -0.4, 0.5, 1)
} else {
strokeStart.toValue = playStrokeStart
strokeStart.duration = 0.2
strokeStart.timingFunction = CAMediaTimingFunction(controlPoints: 0.25, 0, 0.5, 1.2)
strokeStart.beginTime = CACurrentMediaTime() + 0.1
strokeStart.fillMode = kCAFillModeBackwards
strokeEnd.toValue = playStrokeEnd
strokeEnd.duration = 0.3
strokeEnd.timingFunction = CAMediaTimingFunction(controlPoints: 0.25, 0.3, 0.5, 0.9)
}
self.rotate.ocb_applyAnimation(strokeStart)
self.rotate.ocb_applyAnimation(strokeEnd)
// 设置竖线的变化
let topTransform = CABasicAnimation(keyPath: "transform")
topTransform.timingFunction = CAMediaTimingFunction(controlPoints: 0.5, -0.8, 0.5, 1.85)
topTransform.duration = 0.2
topTransform.fillMode = kCAFillModeBackwards
let bottomTransform = topTransform.copy() as! CABasicAnimation
if self.playStatus == .play {
let translation = CATransform3DMakeTranslation(-2, 0, 0)
topTransform.toValue = NSValue(caTransform3D: CATransform3DRotate(translation, -0.65, 0, 0, 1))
topTransform.beginTime = CACurrentMediaTime() + 0.25
bottomTransform.toValue = NSValue(caTransform3D: CATransform3DRotate(translation, 0.84, 0, 0, 1))
bottomTransform.beginTime = CACurrentMediaTime() + 0.25
} else {
topTransform.toValue = NSValue(caTransform3D: CATransform3DIdentity)
topTransform.beginTime = CACurrentMediaTime() + 0.05
bottomTransform.toValue = NSValue(caTransform3D: CATransform3DIdentity)
bottomTransform.beginTime = CACurrentMediaTime() + 0.05
}
self.top.ocb_applyAnimation(topTransform)
self.bottom.ocb_applyAnimation(bottomTransform)
}
}
}
对CALayer进行扩展
extension CALayer {
func ocb_applyAnimation(_ animation: CABasicAnimation) {
let copy = animation.copy() as! CABasicAnimation
if copy.fromValue == nil {
copy.fromValue = self.presentation()!.value(forKeyPath: copy.keyPath!)
}
self.add(copy, forKey: copy.keyPath)
self.setValue(copy.toValue, forKeyPath:copy.keyPath!)
}
}
在控制器中,我们创建一个播放按钮:
class ViewController: UIViewController {
var button: PlayButton! = nil
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor(red: 38.0 / 255, green: 151.0 / 255, blue: 68.0 / 255, alpha: 1)
button = PlayButton(frame: CGRect(x: 133, y: 133, width: 100, height: 100))
// button.transform = CGAffineTransform.init(scaleX: 0.5, y: 0.5) 作用于view
button.layer.transform = CATransform3DMakeScale(0.8, 0.8, 1)
button.addTarget(self, action: #selector(ViewController.toggle(_:)), for:.touchUpInside)
view.addSubview(button)
}
override var preferredStatusBarStyle : UIStatusBarStyle {
return .lightContent
}
func toggle(_ sender: AnyObject!) {
if button.playStatus == .play {
button.playStatus = .pause
} else if button.playStatus == .pause {
button.playStatus = .play
}
}
}