貓咪在上一篇文章中簡(jiǎn)單的介紹了UIView動(dòng)畫(huà)的入門(mén)和實(shí)現(xiàn)浅妆,那這篇文章,就來(lái)和大家一起來(lái)看一看更復(fù)雜的動(dòng)畫(huà)效果是怎么實(shí)現(xiàn)的呢障癌?先來(lái)看一張圖:
正如我們看到的那樣凌外,越往上封裝度越高,動(dòng)畫(huà)的實(shí)現(xiàn)越簡(jiǎn)潔混弥,但是自由度低趴乡。
核心動(dòng)畫(huà)有以下幾類(lèi):
- 圖層類(lèi)(CALayer)
- 動(dòng)畫(huà)和計(jì)時(shí)類(lèi) (CAAnimation 和 CAMediaTiming)
- 布局和約束類(lèi)(CAConstraint)
- 事務(wù)類(lèi) (CATransaction)
一、各個(gè)類(lèi)的概念介紹
1.圖層類(lèi) - CALayer
圖層類(lèi)是核心動(dòng)畫(huà)的基礎(chǔ)蝗拿,它是所有核心動(dòng)畫(huà)圖層類(lèi)的父類(lèi)晾捏。和UIView一樣,同樣有自己的視圖集合哀托,同樣有l(wèi)ayer惦辛、subLayer...也同樣擁有backgroundColor、frame等相似的屬性仓手,我們可以將UIView看做是一個(gè)特殊的CALayer胖齐。
它們之間的區(qū)別就是UIView可以響應(yīng)事件,layer最常用的就是設(shè)置圓角嗽冒、陰影呀伙、邊框等參數(shù)和實(shí)現(xiàn)動(dòng)畫(huà)。因此對(duì)view做動(dòng)畫(huà)實(shí)際上是對(duì)layer進(jìn)行操作添坊。
2.動(dòng)畫(huà)類(lèi) - CAAnimation
核心動(dòng)畫(huà)的動(dòng)畫(huà)類(lèi)使用基本的動(dòng)畫(huà)和關(guān)鍵幀動(dòng)畫(huà)把圖層的內(nèi)容和選取的屬性動(dòng) 畫(huà)的顯示出來(lái)剿另。所有核心動(dòng)畫(huà)的動(dòng)畫(huà)類(lèi)都是從 CAAnimation 類(lèi)繼承而來(lái)。CAAnimation 實(shí)現(xiàn)了 CAMediaTiming 協(xié)議贬蛙,提供了動(dòng)畫(huà)的持續(xù)時(shí)間雨女,速度,和重復(fù)計(jì)數(shù)阳准。 CAAnimation 也實(shí)現(xiàn)了 CAAction 協(xié)議氛堕。該協(xié)議為圖層觸發(fā)一個(gè)動(dòng)畫(huà)動(dòng)作提供了提供 標(biāo)準(zhǔn)化響應(yīng)。
推薦文章:iOS開(kāi)發(fā)CoreAnimation解讀之一——初識(shí)CoreAnimation核心動(dòng)畫(huà)編程
3.布局和約束類(lèi) - CAConstraint
核心動(dòng)畫(huà)的 CAConstraint 類(lèi) 是一個(gè)布局管理器野蝇,它可以指定子圖層類(lèi)限制于你指定的約束集合讼稚。每個(gè)約束 (CAConstraint 類(lèi)的實(shí)例封裝)描述層的幾何屬性(左,右绕沈,頂部或底部的邊緣或水 平或垂直中心)的關(guān)系乱灵,關(guān)系到其同級(jí)之一的幾何屬性層或 superlayer。
4.事務(wù)類(lèi) - CATransaction
CATransition 提供了一個(gè)圖層變化的過(guò)渡效果七冲,它能影響圖層的整個(gè)內(nèi)容。 動(dòng)畫(huà)進(jìn)行的時(shí)候淡入淡出(fade)规婆、推(push)澜躺、顯露(reveal)圖層的內(nèi)容蝉稳。這些過(guò)渡效 果可以擴(kuò)展到你自己定制的 Core Image 濾鏡。
二掘鄙、核心動(dòng)畫(huà)渲染框架
雖然核心動(dòng)畫(huà)的圖層和 Cocoa 的視圖在很大程度上沒(méi)有一定的相似性耘戚,但是他們 兩者最大的區(qū)別是,圖層不會(huì)直接渲染到屏幕上操漠。
在我們常用的模型-視圖-控制器(model-view-controller)概念里面 NSView 和 UIView 是典 型的視圖部分收津,但是在核心動(dòng)畫(huà)里面圖層是模型部分。圖層封裝了幾何浊伙、時(shí)間撞秋、可視 化屬性,同時(shí)它提供了圖層現(xiàn)實(shí)的內(nèi)容嚣鄙,但是實(shí)際顯示的過(guò)程則不是由它來(lái)完成吻贿。
三、CoreGraphics框架
介紹CoreGraphics的資料很多哑子,推薦給大家一篇我覺(jué)得比較好的: CoreGraphics入門(mén)
這是貓咪實(shí)現(xiàn)的兩個(gè)小動(dòng)畫(huà)舅列,上面hamburger的動(dòng)畫(huà)原址:用 Swift 制作一個(gè)漂亮的漢堡按鈕過(guò)渡動(dòng)畫(huà),給大家簡(jiǎn)單介紹一下下面播放按鈕的實(shí)現(xiàn)原理:
import CoreGraphics
import QuartzCore
import UIKit
class PlayButton: UIControl {
// MARK: - enum
// 按鈕的幾種動(dòng)畫(huà)效果
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()
// 菜單的起點(diǎn)和終點(diǎn)
private let menuStrokeStart: CGFloat = 0.325
private let menuStrokeEnd: CGFloat = 0.9
// 中線的起點(diǎn)和重點(diǎn)
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
// 兩條線段相交時(shí)銳角斜面長(zhǎng)度
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è)置左線的錨點(diǎn)和位置
self.top.anchorPoint = CGPoint(x: 1, y: 1)
self.top.position = CGPoint(x: 40, y: 19.25)
// 設(shè)置右線的錨點(diǎn)和位置
self.bottom.anchorPoint = CGPoint(x: 1, y: 0)
self.bottom.position = CGPoint(x: 40, y: 18)
// 設(shè)置中線的位置和起點(diǎn)和終點(diǎn)
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
// 畫(huà)短直線 -> 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")
// 動(dòng)畫(huà)
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)
}
}
}
對(duì)CALayer進(jìn)行擴(kuò)展
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)建一個(gè)播放按鈕:
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
}
}
}