動畫事物
CATransaction是 Core Animation 中的事務(wù)類,在iOS中的圖層中,圖層的每個(gè)改變都是事務(wù)的一部分,CATransaction可以對多個(gè)layer的屬性同時(shí)進(jìn)行修改,同時(shí)負(fù)責(zé)批量的把多個(gè)圖層樹的修改作為一個(gè)原子更新到渲染樹。
屬性和方法了解
- 創(chuàng)建和提交事物(Creating and Committing Transactions)
// 當(dāng)前線程創(chuàng)建一個(gè)新的事物(Transaction),可嵌套
open class func begin()
// 提交當(dāng)前事物中的所有改動,如果事物不存在將會出現(xiàn)異常
open class func commit()
// 提交任意的隱式動畫,將被延遲一直到嵌套的顯示事物被完成
open class func flush()
- 重寫動畫時(shí)間(Overriding Animation Duration and Timing)
// 獲取動畫時(shí)間,默認(rèn)0.25秒
open class func animationDuration() -> CFTimeInterval
// 設(shè)置動畫時(shí)間
open class func setAnimationDuration(_ dur: CFTimeInterval)
// 默認(rèn)nil,設(shè)置和獲取CAMediaTimingFunction(速度控制函數(shù))
open class func animationTimingFunction() -> CAMediaTimingFunction?
open class func setAnimationTimingFunction(_ function: CAMediaTimingFunction?)
- 失效屬性動畫(Temporarily Disabling Property Animations)
// 每一個(gè)線程事物屬性都有存取器,即設(shè)置和獲取方法,默認(rèn)為false,允許隱式動畫
open class func disableActions() -> Bool
open class func setDisableActions(_ flag: Bool)
- 回調(diào)閉包(Getting and Setting Completion Block Objects)
// 動畫完成之后被調(diào)用
open class func completionBlock() -> (() -> Void)?
open class func setCompletionBlock(_ block: (() -> Void)?)
- 管理并發(fā)(Managing Concurrency)
// 兩個(gè)方法用于動畫事物的加鎖與解鎖 在多線程動畫中,保證修改屬性的安全
open class func lock()
open class func unlock()
- 設(shè)置和獲取事物屬性(Getting and Setting Transaction Properties)
open class func value(forKey key: String) -> Any?
open class func setValue(_ anObject: Any?, forKey key: String)
支持的屬性
// 設(shè)置動畫持續(xù)時(shí)間
public let kCATransactionAnimationDuration: String
// 設(shè)置停用animation類動畫
public let kCATransactionDisableActions: String
// 設(shè)置動畫時(shí)序效果
public let kCATransactionAnimationTimingFunction: String
// 設(shè)置動畫完成后的回調(diào)
public let kCATransactionCompletionBlock: String
CATransaction事務(wù)類分為隱式事務(wù)和顯式事務(wù),注意以下兩組概念的區(qū)分:
- 隱式動畫和隱式事務(wù)
隱式動畫通過隱式事務(wù)實(shí)現(xiàn)動畫 。
- 顯式動畫和顯式事務(wù)
顯式動畫有多種實(shí)現(xiàn)方式,顯式事務(wù)是一種實(shí)現(xiàn)顯式動畫的方式。
隱式事務(wù)
隱式事務(wù)是基于CALayer,任何對于CALayer屬性的修改,都是隱式事務(wù).這樣的事務(wù)會在run-loop中被提交。
首先看一個(gè)簡單例子
class ViewController: UIViewController {
lazy var layer: CALayer = {
let layer = CALayer()
layer.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
layer.backgroundColor = UIColor.red.cgColor
return layer
}()
lazy var button: UIButton = {
let button = UIButton(type: .custom)
button.setTitle("button", for: .normal)
button.setTitleColor(UIColor.black, for: .normal)
button.frame = CGRect(x: 100, y: 250 , width: 100, height: 50)
button.addTarget(self, action: #selector(buttonClick) , for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.layer.addSublayer(layer)
view.addSubview(button)
}
@objc func buttonClick() {
layer.backgroundColor = UIColor.yellow.cgColor
}
}
代碼很簡單,而且并沒有添加動畫相關(guān)的代碼,但運(yùn)行程序之后,單擊按鈕,layer的顏色是有漸變的效果的,即動畫效果,這就是隱式動畫

隱式事務(wù)是CoreAnimation的一部分,是對layer-tree進(jìn)行原子更新為render-tree的機(jī)制,由CoreAnimation幫助創(chuàng)建事務(wù),當(dāng)前線程的runloop下次循環(huán)就會自動commit,如果當(dāng)前線程沒有runloop,或者runloop被阻塞,則應(yīng)該使用顯示事務(wù),即手動創(chuàng)建CATransaction
顯示事務(wù)
顯示的調(diào)用事物,修改buttonClick方法
@objc func buttonClick() {
CATransaction.begin()
layer.backgroundColor = UIColor.yellow.cgColor
CATransaction.commit()
}
僅僅是簡單的調(diào)用CATransaction的begin和commit方法就可以實(shí)現(xiàn)顯示事務(wù),運(yùn)行程序之后看到的效果是一樣的。還有相關(guān)方法可以設(shè)置,比如:設(shè)置動畫時(shí)間以及事務(wù)完成的閉包
@objc func buttonClick() {
CATransaction.begin()
CATransaction.setAnimationDuration(2) // 時(shí)間2秒
CATransaction.setCompletionBlock {
print("tranction end")
}
layer.backgroundColor = UIColor.yellow.cgColor
CATransaction.commit()
}
使用非常簡單,但是需要注意代碼的先后順序,需要將更改的關(guān)鍵代碼放在設(shè)置時(shí)間和完成回調(diào)之后,這樣才能達(dá)到想要的效果,如果放在之前,相當(dāng)于在執(zhí)行動畫操作時(shí),還沒有對其進(jìn)行動畫時(shí)間和完成回調(diào)的賦值
在完成的回調(diào)中也可以對layer進(jìn)行修改,同樣默認(rèn)是隱式動畫,如果需要設(shè)置時(shí)間,需要顯示設(shè)置。
@objc func buttonClick() {
CATransaction.begin()
CATransaction.setAnimationDuration(2)
CATransaction.setCompletionBlock {
CATransaction.setAnimationDuration(1)
self.layer.backgroundColor = UIColor.cyan.cgColor
}
layer.backgroundColor = UIColor.yellow.cgColor
CATransaction.commit()
}
嵌套多個(gè)事務(wù)組
效果類似于一組動畫同時(shí)進(jìn)行的效果的效果
@objc func buttonClick() {
CATransaction.begin()
CATransaction.setAnimationDuration(2)
CATransaction.setCompletionBlock {
CATransaction.setAnimationDuration(1)
self.layer.frame = CGRect(x: 50, y: 100, width: 100, height: 100)
}
layer.backgroundColor = UIColor.yellow.cgColor
CATransaction.begin()
layer.cornerRadius = 20
CATransaction.commit()
CATransaction.commit()
}

相當(dāng)于先改變顏色和圓角,是同時(shí)進(jìn)行的,修改圓角屬性默認(rèn)是0.25s,最后移動layer。
除了同時(shí)修改layer的動畫,我們還可以組合UIView和CALayer的動畫
func addStyledButton() {
styledButton = UIButton(frame: CGRect(x: 0, y: 0, width: 125, height: 125))
styledButton.backgroundColor = UIColor(red: 57/255.0, green: 73/255.0, blue: 171/255.0, alpha: 1)
styledButton.layer.cornerRadius = styledButton.frame.width/2
styledButton.center = self.view.center
self.view.addSubview(styledButton)
}
func animateButton(duration: CFTimeInterval = 1.0) {
let oldValue = styledButton.frame.width/2
let newButtonWidth: CGFloat = 60
let timingFunction = CAMediaTimingFunction(controlPoints: 0.65, -0.55, 0.27, 1.55)
/* Do Animations */
CATransaction.begin()
CATransaction.setAnimationDuration(duration)
CATransaction.setAnimationTimingFunction(timingFunction)
// View animations
UIView.animate(withDuration: duration) {
self.styledButton.frame = CGRect(x: 0, y: 0, width: newButtonWidth, height: newButtonWidth)
self.styledButton.center = self.view.center
}
// Layer animations
let cornerAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.cornerRadius))
cornerAnimation.fromValue = oldValue
cornerAnimation.toValue = newButtonWidth/2
styledButton.layer.cornerRadius = newButtonWidth/2
styledButton.layer.add(cornerAnimation, forKey: #keyPath(CALayer.cornerRadius))
CATransaction.commit()
}

如果上面的例子不是CALyer,而是UIView,則并不會觸發(fā)隱式事務(wù)動畫,同樣對于顯示事務(wù)動畫也不會有作用,這是因?yàn)閁IKit禁止了事務(wù)動畫。
之所以CALyer可以對事務(wù)動畫作出響應(yīng),是因?yàn)镃ALyer的實(shí)例方法
open func action(forKey event: String) -> CAAction?
可以對其進(jìn)行響應(yīng),返回對應(yīng)的action。但對于UIView來說,UIView作為CALyer的代理,則根據(jù)名稱來獲取action,會遵循以下順序
1)如果有代理,則調(diào)用代理方法
optional public func action(for layer: CALayer, forKey event: String) -> CAAction?
2)如果沒有委托,或者委托沒有實(shí)現(xiàn)action(for layer: CALayer, forKey event: String) 方法,圖層接著檢查包含屬性名稱對應(yīng)行為映射的actions字典
3)檢查layer的style層級中每個(gè)actions字典
4)調(diào)用layer的類方法
open class func defaultAction(forKey event: String) -> CAAction?
所以當(dāng)需要做事務(wù)動畫時(shí),會按照如上順序獲取對應(yīng)的action,如果獲取到的是nil,則不會對事務(wù)動畫做出響應(yīng),如果返回非nil,則可以做事務(wù)動畫,因此,UIView通過其代理方法func action(for layer: CALayer, forKey event: String)返回nil來禁止事務(wù)動畫。
如果想實(shí)現(xiàn)UIView的隱式動畫,可以自定義UIView,并重寫func action(for layer: CALayer, forKey event: String)方法返回對應(yīng)的action
// CustomView.swift
class CustomView: UIView {
override func action(for layer: CALayer, forKey event: String) -> CAAction? {
return layer.actions?[event]
}
}
// ViewController.swift
lazy var button: UIButton = {
let button = UIButton(type: .custom)
button.setTitle("button", for: .normal)
button.setTitleColor(UIColor.black, for: .normal)
button.frame = CGRect(x: 100, y: 250 , width: 100, height: 50)
button.addTarget(self, action: #selector(buttonClick) , for: .touchUpInside)
return button
}()
lazy var myView: CustomView = {
let view = CustomView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
view.backgroundColor = UIColor.red
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(myView)
view.addSubview(button)
}
@objc func buttonClick() {
myView.backgroundColor = UIColor.yellow
}
運(yùn)行程序之后,可以看到根之前CALayer是一樣的效果
關(guān)于事務(wù)動畫,還有一個(gè)場景會經(jīng)常用到,即鍵盤監(jiān)聽。在某些情況下,需要對鍵盤彈出進(jìn)行監(jiān)聽,并彈出一個(gè)輸入框
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow(_:)),
name: UIResponder.keyboardWillShowNotification ,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillHide(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil)
@objc func keyboardWillShow(_ notification: Notification) {
}
@objc func keyboardWillHide(_ notification: Notification) {
}
可以知道輸入框與鍵盤總是保持相同的速率出現(xiàn)和消失,其實(shí)這是因?yàn)樵阪I盤彈出和消失情況下發(fā)送通知,是在事務(wù)動畫之中執(zhí)行的,如果想要禁用動畫效果,只需要在開始和結(jié)束兩個(gè)方法中禁用即可
@objc func keyboardWillShow(_ notification: Notification) {
UIView.setAnimationsEnabled(false)
//...
UIView.setAnimationsEnabled(true)
}
@objc func keyboardWillHide(_ notification: Notification) {
UIView.setAnimationsEnabled(false)
//...
UIView.setAnimationsEnabled(true)
}