貓咪在上一篇文章中簡單的介紹了UIView動畫的入門和實現(xiàn),那這篇文章,就來和大家一起來看一看更復(fù)雜的動畫效果是怎么實現(xiàn)的呢?先來看一張圖:

正如我們看到的那樣,越往上封裝度越高,動畫的實現(xiàn)越簡潔,但是自由度低。
核心動畫有以下幾類:
- 圖層類(CALayer)
- 動畫和計時類 (CAAnimation 和 CAMediaTiming)
- 布局和約束類(CAConstraint)
- 事務(wù)類 (CATransaction)
一、各個類的概念介紹
1.圖層類 - CALayer
圖層類是核心動畫的基礎(chǔ),它是所有核心動畫圖層類的父類。和UIView一樣,同樣有自己的視圖集合,同樣有l(wèi)ayer、subLayer...也同樣擁有backgroundColor、frame等相似的屬性,我們可以將UIView看做是一個特殊的CALayer。
它們之間的區(qū)別就是UIView可以響應(yīng)事件,layer最常用的就是設(shè)置圓角、陰影、邊框等參數(shù)和實現(xiàn)動畫。因此對view做動畫實際上是對layer進行操作。
2.動畫類 - CAAnimation
核心動畫的動畫類使用基本的動畫和關(guān)鍵幀動畫把圖層的內(nèi)容和選取的屬性動 畫的顯示出來。所有核心動畫的動畫類都是從 CAAnimation 類繼承而來。CAAnimation 實現(xiàn)了 CAMediaTiming 協(xié)議,提供了動畫的持續(xù)時間,速度,和重復(fù)計數(shù)。 CAAnimation 也實現(xiàn)了 CAAction 協(xié)議。該協(xié)議為圖層觸發(fā)一個動畫動作提供了提供 標準化響應(yīng)。
推薦文章:iOS開發(fā)CoreAnimation解讀之一——初識CoreAnimation核心動畫編程
3.布局和約束類 - CAConstraint
核心動畫的 CAConstraint 類 是一個布局管理器,它可以指定子圖層類限制于你指定的約束集合。每個約束 (CAConstraint 類的實例封裝)描述層的幾何屬性(左,右,頂部或底部的邊緣或水 平或垂直中心)的關(guān)系,關(guān)系到其同級之一的幾何屬性層或 superlayer。
4.事務(wù)類 - CATransaction
CATransition 提供了一個圖層變化的過渡效果,它能影響圖層的整個內(nèi)容。 動畫進行的時候淡入淡出(fade)、推(push)、顯露(reveal)圖層的內(nèi)容。這些過渡效 果可以擴展到你自己定制的 Core Image 濾鏡。
二、核心動畫渲染框架
雖然核心動畫的圖層和 Cocoa 的視圖在很大程度上沒有一定的相似性,但是他們 兩者最大的區(qū)別是,圖層不會直接渲染到屏幕上。
在我們常用的模型-視圖-控制器(model-view-controller)概念里面 NSView 和 UIView 是典 型的視圖部分,但是在核心動畫里面圖層是模型部分。圖層封裝了幾何、時間、可視 化屬性,同時它提供了圖層現(xiàn)實的內(nèi)容,但是實際顯示的過程則不是由它來完成。
三、CoreGraphics框架
介紹CoreGraphics的資料很多,推薦給大家一篇我覺得比較好的: CoreGraphics入門


這是貓咪實現(xiàn)的兩個小動畫,上面hamburger的動畫原址:用 Swift 制作一個漂亮的漢堡按鈕過渡動畫,給大家簡單介紹一下下面播放按鈕的實現(xiàn)原理:
import CoreGraphics
import QuartzCore
import UIKit
class PlayButton: UIControl {
// MARK: - enum
// 按鈕的幾種動畫效果
enum PlayAnimation {
case rotateAndGrad
case grad
case breathe
}
// 播放狀態(tài)
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
// 設(shè)置layer的相關(guān)屬性
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
// 設(shè)置layer的bounds
let strokingPath = CGPath(__byStroking: (layer?.path!)!, transform: nil, lineWidth: 4, lineCap: .round, lineJoin: .miter, miterLimit: 4)
layer?.bounds = (strokingPath?.boundingBoxOfPath)!
// 設(shè)置行為
layer?.actions = [
"strokeStart": NSNull(),
"strokeEnd": NSNull(),
"transform": NSNull()
]
self.layer.addSublayer(layer!)
}
// 設(shè)置左線的錨點和位置
self.top.anchorPoint = CGPoint(x: 1, y: 1)
self.top.position = CGPoint(x: 40, y: 19.25)
// 設(shè)置右線的錨點和位置
self.bottom.anchorPoint = CGPoint(x: 1, y: 0)
self.bottom.position = CGPoint(x: 40, y: 18)
// 設(shè)置中線的位置和起點和終點
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)
// 設(shè)置豎線的變化
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!)
}
}
在控制器中,我們創(chuàng)建一個播放按鈕:
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
}
}
}