如何你想某事正確,自己動手做吧。——Charles-Guillaume étienne
前一章介紹了隱式動畫的概念。隱式動畫是iOS上創建用戶界面的一種直接的方式,它們是UIKit
自身動畫機制的基礎,但它們并不是一個完整的通用的動畫方案。這一章,我們將講解顯式動畫,這使我們得以對特殊屬性指定自定義動畫或者創建非線性動畫,如沿弧移動。
屬性動畫
我們將講解的第一種顯式動畫類型是屬性動畫。屬性動畫針對于圖層的一個單一屬性,并指定動畫屬性的目標值的區間。屬性動畫分為兩類:基礎和關鍵幀。
基礎動畫
基礎動畫是隨時間發生的,是值改變的最簡單的方法,它是CABasicAnimation
設計的模型。
CABasicAnimation
是CAPropertyAnimation
這一抽象類的具體子類,它是CAAnimation
的子類,而CAAnimation
是Core Animation
提供的所有動畫類型的抽象基類。身為抽象類,CAAnimation
自身并不會明確實現功能。它提供了一個時間函數(正如第10章“緩動”所講解的),一個委托(用于獲得動畫狀態反饋),以及一個removedOnCompletion
標志用于指示是否在結束后自動釋放(這默認為YES
,這會防止內存溢出)。CAAnimation
也實現包括CAAction
(允許任何CAAnimation
子類被作為圖層動作支持)以及CAMediaTiming
(將在第9章“圖層時間”中細說)等一系列協議。
CAPropertyAnimation
作用于單一屬性,由動畫的keyPath
值指定。CAAnimation
通常用于某一CALayer
,同樣keyPath
是與該圖層相關的。事實上這是一個關鍵路徑(一系列由點限定的鍵,它們指向一個有層次的直接結構對象)而并非僅是一個屬性名,keyPath
十分有趣,它意味著動畫不僅可以應用于圖層自身,還可以應用于成員對象的屬性,甚至虛擬屬性(稍后細說)。
CABasicAnimation
對CAPropertyAnimation
擴展了三個額外的屬性:
id fromValue
id toValue
id byValue
它們的意思如同字面所示:fromValue
表示動畫開始時的屬性值;toValue
表示動畫結束時的屬性值;byValue
表示動畫期間改變的屬性值。
通過這三個屬性,你可以用不同的方式改變某個值。它們的類型是id
(而不是具體類),這是因為屬性動畫可以用于包括數值、向量、變形矩陣以及顏色和圖像在內的不同的屬性類型。
id
類型的屬性可以包含任意派生的NSObject
,但通常你會想要不是繼承自NSObject
的屬性類型,這意味著你將需要將值包裝成一個對象(被稱作boxing)或者轉型為一個對象(被稱作toll-free bredging),這使得任意Core Foundation
類型表現的如同Objective-C
類,即使它們本身并不是。有時如何將想要的數據類型轉換成適配id
的值是并不明顯的,但表格8.1列舉了一些普遍的情況。
表格8.1 用CAPropertyAnimation`封裝原始值
類型 | 對象類型 | 代碼例子 |
---|---|---|
CGFloat | NSNumber | id obj = @(float); |
CGPoint | NSValue | id obj = [NSValue valueWithCGPoint: point); |
CGSize | NSValue | id obj = [NSValue valueWithCGSize: size); |
CGRect | NSValue | id obj = [NSValue valueWithCGRect: rect); |
CATransform3D | NSValue | id obj = [NSValue valueWithCATransform3D: transform); |
CGImageRef | id | id obj = (__bridge id)imageRef; |
CGColorRef | id | id obj = (__bridge id)colorRef; |
fromValue
,toValue
,和byValue
屬性可以被用于不同的組合,但你不能同時指定三個值,這樣會引起沖突。例如,如果你指定fromValue
為2
,toValue
為4
,而byValue
為3,Core Animation
并不知道最終值應該為4
(由toValue
指定)還是5
(formValue
+byValue
)。關于這些屬性值到底如何使用在CABasicAnimation
的頭文件中有良好的文檔,所以在這我們并不重復它們。通常,你只需要指定toValue
或byValue
;其它值會根據上下文推斷出來。
讓我們試一下:我們將會修改第7章“隱式動畫”的顏色漸變動畫,我們將使用一個明確的CABasicAnimation
而非一個隱式動畫。表8.1展示了代碼。
表8.1 用CABasicAnimation設置圖層背景顏色
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var layerView: UIView!
var colorLayer: CALayer!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 判斷橫屏
let screenSize = UIScreen.mainScreen().applicationFrame.size
if (screenSize.width > screenSize.height) {
// 創建子圖層
self.colorLayer = CALayer()
self.colorLayer.frame = CGRectMake(50, 50, 100, 100)
self.colorLayer.backgroundColor = UIColor.blueColor().CGColor
// 添加到視圖中
self.layerView.layer.addSublayer(self.colorLayer)
}
}
@IBAction func changeColor(sender: AnyObject) {
// 創建隨機顏色
let red: CGFloat = CGFloat(arc4random()) / CGFloat(INT_MAX)
let green: CGFloat = CGFloat(arc4random()) / CGFloat(INT_MAX)
let blue: CGFloat = CGFloat(arc4random()) / CGFloat(INT_MAX)
let color = UIColor(red: red, green: green, blue: blue, alpha: 1.0).CGColor
// 創建基本動畫
let animation = CABasicAnimation()
animation.keyPath = "backgroundColor"
animation.toValue = color
// 對圖層應用動畫
self.colorLayer.addAnimation(animation, forKey: nil)
}
}
當我們運行程序時,它并不像想象中運行。點擊按鈕后圖層會動畫成一個新顏色,但它馬上又跳回原值。
其原因是動畫并沒有修改的模型,而修改了它的展示(見第7章)。一但動畫結束,它會從圖層移除,圖層變回它模型屬性定義的樣子。我們從未修改過backgroundColor
屬性,所以圖層返回它的初值。
當我們之前使用隱式動畫時,底層動作是使用一個CABasicAnimation
實現的,恰如我們之前用過那個一樣。(你可能回憶起在第7章,我們輸出了-actionForLayer:forKey:
委托方法的日志記錄,然后發現動作類型是CABasicAnimation
。)然而,在那里,我們通過設置屬性來觸發動畫。現在我們用直接使用的畫,但我們并沒有設置屬性(因此引發了閃回問題)。
將我們的動畫注冊為圖層動作(然后簡單地通過改變值來觸發動畫)是目前為止最簡單的保持屬性值并同步動畫狀態的方法,但假使我們因為某些原因我們不能使用這個方法(通常因為我們需要動畫的圖層是一個UIView
的主圖層),我們有兩個選擇來更新屬性值:動畫開始前或結束后。
相比之下在動畫開始前更新屬性會更簡單點,但這意味著我們無法利用隱式的fromValue
的優勢,因此我們需要手動設置動畫的fromValue
來匹配圖層的當前值。
考慮到這點,在創建動畫的代碼中,如果我們在將它添加到圖層時添加如下兩行代碼,將可以避免閃回:
animation.fromValue = self.colorLayer.backgroundColor
self.colorLayer.backgroundColor = color
這起作用了,但它并不可靠。我們應該從展示層(如果存在)而非模型層派生出fromValue
,除非進程中早有動畫。而且,因為這個圖層并不是主圖層,在設置屬性前我們應該用CATransaction
來禁用隱式動畫,否則默認的圖層行為會影響我們的顯式動畫。(事實上,顯式動畫通常會覆蓋隱式動畫,但這一行為并沒有記錄在文檔中,所以為了安全起見不要隨意使用。)
如果我們做了這些改變,我們以以下這些代碼結束:
let layer = (self.colorLayer.presentationLayer() != nil) ? self.colorLayer.presentationLayer() as? CALayer : self.colorLayer
animation.fromValue = layer.backgroundColor
CATransaction.begin()
CATransaction.setDisableActions(true)
self.colorLayer.backgroundColor = color
CATransaction.commit()
這樣需要給每個動畫加上許多代碼。幸運的是,我們可以從CABasicAnimation
對象本身自動派生這些信息,所以我們可以創建一個可復用的方法。表8.2展示了我們第一個例子的改版,它包括一個方法來應用一個CABasicAnimation
而無需重復這些冗長的代碼。
表8.2 一個用于修復動畫閃回的可復用方法
func applyBasicAnimation(animation: CABasicAnimation, toLayer layer: CALayer) {
// 設置起始值(如果可能使用展示層)
animation.fromValue = ((layer.presentationLayer() != nil) ? layer.presentationLayer() as? CALayer : layer)?.valueForKeyPath(animation.keyPath)
// 提前更新屬性
// 注意:這一方法只在toValue != nil時可用
CATransaction.begin()
CATransaction.setDisableActions(true)
layer.setValue(animation.toValue, forKeyPath: animation.keyPath)
CATransaction.commit()
// 給圖層應用動畫
layer.addAnimation(animation, forKey: nil)
}
@IBAction func changeColor(sender: AnyObject) {
// 創建隨機顏色
let red: CGFloat = CGFloat(arc4random()) / CGFloat(INT_MAX)
let green: CGFloat = CGFloat(arc4random()) / CGFloat(INT_MAX)
let blue: CGFloat = CGFloat(arc4random()) / CGFloat(INT_MAX)
let color = UIColor(red: red, green: green, blue: blue, alpha: 1.0).CGColor
// 創建基本動畫
let animation = CABasicAnimation()
animation.keyPath = "backgroundColor"
animation.toValue = color
// 添加無閃回的動畫
self.applyBasicAnimation(animation, toLayer: self.colorLayer)
}
這一簡單實現只處理有toValue
而非byValue
的動畫,但它是面向通用解決方案的好的開始。你可以將其封裝成CALayer
的類別(category)方法來使其更加方便及可復用。
這一切看起來好像是用了很復雜的方法解決這一簡單的問題,但這一選擇其實有更復雜的考慮。如果在我們動畫開始前不更新目標屬性,我們在其完全結束前都不能更新它,否則我們將會在進程里取消這一CABasicAnimation
。這意味著我們需要在動畫結束后的恰當時刻更新屬性,但應該在其從圖層移除前,否則屬性會閃回初始值。我們如何判定這一時間點?
CAAniamtionDelegate
在第7章中,當我們使用隱式動畫時,我們可以用CATransaction
閉包來檢測動畫結束。然而使用顯式動畫時,這一方法并不適用,這是因為這一動畫與事務無關。
為了發現顯式動畫何時結束,我們需要使用動畫的delegate
屬性,它遵循CAAnimationDelegate
協議。
CAAnimationDelegate
是一個自助協議,所以你不會在任何頭文件中找到CAAnimationDelegate
這一協議的定義,但你可以在Apple的開發者文檔的CAAnimation
中發現相應的支持方法。在這里,我們使用-animationDidStop:finished:
方法來在動畫結束后立即更新我們的圖層backgroundColor
。
我們需要在更新屬性時開始新事務并禁用圖層動作;否則,動畫將發生兩次——一次出于我們的顯式CABasicAnimation
,再一次因為該屬性的隱式動畫。見表8.3所示的完整實現。
表8.3 實現背景顏色值一次動畫
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var layerView: UIView!
var colorLayer: CALayer!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 判斷橫屏
let screenSize = UIScreen.mainScreen().applicationFrame.size
if (screenSize.width > screenSize.height) {
// 創建子圖層
self.colorLayer = CALayer()
self.colorLayer.frame = CGRectMake(50, 50, 100, 100)
self.colorLayer.backgroundColor = UIColor.blueColor().CGColor
// 添加到視圖中
self.layerView.layer.addSublayer(self.colorLayer)
}
}
@IBAction func changeColor(sender: AnyObject) {
// 創建隨機顏色
let red: CGFloat = CGFloat(arc4random()) / CGFloat(INT_MAX)
let green: CGFloat = CGFloat(arc4random()) / CGFloat(INT_MAX)
let blue: CGFloat = CGFloat(arc4random()) / CGFloat(INT_MAX)
let color = UIColor(red: red, green: green, blue: blue, alpha: 1.0).CGColor
// 創建基本動畫
let animation = CABasicAnimation()
animation.keyPath = "backgroundColor"
animation.toValue = color
animation.delegate = self
// 給圖層應用動畫
self.colorLayer.addAnimation(animation, forKey: nil)
}
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
// 設置backgroundColor屬性來匹配動畫toValue
CATransaction.begin()
CATransaction.setDisableActions(true)
self.colorLayer.backgroundColor = (anim as? CABasicAnimation)?.toValue as! CGColorRef
CATransaction.commit()
}
}
使用CAAnimation
的委托方法而非閉包帶來的問題是你很容易陷入追蹤許多動畫和圖層的麻煩中。當在視圖控制器中創建動畫時,你通常會使用控制器本身作為動畫委托(正如我們在表8.3中所做的一樣),但因為所有的動畫都會調用同一個委托方法,你需要尋找圖層的相應的圖層。
考慮第3章“圖層幾何”中的時鐘;我們最初通過簡單的非動畫地更新指針角度來實現時鐘。如果我們讓指針像現實一樣動畫到相應位置看起來會更好。
我們不能使用隱式動畫來移動指針,因為指針是用UIView
實例展示的,而隱式動畫對它們的主圖層是禁用的。我們可以輕松地使用UIView
動畫方法來實現動畫,但如果使用顯式動畫,我們將得以控制動畫時間(將在第10章細說)。使用CABasicAnimation
移動指針動畫十分復雜,因為我們需要在-animaitonDidStop:finished:
方法中檢測指針的相應動畫(這樣我們才能設置它終止的位置)。
動畫本身作為委托方法的一個參數傳遞。你可能認為你可以在控制器中將動畫存儲成屬性然后與委托方法中的參數進行比較,但這并沒有用,因為委托返回的動畫是原始的一份不可變的拷貝,而非同一對象。
當我們使用-addAnimation:forKey:
來向我們的圖層添加動畫時,有一個我們至今總設為nil
的key
參數。這個鍵(key)是一個用于動畫的唯一標識的NSString
,可用于在圖層在-animationForKey:
方法中使用。當前附加到圖層的所以動畫的鍵可以使用animationKeys
取得。如果我們對每個動畫使用一個獨一無二的鍵,我們可以遍歷每一個動畫圖層的動畫鍵并與傳遞給我們委托方法的動畫對象調用-animationForKey:
的結果相比較。但這仍不是一個優雅的解決方案。
幸運的是,存在更為簡單的方法。像所有NSObject
子類一樣,CAAnimation
遵循KVC(Key-Value Coding鍵值碼)自助協議,這使得我們可以使用名字通過-setValue:forKey:
和-valueForKey:
方法來設置或取得屬性。但CAAnimation
有一個不一般的特性:它表現的如同NSDictionary
,允許你直接設置鍵值對,即使他們并不匹配任何你在用的動畫類的已聲明屬性。
這意味著你可以給一個動畫加上額外的數據標簽供自己使用。這里,我們將給動畫加上時鐘指針的UIView
,這樣我們可以容易的判斷每一個動畫相應的視圖。接下來我們在委托中使用這一信息來更新正確的指針(如表8.4)。
表8.4 使用KVC來給動畫添加額外的數據標簽
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var hourHand: UIImageView!
@IBOutlet weak var minuteHand: UIImageView!
@IBOutlet weak var secondHand: UIImageView!
var timer: NSTimer!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 判斷橫屏
let screenSize = UIScreen.mainScreen().applicationFrame.size
if (screenSize.width > screenSize.height) {
// 調整錨點
self.secondHand.layer.anchorPoint = CGPointMake(0.5, 0.9)
self.minuteHand.layer.anchorPoint = CGPointMake(0.5, 0.9)
self.hourHand.layer.anchorPoint = CGPointMake(0.5, 0.9)
// 啟動計時器
self.timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "tick", userInfo: nil, repeats: true)
// 設置初始指針位置
self.updateHandsAnimated(false)
}
}
func tick() {
self.updateHandsAnimated(true)
}
func updateHandsAnimated(animated: Bool) {
// 將時間轉換成小時、分鐘和秒
let calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierChinese)!
let units = NSCalendarUnit.CalendarUnitHour | NSCalendarUnit.CalendarUnitMinute | NSCalendarUnit.CalendarUnitSecond
let components = calendar.components(units, fromDate: NSDate())
// 計算時針角度
let hoursAngle: CGFloat = (CGFloat(components.hour) / 12.0) * CGFloat(M_PI * 2.0)
// 計算分針角度
let minsAngle: CGFloat = (CGFloat(components.minute) / 60.0) * CGFloat(M_PI * 2.0)
// 計算秒針角度
let secsAngle: CGFloat = (CGFloat(components.second) / 60.0) * CGFloat(M_PI * 2.0)
// 旋轉指針
self.setAngle(hoursAngle, forHand: hourHand, animated: animated)
self.setAngle(minsAngle, forHand: minuteHand, animated: animated)
self.setAngle(secsAngle, forHand: secondHand, animated: animated)
}
func setAngle(angle: CGFloat, forHand handView: UIView, animated: Bool) {
// 產生形變
let transform = CATransform3DMakeRotation(angle, 0, 0, 1)
if (animated) {
// 創建形變動畫
let animation = CABasicAnimation()
animation.keyPath = "transform"
animation.toValue = NSValue(CATransform3D: transform)
animation.duration = 0.5
animation.delegate = self
animation.setValue(handView, forKey: "handView")
handView.layer.addAnimation(animation, forKey: nil)
} else {
// 直接設置形變
handView.layer.transform = transform
}
}
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
// 為指針視圖設置最終位置
let handView: UIImageView = anim.valueForKey("handView") as! UIImageView
handView.layer.transform = (anim as! CABasicAnimation).toValue.CATransform3DValue
}
}
我們成功地在每一圖層結束動畫時辨別出它們,并且將它們的值更新成正確的形變值。到這一步,一切都很棒。
不幸的是,即使做了這些步驟,我們仍有另一個問題。表8.4在模擬器上運作正常,但如果我們在一臺iOS設備上運行它,我們會看見我們的時鐘指針在調用-animationDidStop:finished:
委托方法前會輕微地跳回其初始值。同樣的事情發生在表8.3的圖層顏色上。
問題在于盡管回調函數是在動畫結束后調用的,但并不能保證它在屬性重置為先前狀態前被調用。這是一個好例子用來說明你總應該在真機上測試動畫代碼,而不僅是在模擬器上。
我們可以通過使用一個叫fillMode
的屬性來解決這個問題,這個將在下一章中講解,但在本章中我們在應用動畫之前設置要動畫的屬性為最終值,這比嘗試在動畫結束后更新它更簡單。
關鍵幀動畫
CABasicAnimaiton
有趣之處在于它向我們展示了大多數iOS上的隱式動畫的底層機制。相比于用隱式動畫實現同樣動畫效果,顯式地向一個圖層添加CABasicAnimation
常常需要更多的工作才能換來一點小小的收益(無論是有組織圖層的隱式動畫或視圖或主圖層的UIView
動畫)。
然而CAKeyframeAnimation
是更為強大的,且在UIKit
中沒有暴露相應等同接口的。它像CABasicAnimation
一樣是CAPropertyAnimation
的子類。它同樣作用于單一屬性,但不同于CABasicAnimation
它并未限定為唯一的開始或結束值,它可以賦予一系列動畫區間的系列值。
術語關鍵幀來源于傳統動畫,表示的是首席繪畫師只繪制顯著發生的幀(關鍵幀),而技巧差點的藝術家繪制之前的幀(這可以容易地從關鍵幀中推出)。同樣的原則適用于CAKeyframeAnimation:
,你提供顯著幀,然后Core Animation
使用一個叫“插值”的過程填充空隙。
我們可以用我們先前的顏色圖層演示這個。我們將設置一組顏色然后用一個關鍵幀動畫采用一條命令回放它們(如表8.5)。
表8.5 使用CAKeyframeAnimation應用一系列顏色
@IBAction func changeColor(sender: AnyObject) {
// 創建關鍵幀動畫
let animation = CAKeyframeAnimation()
animation.keyPath = "backgroundColor"
animation.duration = 2.0
animation.values = [
UIColor.blueColor().CGColor,
UIColor.redColor().CGColor,
UIColor.greenColor().CGColor,
UIColor.blueColor().CGColor
]
// 給圖層應用動畫
self.colorLayer.addAnimation(animation, forKey: nil)
}
注意我們指定序列的開始和結束都是藍色。這是必須的,因為CAKeyframeAnimation
并沒有一個選項來自動使用當前值作為第一幀(因此我們在CABasicAnimation
中將fromValue
設為nil
)。動畫在開始后會立即跳到第一個關鍵幀值,然后在結束后立即退回其初始屬性值,為了形成一個平滑的動畫,我們需要開始和結束的關鍵幀都匹配當前的屬性值。
當然,也可以創建開始與結束不同值的動畫。那樣的話,我們需要在觸發動畫前手動更新屬性值來切爾西最后的關鍵幀,正如我們先前說過的那樣。
我們已經使用duration
屬性將動畫時長從默認的0.25秒增加到2秒,這樣動畫不至于快得看不清。如果你運行這一動畫,你會看見圖層依次變換這些顏色,但效果看起來有點奇怪。原因是動畫以一個恒定速度運行。在顏色之間過渡時并不會減速,這使得結果有點不真實感。為了使動畫看起來更自然,我們需要調整緩動(easing),這將會在第10章講解。
對于顏色改變的動畫來說一系列值顯得有意義,但通常對于描述動作來說就十分怪異了。CAKeyframeAnimation
有一個可選方法來指定動畫,就是使用CGPath
。path
屬性允許你用一種自然的方式定義運行序列,通過使用Core Graphics
函數來繪制你的動畫。
讓我們用一個沿簡單曲線移動的飛船圖像的動畫來演示這點。為了創建路徑,我們將使用一個三次貝塞爾曲線(cubic Bézier curve)
,這是一個用一個起點、一個終點以及兩個額外的控制點來描述形狀的特定曲線類型。可以使用純粹基于C的Core Graphics
的命令來創建這一路徑,但使用UIKit
提供的高層的UIBezierPath
類會更簡單。
盡管對于動畫來說并不是必須的,我們將用CAShapeLayer
來在屏幕上繪制這一曲線。這使其很容易視覺化的看出我們的動畫走向。在我們畫完CGPath
之后,我們用它創建一個CAKeyframeAnimation
,然后將之用于我們的飛船。表8.6展示了相應代碼,圖8.1展示了相應結果。
表8.6 沿三次貝塞爾曲線運動的圖層動畫
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var containerView: UIView!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 判斷橫屏
let screenSize = UIScreen.mainScreen().applicationFrame.size
if (screenSize.width > screenSize.height) {
// 創建路徑
let bezierPath = UIBezierPath()
bezierPath.moveToPoint(CGPointMake(0, 150))
bezierPath.addCurveToPoint(CGPointMake(300, 150), controlPoint1: CGPointMake(75, 0), controlPoint2: CGPointMake(225, 300))
// 使用CAShapeLayer繪制路徑
let pathLayer = CAShapeLayer()
pathLayer.path = bezierPath.CGPath
pathLayer.fillColor = UIColor.clearColor().CGColor
pathLayer.strokeColor = UIColor.redColor().CGColor
pathLayer.lineWidth = 3.0
self.containerView.layer.addSublayer(pathLayer)
// 增加飛船
let shipLayer = CALayer()
shipLayer.frame = CGRectMake(0, 0, 64, 64)
shipLayer.position = CGPointMake(0, 150)
// 譯者用之前的雪人圖像代替飛船,讀者理解方法就好
shipLayer.contents = UIImage(named: "Snowman")?.CGImage
self.containerView.layer.addSublayer(shipLayer)
// 創建關鍵幀動畫
let animation = CAKeyframeAnimation()
animation.keyPath = "position"
animation.duration = 4.0
animation.path = bezierPath.CGPath
shipLayer.addAnimation(animation, forKey: nil)
}
}
}
如果你運行這一案例,你會注意到動畫看起來有一點不真實,因為它移動時總指向同一個方向而不是隨曲線切線改變。你可以調整它的affineTransform
在其移動時改變朝向,但同步其它動畫將會十分麻煩。
幸運的是,Apple預料到了這一情況,給CAKeyframeAnimation
增加了一個叫rotationMode
的屬性。將rotationMode
設置為固定值kCAAnimationRotateAuto
(如表8.7),圖層將會在動畫時自動隨切線旋轉(如圖8.2)。
表8.7 使用rotationMode自動對齊圖層和曲線
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 判斷橫屏
let screenSize = UIScreen.mainScreen().applicationFrame.size
if (screenSize.width > screenSize.height) {
// 創建路徑
let bezierPath = UIBezierPath()
bezierPath.moveToPoint(CGPointMake(0, 150))
bezierPath.addCurveToPoint(CGPointMake(300, 150), controlPoint1: CGPointMake(75, 0), controlPoint2: CGPointMake(225, 300))
// 使用CAShapeLayer繪制路徑
let pathLayer = CAShapeLayer()
pathLayer.path = bezierPath.CGPath
pathLayer.fillColor = UIColor.clearColor().CGColor
pathLayer.strokeColor = UIColor.redColor().CGColor
pathLayer.lineWidth = 3.0
self.containerView.layer.addSublayer(pathLayer)
// 增加飛船
let shipLayer = CALayer()
shipLayer.frame = CGRectMake(0, 0, 64, 64)
shipLayer.position = CGPointMake(0, 150)
// 譯者用之前的雪人圖像代替飛船,讀者理解方法就好
shipLayer.contents = UIImage(named: "Snowman")?.CGImage
self.containerView.layer.addSublayer(shipLayer)
// 創建關鍵幀動畫
let animation = CAKeyframeAnimation()
animation.keyPath = "position"
animation.duration = 4.0
animation.path = bezierPath.CGPath
animation.rotationMode = kCAAnimationRotateAuto
shipLayer.addAnimation(animation, forKey: nil)
}
}
虛擬屬性
我們先前說過事實上屬性動畫作用于關鍵路徑而非鍵,這意味我們可以對子屬性甚至虛擬屬性添加動畫。但什么是虛擬屬性?
想象一個旋轉的動畫:如果我們想要添加一個旋轉對象動畫,我們不得不使用transform
,因為CALayer
并沒有任何顯式的角度/朝向屬性。我們可以如表8.8所示這樣做。
表8.8 動畫于transform屬性來旋轉圖層
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var containerView: UIView!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 判斷橫屏
let screenSize = UIScreen.mainScreen().applicationFrame.size
if (screenSize.width > screenSize.height) {
// 增加飛船
let shipLayer = CALayer()
shipLayer.frame = CGRectMake(0, 0, 64, 64)
shipLayer.position = CGPointMake(0, 150)
// 譯者用之前的雪人圖像代替飛船,讀者理解方法就好
shipLayer.contents = UIImage(named: "Snowman")?.CGImage
self.containerView.layer.addSublayer(shipLayer)
// 飛船旋轉動畫
let animation = CABasicAnimation()
animation.keyPath = "transform"
animation.duration = 2.0
animation.toValue = NSValue(CATransform3D: CATransform3DMakeRotation(CGFloat(M_PI), 0, 0, 1))
shipLayer.addAnimation(animation, forKey: nil)
}
}
}
這生效了,但這看起來更像是運氣好而非是設計的。如果我們將旋轉角度從M_PI
(180度)變為2 * M_PI
(360度),然后運行動畫,我們會發現飛船壓根不動。這是因為矩陣展示中360度等同于0度,所以就動畫而言,數值并沒有改變。
現在嘗試再次使用M_PI
,但是賦值給byValue
而非toValue
屬性,這表明旋轉應該是相對于當前值而言的。你可能以為這會和設置toValue
得到一樣的效果,因為0 + 90度==90度,但實際上圖像會拉伸而非旋轉,因為變形矩陣不能像角度值一樣相加。
如果我們想獨立于飛船角度移動或縮放飛船會怎么樣?因為它們都需要我們修改transform
屬性,我們需要重新計算每個時刻下的這些動畫的結合效果,然后從這些組合變形數據中創建一個復雜的關鍵幀動畫,即使我們真正想要做的只是對我們獨立圖層的一些概念上分享的屬性添加動畫效果。
幸運的是,有一個解決方案:為了旋轉圖層,我們可以將我們的動畫添加到transform.rotation
關鍵路徑上,而非直接添加到transform
屬性自身上(如表8.9)。
表8.9 給虛擬的transform.rotation屬性添加動畫
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var containerView: UIView!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 判斷橫屏
let screenSize = UIScreen.mainScreen().applicationFrame.size
if (screenSize.width > screenSize.height) {
// 增加飛船
let shipLayer = CALayer()
shipLayer.frame = CGRectMake(0, 0, 128, 128)
shipLayer.position = CGPointMake(150, 150)
// 譯者用之前的雪人圖像代替飛船,讀者理解方法就好
shipLayer.contents = UIImage(named: "Snowman")?.CGImage
self.containerView.layer.addSublayer(shipLayer)
// 飛船旋轉動畫
let animation = CABasicAnimation()
animation.keyPath = "transform.rotation"
animation.duration = 2.0
animation.byValue = CGFloat(M_PI * 2)
shipLayer.addAnimation(animation, forKey: nil)
}
}
}
這個方法效果十分好。使用transform.rotation
而非transform
的好處如下:
- 它允許我們在一步中不用關鍵幀旋轉多于180度。
- 它允許我們使用相對值而非絕對值旋轉(通過設置
byValue
而非toValue
)。 - 它允許我們用一個簡單的數字值指定角度而不用構建一個
CATransform3D
。 - 它不會與
transform.position
或transform.scale
沖突(這些也是使用關鍵路徑的獨立動畫)。
關于transform.rotation
屬性的奇怪事情是它并不是真實存在的。由于CATransform3D
并非對象,所以這個屬性不能存在;它是結構體
所以不能有類似KVC(鍵值碼)的屬性。transfrom.rotation
實際上是一個虛擬屬性,它是CALayer
提供的用來簡化動畫變形進程的。
你不能直接設置如同transform.rotation
或transform.scale
等屬性;它們只用于動畫。當你給這些屬性添加動畫時,Core Animation
通過使用一個叫CAValueFuntion
的類來自動更新你改變的必須的transform
屬性。
CAValueFuntion
被用于轉換我們賦值給虛擬的transform.rotation
屬性的簡單浮點數為真正用于移動圖層所需要的CATransform3D
矩陣值。你可以通過設置給定的CAPropertyAnimation
的valueFuntion
屬性改變值函數。你指定的函數會覆寫默認的。
CAValueFunction
看起來像是一個用于給不能自然相加或添加的屬性動畫(例如變形矩陣)的有用的機制,但因為CAValueFunction
的實現細節是私有的,現在并不能直接繼承它來創建一個新的值函數。你只能使用Apple早已提供的可用常量函數(現在全部關聯變形矩陣的虛擬屬性,因此有點少,因為默認的這些屬性的動作早已使用合適的值函數)。
動畫組
盡管CABasicAnimation
和CAKeyframeAnimation
只針對獨立的屬性,多個這種動畫可以用CAAnimationGroup
組合在一起。CAAnimationGroup
是另一個CAAnimation
的具體子類,它增加了一個animations
數組屬性,用來組合其它動畫。讓我們在表8.6中組合關鍵幀動畫和另一個改變圖層背景色的動畫(如表8.10)。表8.3展示了結果。
向圖層添加動畫組和單獨添加多個動畫并沒有本質上的區別,所以你現在可能還不是很清楚何時以及為什么使用這個類。它為集體設置動畫時長,或者通過一條指令添加或移除多個動畫提供了一些便利,但對于第9章講解的層次時間顯得并沒有什么用。
表8.10 將關鍵幀動畫和基本動畫組合起來
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var containerView: UIView!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 判斷橫屏
let screenSize = UIScreen.mainScreen().applicationFrame.size
if (screenSize.width > screenSize.height) {
// 創建路徑
let bezierPath = UIBezierPath()
bezierPath.moveToPoint(CGPointMake(0, 150))
bezierPath.addCurveToPoint(CGPointMake(300, 150), controlPoint1: CGPointMake(75, 0), controlPoint2: CGPointMake(225, 300))
// 使用CAShapeLayer繪制路徑
let pathLayer = CAShapeLayer()
pathLayer.path = bezierPath.CGPath
pathLayer.fillColor = UIColor.clearColor().CGColor
pathLayer.strokeColor = UIColor.redColor().CGColor
pathLayer.lineWidth = 3.0
self.containerView.layer.addSublayer(pathLayer)
// 添加有色圖層
let colorLayer = CALayer()
colorLayer.frame = CGRectMake(0, 0, 64, 64)
colorLayer.position = CGPointMake(0, 150)
colorLayer.backgroundColor = UIColor.greenColor().CGColor
self.containerView.layer.addSublayer(colorLayer)
// 創建位置動畫
let animation1 = CAKeyframeAnimation()
animation1.keyPath = "position"
animation1.path = bezierPath.CGPath
animation1.rotationMode = kCAAnimationRotateAuto
// 創建顏色動畫
let animation2 = CABasicAnimation()
animation2.keyPath = "backgroundColor"
animation2.toValue = UIColor.redColor().CGColor
// 創建組動畫
let groupAnimation = CAAnimationGroup()
groupAnimation.animations = [animation1, animation2]
groupAnimation.duration = 4.0
// 給顏色涂層添加這一動畫
colorLayer.addAnimation(groupAnimation, forKey: nil)
}
}
}
過渡
對于iOS應用來說,使用屬性動畫來改變布局是十分困難的。例如,你可能需要轉化某些文本或圖像,或一次性替換掉整個網格或表格。屬性動畫只作用于圖層的可動畫屬性,所以如果你需要改變一個不可動畫的屬性(例如圖像)或從層次中增刪圖層,屬性動畫就不起作用了。
這時過渡的作用就體現了。過渡動畫并不像屬性動畫一樣嘗試在兩個值之間平滑的插值;相反的是它被設計為一系列分治策略——用一個動畫掩飾內容改變。過渡影響整個圖層而非某一指定屬性。過渡會留一個舊圖層樣式的快照,然后一次將之動畫過渡到新的樣式。
我們使用CATransition
來創建過渡,它是CAAnimation
的另一個子類。它繼承了CAAnimation
除了時間函數外的一切,CATransition
有一個type
和一個subtype
用來指定過渡效果。type
屬性是一個NSString
,它可以被設置為下列常量之一:
kCATransitionFade
kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal
你現在被限制為這四個基本的CATransition
類型,但還有一些方法讓你可以實現額外的過渡效果,這將在本章后面講解。
默認的過渡類型是kCATransitionFade
,這會在你修改屬性或內容后創建先前圖層樣式和新樣式的平滑交替效果。
我們在第7章的自定義行為案例中曾使用過kCATransitionPush
類型;這會從側面滑入新的圖層樣式,從另一面推出舊的樣式。
kCATransitionMoveIn
和kCATransitionReveal
類似于kCATransitionPush
;它們都實現了一個方向滑動動畫,但有些許不同;kCATransitionMoveIn
從先前樣式的上移入新的圖層樣式,但并不像推過渡一樣推出舊樣式,kCATransitionReveal
移出舊樣式來露出新樣式而不是移入新樣式。
后三個標準類型有自然地內在指向的。默認情況下,它們從左側滑入,但你可以使用subtype
屬性控制它們的方向,它接受如下常量:
kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom
表8.11展示了一個使用CATransition
添加不可動畫屬性的動畫的簡單例子。在這我們改變了UIImage
的image
屬性,它正常并不能通過隱式動畫或CAPropertyAnimation
添加動畫,這是因為Core Animation
并不知道如何在圖像之間插值。通過對圖層應用交替漸變過渡,我們可以無視內容類型創建平滑的動畫改變(如圖8.4)。試試改變過渡的type
常量來看看其它可用的效果。
表8.11 使用CATransition給UIImageView添加動畫
import UIKit
import Foundation
class ViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
var images: NSArray!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 判斷橫屏
let screenSize = UIScreen.mainScreen().applicationFrame.size
if (screenSize.width > screenSize.height) {
// 設置圖像
self.images = NSArray(objects:
UIImage(named: "Snowman")!,
UIImage(named: "Cone")!,
UIImage(named: "Igloo")!
)
}
}
@IBAction func switchImage(sender: AnyObject) {
// 設置交替漸變過渡
let transition = CATransition()
transition.type = kCATransitionFade
// 給imageView的主圖層添加過渡
self.imageView.layer.addAnimation(transition, forKey: nil)
// 循環至下一圖像
let currentImage = self.imageView.image
var index = self.images.indexOfObject(currentImage!)
index = (index + 1) % self.images.count
self.imageView.image = self.images[index] as? UIImage
}
}
正如你從代碼中所見,過渡可以用和屬性或動畫組一樣使用-addAnimation:forKey:
的方式添加到圖層上。然而,不同于屬性動畫,同一時刻只有一個CATransition
用于一個給定圖層上。因此,無論你給鍵指定了什么值,過渡實際上都會附上一個transition
鍵,用常量kCATransition
表示。
隱式過渡
CATransition
可以平滑的實現圖層的任何改變,這使得在其它情況下難以添加動畫的屬性有了一個理想的候補方法。Apple當然意識了這點,CATransition
被用作設置CALayer contents
屬性的默認行為。這在有其它隱式動畫行為的視圖主圖層上是禁用的,但對于你自己創建的圖層,這意味著圖層contents
圖像的改變會自動添加交替漸變動畫。
在第7章我們使用CATranstion
作為圖層行為來給我們的圖層背景色添加動畫。backgroundColor
屬性可以用一個普通的CAPropertyAnimation
添加動畫,但這并不意味著你不能用一個CATransition
來代替。
圖層樹改變動畫
CATransition
并不對指定圖層屬性進行操作,這意味著你可以用它進行圖層改變的動畫而不需要明確知道什么改變了。例如,你可以在不知道哪些行被添加或移除的情況下,用一個交替漸變平滑的覆寫一個復雜的UITableView
的重載動畫,或者改變兩個不同的UIViewController
實例間的事務而無需知道任何有關它們內在視圖層次的信息。
這兩種情況都有別于目前我們所做過的其它例子,因為它們動畫執行不僅與圖層屬性改變有關而且和實際的圖層樹改變有關——我們需要在動畫過程中從圖層層次結構中確切地添加或移除圖層。
這一技巧是用于保證附加了CATransition
的圖層不會自己在事務期間從圖層樹中移除,因為隨后CATransition
會隨之一切移除。通常來說,你只需要將事務附加到受它們影響的圖層的父圖層上。
在表8.12中我們將展示如何在UITabBarController
的選項卡之間實現交替漸變事務。這里我們簡單地使用了默認的Tabbed Application
項目模板,使用UITabBarControllerDelegate
中的-tabBarController:didSelectViewController:
方法來施加動畫事務。我們將事務附加到UITabBarController
的視圖圖層上,因為它們在選項卡相互交換時不會被替換。
表8.12 給UITabBarController添加動畫
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UITabBarControllerDelegate {
var window: UIWindow?
var tabBarController: UITabBarController?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
let viewController1 = FirstViewController()
let viewController2 = SecondViewController()
self.tabBarController = UITabBarController()
self.tabBarController?.viewControllers = [viewController1, viewController2]
self.tabBarController?.delegate = self
self.window?.rootViewController = self.tabBarController
self.window?.makeKeyAndVisible()
return true
}
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
// 設置交替漸變事務
let transition = CATransition()
transition.type = kCATransitionFade
// 向選項卡控制器視圖添加事務
self.tabBarController?.view.layer.addAnimation(transition, forKey: nil)
}
}
自定義事務
我們說過事務是一個強大的方式來給那些難以平滑改變的屬性添加動畫的。但列舉CATransition
看起來有點限制。
更奇怪的是Apple通過
UIView +transitionFromView:duration:options:completion:和
+transitionWithView:duration:options:animations:方法來揭露Core Animation
事務,但可用選項與通過CATransition type
屬性可訪問的常量完全不同。可用于UIView
事務方法options
參數的指定常量如下:
UIViewAnimationOptionTransitionFlipFromLeft
UIViewAnimationOptionTransitionFlipFromRight
UIViewAnimationOptionTransitionCurlUp
UIViewAnimationOptionTransitionCurlDown
UIViewAnimationOptionTransitionCrossDissolve
UIViewAnimationOptionTransitionFlipFromTop
UIViewAnimationOptionTransitionFlipFromBottom
除了UIViewAnimationOptionTransitionCrossDissolve
,其它事務都不符合CATransition
類型。你可以修改我們之前的事務例子來測試這些可選事務(如表8.13)。
表8.13 使用UIKit方法的可選事務實現
import UIKit
import Foundation
class ViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
var images: NSArray!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 判斷橫屏
let screenSize = UIScreen.mainScreen().applicationFrame.size
if (screenSize.width > screenSize.height) {
// 設置圖像
self.images = NSArray(objects:
UIImage(named: "Snowman")!,
UIImage(named: "Cone")!,
UIImage(named: "Igloo")!
)
}
}
@IBAction func switchImage(sender: AnyObject) {
UIView.transitionWithView(self.imageView, duration: 1.0, options: UIViewAnimationOptions.TransitionFlipFromLeft, animations: {
// 循環至下一圖像
let currentImage = self.imageView.image
var index = self.images.indexOfObject(currentImage!)
index = (index + 1) % self.images.count
self.imageView.image = self.images[index] as? UIImage
}, completion: nil)
}
}
從iOS 5(從這時候開始引入了Core Image
框架)開始的某些文檔似乎在暗示,可能可以使用CIFilter
和CATransition
的filter
屬性的組合來創建額外的事務類型。然而,直到iOS 6,還是不可以用。嘗試對CATransition
使用Core Image
過濾器并沒有任何效果。(但在Mac OS上這個是支持的,這也導致了文檔的矛盾。)
因此,你不得不選擇使用CATransition
或者UIView
的事務方法,這取決于你想實現的效果。希望iOS將來的版本可以支持Core Image
事務過濾器,這樣可以通過CATransition
使得所有的Core Image
事務動畫可用(甚至可以創建新的動畫)。
然而,這并不意味著不可以在iOS中實現自定義的事務動畫效果。它只意味著你們需要多做一些工作。正如先前提及的,事務動畫的基本原則是你先取得當前圖層狀態的快照,然后在你改變場景之后的圖層時對這一快照添加動畫。如果我們知道如何對取得圖層的快照,我們可以使用正常的屬性動畫來實現動畫,這樣我們根本就不需要使用CATransiton
或者UIKit
的事務方法。
最終我們不難發現,獲得圖層快照會相對容易一些。CALayer
有一個叫-renderInContext:
的方法可以把當前內容繪入Core Graphics
上下文中,這樣就可以捕獲一張當前內容的圖像,這個圖像可以用來在另一個視圖中顯示。如果我們將這一快照視圖置于原始視圖之前,它會遮蓋住我們對真正視圖內容的所有改變,這允許我們重新實現一個簡單事務的效果。
表8.14演示了這一想法的基本實現:我們獲得當前視圖狀態的快照,然后改變原始視圖背景色的同時旋轉并漸隱快照。圖8.5展示了我們進行中的自定義事務。
為了讓一切簡單化,我們使用UIView
的-animateWithDuration:completion:
方法來實現動畫效果。盡管我們也可以使用CABasicAnimation
實現同樣的效果,但我們就不得不為圖層變形和透明度屬性設置分離的動畫,并且我們需要實現CAAnimationDelegate
在動畫完成后從屏幕中移除coverView
。
表8.14 使用renderInContext:創建自定義事務
import UIKit
class ViewController: UIViewController {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 判斷橫屏
let screenSize = UIScreen.mainScreen().applicationFrame.size
if (screenSize.width > screenSize.height) {
}
}
@IBAction func performTransition(sender: AnyObject) {
// 保持當前視圖快照
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, true, 0.0)
self.view.layer.renderInContext(UIGraphicsGetCurrentContext())
let coverImage = UIGraphicsGetImageFromCurrentImageContext()
// 將快照視圖插入到當前視圖前
let coverView = UIImageView(image: coverImage)
coverView.frame = self.view.bounds
self.view.addSubview(coverView)
// 更新視圖(我們將簡單地隨機圖層背景色)
let red: CGFloat = CGFloat(arc4random()) / CGFloat(INT_MAX)
let green: CGFloat = CGFloat(arc4random()) / CGFloat(INT_MAX)
let blue: CGFloat = CGFloat(arc4random()) / CGFloat(INT_MAX)
self.view.backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 1.0)
// 添加動畫效果(任何你喜歡的都行)
UIView.animateWithDuration(1.0, animations: {
// 縮放、旋轉并漸隱視圖
var transform = CGAffineTransformMakeScale(0.01, 0.01)
transform = CGAffineTransformRotate(transform, CGFloat(M_PI_2))
coverView.transform = transform
coverView.alpha = 0.0
}, completion: {
finished in
// 完成后移除覆蓋視圖
coverView.removeFromSuperview()
})
}
}
有一點需要注意:-renderInContext:
方法會捕獲圖層的主圖像和子圖層,但并不會正確處理這些子圖層所應用的變形,而且不會處理視頻或OpenGL內容。CATransition
并不受此限制,它大概用了一個私有方法來捕獲快照。
取消進行中的動畫
正如這章前面所說,你可以使用-addAnimation:forKey:
方法中的key
參數來將應用到圖層上的動畫撤回。你可以使用如下方法:
SWIFT
func animationForKey(_ key: String) -> CAAnimation?
OBJECTIVE-C
- (CAAnimation * nullable)animationForKey:(NSString * nonnull)key
系統并不支持修改進行中的動畫,所以這一參數的主要目的是用來檢查動畫屬性或者檢測圖層上是否有某一特定動畫。
要終止指定動畫,你可以使用如下方法將之從圖層上移除:
SWIFT
func removeAnimationForKey(_ key: String!)
OBJECTIVE-C
- (void)removeAnimationForKey:(NSString *)key
或者用這個方法移除所有動畫:
SWIFT
func removeAllAnimations()
OBJECTIVE-C
- (void)removeAllAnimations
一旦動畫被移除,圖層樣式會更新來匹配當前模型值。動畫會在結束后自動移除,除非你將動畫的removedOnCompletion
屬性設置為NO
。如果你設置動畫不自動移除,你要記得在不再需要它時手動移除它;否則,它會在圖層自身最終銷毀前一直存儲在內存中。
讓我們再次擴展旋轉飛船案例,添加按鈕來控制動畫的開始和結束。這一次,我們為我們的動畫鍵提供一個非nil
值,這樣我們就可以稍后移除它。-animationDidStop:finished:~方法中的
flag參數表示動畫是自然結束的還是被中止的,我們將在控制臺中輸出記錄。如何我們用結束按鈕中止動畫,控制臺會輸出
NO,但如果讓其完成動畫,會輸出
YES`。
看表8.15中更新后的例子代碼。圖8.6展示了結果。
表8.15 開始、結束動畫
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var containerView: UIView!
var shipLayer: CALayer!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// 判斷橫屏
let screenSize = UIScreen.mainScreen().applicationFrame.size
if (screenSize.width > screenSize.height) {
// 增加飛船
self.shipLayer = CALayer()
self.shipLayer.frame = CGRectMake(0, 0, 128, 128)
self.shipLayer.position = CGPointMake(150, 150)
// 譯者用之前的雪人圖像代替飛船,讀者理解方法就好
self.shipLayer.contents = UIImage(named: "Snowman")?.CGImage
self.containerView.layer.addSublayer(shipLayer)
}
}
@IBAction func start(sender: AnyObject) {
// 飛船旋轉動畫
let animation = CABasicAnimation()
animation.keyPath = "transform.rotation"
animation.duration = 2.0
animation.byValue = CGFloat(M_PI * 2)
animation.delegate = self
self.shipLayer.addAnimation(animation, forKey: "rotateAnimation")
}
@IBAction func stop(sender: AnyObject) {
self.shipLayer.removeAnimationForKey("rotateAnimation")
}
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
// 輸出動畫停止
NSLog("The animation stopped (finished: %@)", flag ? "YES": "NO");
}
}
譯者注:不難發現,原圖像仍然存在,譯者會在解決問題后再次更新此版本。
總結
這一章中,我們講解了屬性動畫(這允許你控制獨立圖層屬性動畫),動畫組(這允許將多個屬性動畫組合成單一單元),事務(影響整個圖層并可用于對圖層內容的任何改變添加動畫,包括子圖層的添加和移除)。
在第9章中,我們將學習CAMediaTiming
協議并解釋Core Animation
如何處理時間流逝。