本章開始將正式進入動畫的部分三痰,首先要介紹的是隱式動畫。所謂隱式動畫就是由系統(tǒng)自動完成的動畫窜管。
事務(wù)
Core Animation
基于一個假設(shè),說屏幕上的任何東西都可以(或者可能)做動畫散劫。 動畫并不需要你在Core Animation
中手動打開,相反需要明確地關(guān)閉,否則他會一 直存在。例如下面的例子幕帆,在改變CALayer
背景色的時候它會自己從舊值平滑的過渡到新值蜓肆。
class TransactionViewController: UIViewController {
@IBOutlet weak var layerView: UIView!
weak var colorLayer: CALayer!
override func viewDidLoad() {
super.viewDidLoad()
let layer = CALayer()
self.layerView.layer.addSublayer(layer)
layer.frame = CGRect(x: 35, y: 20, width: 180, height: 180)
layer.backgroundColor = UIColor.blue.cgColor;
colorLayer = layer
}
@IBAction func changeBtnClick(_ sender: UIButton) {
let red = CGFloat(arc4random() % 256) / 255.0
let green = CGFloat(arc4random() % 256) / 255.0
let blue = CGFloat(arc4random() % 256) / 255.0
colorLayer.backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0).cgColor
}
}
這個就是一個隱式動畫症概,我們沒有指定動畫類型诅蝶,只是改變了一個屬性调炬,然后由系統(tǒng)來自動完成動畫效果缰泡。
那么這些又跟事務(wù)有什么關(guān)系呢棘钞?iOS中的事務(wù)可以理解為一系列動畫的集合宜猜,任何用指定事 務(wù)去改變可以做動畫的圖層屬性都不會立刻發(fā)生變化,而是當(dāng)事務(wù)一旦提交的時候 開始用一個動畫過渡到新值姨拥。
事務(wù)是通過 CATransaction
類來做管理叫乌。CATransaction
是在圖層樹改變的時候在一個沒有活躍事務(wù)的線程中由CoreAnimation
自動創(chuàng)建的综芥,并且在run-loop重復(fù)的時候自動提交膀藐。CATransaction
沒有屬性或者實例方法,不需要使用者來單獨創(chuàng)建额各。但是可以用 begin()
和 commit()
分別來入椣豪玻或者出棧蝇闭。
任何可以做動畫的圖層屬性都會被添加到棧頂?shù)氖聞?wù),你可以通
過 setAnimationDuration(_:)
來設(shè)置或者通過 animationDuration()
來獲取 當(dāng)前動畫的時間(默認(rèn)值為0.25)
接下來我們來讓上面例子中顏色的改變慢一點呻引。
@IBAction func changeBtnClick(_ sender: UIButton) {
//開始事務(wù)
CATransaction.begin()
defer {
//提交事務(wù)
CATransaction.commit()
}
CATransaction.setAnimationDuration(1.0)
let red = CGFloat(arc4random() % 256) / 255.0
let green = CGFloat(arc4random() % 256) / 255.0
let blue = CGFloat(arc4random() % 256) / 255.0
colorLayer.backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0).cgColor
}
當(dāng)然UIView中也有對應(yīng)的方法 beginAnimations(_ : , context: )
逻悠, commitAnimations()
等方法童谒。當(dāng)然最常用的還是 UIView中基于閉包的動畫方法:animate(withDuration: , animations:)
完成塊
上面提到了 UIView
中常用的使用閉包來處理動畫的方式饥伊,而且還有有一個 completion
的閉包來表示動畫已經(jīng)執(zhí)行結(jié)束(完成塊)撵渡。CATranscation
中也有對應(yīng)的方法 setCompletionBlock()
作為動畫完成后的回調(diào)。下面我們來讓方塊每次改變顏色后都旋轉(zhuǎn)90°越除。
@IBAction func changeBtnClick(_ sender: UIButton) {
//開始事務(wù)
CATransaction.begin()
defer {
//提交事務(wù)
CATransaction.commit()
}
CATransaction.setAnimationDuration(1.0)
CATransaction.setCompletionBlock {
var transform = self.colorLayer.affineTransform()
transform = transform.rotated(by: CGFloat(M_PI_2))
self.colorLayer.setAffineTransform(transform)
}
let red = CGFloat(arc4random() % 256) / 255.0
let green = CGFloat(arc4random() % 256) / 255.0
let blue = CGFloat(arc4random() % 256) / 255.0
colorLayer.backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0).cgColor
}
圖層行為
處于好奇摘盆,你可能會嘗試打開事務(wù)之后直接對視圖孩擂,也就是對UIView
設(shè)置背景顏色类垦。然后你就會發(fā)現(xiàn)視圖的背景顏色是瞬間改變的蚤认,并沒有漸變的過程砰琢。原因也很簡單训唱,隱式動畫被UIView
禁用掉了挚冤。
之前也提到過你辣,UIView
顯示的內(nèi)容實際都是它內(nèi)部的關(guān)聯(lián)圖層上的內(nèi)容舍哄,那么UIView
是怎么禁用掉內(nèi)部圖層的隱式動畫的呢表悬?當(dāng)我們改變圖層屬性的時候蟆沫,它會調(diào)用 action(forKey:)
方法戒悠。關(guān)于 action(forKey:)
的調(diào)用我們可以在CALayer的頭文件中找到說明:
- 首先查看代理是否實現(xiàn)了
action(for layer: CALayer, forKey event: String)
如果實現(xiàn)了則調(diào)用并返回結(jié)果 - 如果沒有代理或者代理未實現(xiàn)該方法绸狐,那么圖層接著會檢查包含屬性名稱對應(yīng)行為的
action
字典 - 如果
action
中沒有包含的屬性寒矿,那么會繼續(xù)在圖層的style
字典中查找屬性名 - 如果
style
中也沒有查找到的話符相,圖層會直接調(diào)用defaultActionForKey()
方法啊终,返回一個標(biāo)準(zhǔn)行為傲须。
結(jié)果也就很明顯了,UIView
為繼承了代理搞旭,并且如果當(dāng)前View
不在動畫塊內(nèi)的話肄渗,action(forKey:)
方法就返回nil
翎嫡。
override func viewDidLoad() {
super.viewDidLoad()
print("Outside: \(layerView .action(for: self.layerView.layer, forKey: "backgroundColor"))")
UIView.beginAnimations(nil, context: nil)
print("Inside: \(layerView .action(for: self.layerView.layer, forKey: "backgroundColor"))")
UIView.commitAnimations();
}
當(dāng)然我們也可以通過 CATransaction
來禁用動畫,在 CATransaction.begin()
之后調(diào)用
CATransaction.setDisableActions(true)
鋪墊了這么多具伍,接下來我們嘗試通過為colorLayer
定義一個action
字典來實現(xiàn)自定義圖層行為人芽,例如我們希望新的顏色不是漸變的萤厅,而是從左側(cè)劃入的惕味。
class TransactionViewController: UIViewController {
@IBOutlet weak var layerView: UIView!
weak var colorLayer: CALayer!
override func viewDidLoad() {
super.viewDidLoad()
let layer = CALayer()
self.layerView.layer.addSublayer(layer)
layer.frame = CGRect(x: 35, y: 20, width: 180, height: 180)
layer.backgroundColor = UIColor.clear.cgColor;
let transition = CATransition()
transition.type = kCATransitionPush
transition.subtype = kCATransitionFromLeft
layer.actions = ["backgroundColor": transition]
colorLayer = layer
}
@IBAction func changeBtnClick(_ sender: UIButton) {
//開始事務(wù)
CATransaction.begin()
defer {
//提交事務(wù)
CATransaction.commit()
}
CATransaction.setAnimationDuration(1.0)
CATransaction.setCompletionBlock {
var transform = self.colorLayer.affineTransform()
transform = transform.rotated(by: CGFloat(M_PI_2))
self.colorLayer.setAffineTransform(transform)
}
let red = CGFloat(arc4random() % 256) / 255.0
let green = CGFloat(arc4random() % 256) / 255.0
let blue = CGFloat(arc4random() % 256) / 255.0
colorLayer.backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0).cgColor
}
}
呈現(xiàn)(presentation)與模型(model)
這部分內(nèi)容比較繁瑣主守,我就只是引用原文的內(nèi)容了丸逸,不過簡單來講就是我們看到的動畫(presentation)與我們實際操作的圖層(model)時兩個圖層。
CALayer
的屬性行為其實很不正常憔维,因為改變一個圖層的屬性并沒有立刻生效畏邢,而是通過一段時間漸變更新舒萎。這是怎么做到的呢?
當(dāng)你改變一個圖層的屬性摊灭,屬性值的確是立刻更新的(如果你讀取它的數(shù)據(jù)败徊,你會發(fā)現(xiàn)它的值在你設(shè)置它的那一刻就已經(jīng)生效了)煤杀,但是屏幕上并沒有馬上發(fā)生改變沈自。這是因為你設(shè)置的屬性并沒有直接調(diào)整圖層的外觀酥泛,相反柔袁,他只是定義了圖層動畫結(jié)束之后將要變化的外觀捶索。
當(dāng)設(shè)置 CALayer
的屬性,實際上是在定義當(dāng)前事務(wù)結(jié)束之后圖層如何顯示的模型燎竖。Core Animation
扮演了一個控制器的角色构回,并且負(fù)責(zé)根據(jù)圖層行為和事務(wù)設(shè)置去不斷更新視圖的這些屬性在屏幕上的狀態(tài)纤掸。
我們討論的就是一個典型的微型 MVC
模式。CALayer
是一個連接用戶界面(就是 MVC
中的 view
)虛構(gòu)的類掏愁,但是在界面本身這個場景下,CALayer
的行為更像是存儲了視圖如何顯示和動畫的數(shù)據(jù)模型印蓖。實際上,在蘋果自己的文檔中他宛,圖層樹通常都是值的圖層樹模型。
在iOS
中欠气,屏幕每秒鐘重繪60次厅各。如果動畫時長比60分之一秒要長, Core Animation
就需要在設(shè)置一次新值和新值生效之間预柒,對屏幕上的圖層進行重新組織队塘。這意味著 CALayer
除了“真實”值(就是你設(shè)置的值)之外,必須要知道當(dāng)前顯示在屏幕上的屬性值的記錄宜鸯。
每個圖層屬性的顯示值都被存儲在一個叫做呈現(xiàn)圖層的獨立圖層當(dāng)中憔古,他可以通過 -presentationLayer
方法來訪問。這個呈現(xiàn)圖層實際上是模型圖層的復(fù)制淋袖,但是它的屬性值代表了在任何指定時刻當(dāng)前外觀效果鸿市。換句話說焰情,你可以通過呈現(xiàn)圖層的值來獲取當(dāng)前屏幕上真正顯示出來的值初橘。
我們在第一章中提到除了圖層樹展东,另外還有呈現(xiàn)樹。呈現(xiàn)樹通過圖層樹中所有圖層的呈現(xiàn)圖層所形成。注意呈現(xiàn)圖層僅僅當(dāng)圖層首次被提交(就是首次第一次在屏幕上顯示)的時候創(chuàng)建,所以在那之前調(diào)用 -presentationLayer
將會返回nil。
你可能注意到有一個叫做 –modelLayer
的方法屹培。在呈現(xiàn)圖層上調(diào)用 –modelLayer
將會返回它正在呈現(xiàn)所依賴的 CALayer
溜歪。通常在一個圖層上調(diào)用 -modelLayer
會返回 self
(實際上我們已經(jīng)創(chuàng)建的原始圖層就是一種數(shù)據(jù)模型)自阱。
大多數(shù)情況下叫确,你不需要直接訪問呈現(xiàn)圖層次乓,你可以通過和模型圖層的交互,來讓 Core Animation
更新顯示。兩種情況下呈現(xiàn)圖層會變得很有用,一個是同步動畫,一個是處理用戶交互肖抱。
- 如果你在實現(xiàn)一個基于定時器的動畫荤崇,而不僅僅是基于事務(wù)的動畫每篷,這個時候準(zhǔn)確地知道在某一時刻圖層顯示在什么位置就會對正確擺放圖層很有用了仑嗅。
- 如果你想讓你做動畫的圖層響應(yīng)用戶輸入,你可以使用-hitTest:方法來判斷指定圖層是否被觸摸涯保,這時候?qū)Τ尸F(xiàn)圖層而不是模型圖層調(diào)用-hitTest:會顯得更有意義及志,因為呈現(xiàn)圖層代表了用戶當(dāng)前看到的圖層位置冶共,而不是當(dāng)前動畫結(jié)束之后的位置谐岁。
接下來的例子中塞祈,點擊屏幕上的任意位置將會讓圖層平移到那里坯临。點擊圖層本身可以隨機改變它的顏色辟宗。我們通過對呈現(xiàn)圖層調(diào)用 -hitTest:
來判斷是否被點擊。
如果修改代碼讓 -hitTest:
直接作用于 colorLayer
而不是呈現(xiàn)圖層芥丧,你會發(fā)現(xiàn)當(dāng)圖層移動的時候它并不能正確顯示坊罢。這時候你就需要點擊圖層將要移動到的位置而不是圖層本身來響應(yīng)點擊(這就是為什么用呈現(xiàn)圖層來響應(yīng)交互的原因)。
class HitTestViewController: UIViewController {
weak var colorLayer: CALayer!
override func viewDidLoad() {
super.viewDidLoad()
let layer = CALayer()
view.layer.addSublayer(layer)
layer.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
layer.position = view.layer.position
layer.backgroundColor = UIColor.red.cgColor
colorLayer = layer
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let point = touches.first?.location(in: view)
if self.colorLayer.presentation()?.hitTest(point!) != nil {
let red = CGFloat(arc4random() % 256) / 255.0
let green = CGFloat(arc4random() % 256) / 255.0
let blue = CGFloat(arc4random() % 256) / 255.0
colorLayer.backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0).cgColor
} else {
CATransaction.begin()
CATransaction.setAnimationDuration(4.0)
colorLayer.position = point!
CATransaction.commit()
}
}
}
總結(jié)
本章主要介紹了 Core Animation
的隱式動畫已經(jīng)背后實現(xiàn)的原理始衅。
往期回顧:
序章
第一章 - 圖層樹
第二章 - 寄宿圖
第三章 - 圖層幾何
第四章 - 視覺效果
第五章 - 變換
第六章 專用圖層(上)
第六章 專用圖層(下)
項目中使用的代碼