II.8 顯式動畫

如何你想某事正確,自己動手做吧。——Charles-Guillaume étienne

前一章介紹了隱式動畫的概念。隱式動畫是iOS上創建用戶界面的一種直接的方式,它們是UIKit自身動畫機制的基礎,但它們并不是一個完整的通用的動畫方案。這一章,我們將講解顯式動畫,這使我們得以對特殊屬性指定自定義動畫或者創建非線性動畫,如沿弧移動。

屬性動畫

我們將講解的第一種顯式動畫類型是屬性動畫。屬性動畫針對于圖層的一個單一屬性,并指定動畫屬性的目標值的區間。屬性動畫分為兩類:基礎關鍵幀

基礎動畫

基礎動畫是隨時間發生的,是值改變的最簡單的方法,它是CABasicAnimation設計的模型。

CABasicAnimationCAPropertyAnimation這一抽象類的具體子類,它是CAAnimation的子類,而CAAnimationCore Animation提供的所有動畫類型的抽象基類。身為抽象類,CAAnimation自身并不會明確實現功能。它提供了一個時間函數(正如第10章“緩動”所講解的),一個委托(用于獲得動畫狀態反饋),以及一個removedOnCompletion標志用于指示是否在結束后自動釋放(這默認為YES,這會防止內存溢出)。CAAnimation也實現包括CAAction(允許任何CAAnimation子類被作為圖層動作支持)以及CAMediaTiming(將在第9章“圖層時間”中細說)等一系列協議。

CAPropertyAnimation作用于單一屬性,由動畫的keyPath值指定。CAAnimation通常用于某一CALayer,同樣keyPath是與該圖層相關的。事實上這是一個關鍵路徑(一系列由點限定的鍵,它們指向一個有層次的直接結構對象)而并非僅是一個屬性名,keyPath十分有趣,它意味著動畫不僅可以應用于圖層自身,還可以應用于成員對象的屬性,甚至虛擬屬性(稍后細說)。

CABasicAnimationCAPropertyAnimation擴展了三個額外的屬性:

  • 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;

fromValuetoValue,和byValue屬性可以被用于不同的組合,但你不能同時指定三個值,這樣會引起沖突。例如,如果你指定fromValue2toValue4,而byValue為3,Core Animation并不知道最終值應該為4(由toValue指定)還是5formValue+byValue)。關于這些屬性值到底如何使用在CABasicAnimation的頭文件中有良好的文檔,所以在這我們并不重復它們。通常,你只需要指定toValuebyValue;其它值會根據上下文推斷出來。

讓我們試一下:我們將會修改第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:來向我們的圖層添加動畫時,有一個我們至今總設為nilkey參數。這個鍵(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有一個可選方法來指定動畫,就是使用CGPathpath屬性允許你用一種自然的方式定義運行序列,通過使用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)
        }
    }

}
圖8.1 沿貝塞爾曲線移動的圖像圖層

如果你運行這一案例,你會注意到動畫看起來有一點不真實,因為它移動時總指向同一個方向而不是隨曲線切線改變。你可以調整它的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)
    }
}
圖8.2 匹配曲線切線旋轉的圖層

虛擬屬性

我們先前說過事實上屬性動畫作用于關鍵路徑而非,這意味我們可以對子屬性甚至虛擬屬性添加動畫。但什么是虛擬屬性?

想象一個旋轉的動畫:如果我們想要添加一個旋轉對象動畫,我們不得不使用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.positiontransform.scale沖突(這些也是使用關鍵路徑的獨立動畫)。

關于transform.rotation屬性的奇怪事情是它并不是真實存在的。由于CATransform3D并非對象,所以這個屬性不能存在;它是結構體所以不能有類似KVC(鍵值碼)的屬性。transfrom.rotation實際上是一個虛擬屬性,它是CALayer提供的用來簡化動畫變形進程的。

你不能直接設置如同transform.rotationtransform.scale等屬性;它們只用于動畫。當你給這些屬性添加動畫時,Core Animation通過使用一個叫CAValueFuntion的類來自動更新你改變的必須的transform屬性。

CAValueFuntion被用于轉換我們賦值給虛擬的transform.rotation屬性的簡單浮點數為真正用于移動圖層所需要的CATransform3D矩陣值。你可以通過設置給定的CAPropertyAnimationvalueFuntion屬性改變值函數。你指定的函數會覆寫默認的。

CAValueFunction看起來像是一個用于給不能自然相加或添加的屬性動畫(例如變形矩陣)的有用的機制,但因為CAValueFunction的實現細節是私有的,現在并不能直接繼承它來創建一個新的值函數。你只能使用Apple早已提供的可用常量函數(現在全部關聯變形矩陣的虛擬屬性,因此有點少,因為默認的這些屬性的動作早已使用合適的值函數)。

動畫組

盡管CABasicAnimationCAKeyframeAnimation只針對獨立的屬性,多個這種動畫可以用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)
        }
    }
}
圖8.3 一個關鍵幀路徑和基本顏色屬性動畫組

過渡

對于iOS應用來說,使用屬性動畫來改變布局是十分困難的。例如,你可能需要轉化某些文本或圖像,或一次性替換掉整個網格或表格。屬性動畫只作用于圖層的可動畫屬性,所以如果你需要改變一個不可動畫的屬性(例如圖像)或從層次中增刪圖層,屬性動畫就不起作用了。

這時過渡的作用就體現了。過渡動畫并不像屬性動畫一樣嘗試在兩個值之間平滑的插值;相反的是它被設計為一系列分治策略——用一個動畫掩飾內容改變。過渡影響整個圖層而非某一指定屬性。過渡會留一個舊圖層樣式的快照,然后一次將之動畫過渡到新的樣式。

我們使用CATransition來創建過渡,它是CAAnimation的另一個子類。它繼承了CAAnimation除了時間函數外的一切,CATransition有一個type和一個subtype用來指定過渡效果。type屬性是一個NSString,它可以被設置為下列常量之一:

kCATransitionFade
kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal

你現在被限制為這四個基本的CATransition類型,但還有一些方法讓你可以實現額外的過渡效果,這將在本章后面講解。

默認的過渡類型是kCATransitionFade,這會在你修改屬性或內容后創建先前圖層樣式和新樣式的平滑交替效果。

我們在第7章的自定義行為案例中曾使用過kCATransitionPush類型;這會從側面滑入新的圖層樣式,從另一面推出舊的樣式。

kCATransitionMoveInkCATransitionReveal類似于kCATransitionPush;它們都實現了一個方向滑動動畫,但有些許不同;kCATransitionMoveIn從先前樣式的上移入新的圖層樣式,但并不像推過渡一樣推出舊樣式,kCATransitionReveal移出舊樣式來露出新樣式而不是移入新樣式。

后三個標準類型有自然地內在指向的。默認情況下,它們從左側滑入,但你可以使用subtype屬性控制它們的方向,它接受如下常量:

kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom

表8.11展示了一個使用CATransition添加不可動畫屬性的動畫的簡單例子。在這我們改變了UIImageimage屬性,它正常并不能通過隱式動畫或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表示。

圖8.4 使用`CATransition`在圖像中平滑過渡

隱式過渡

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框架)開始的某些文檔似乎在暗示,可能可以使用CIFilterCATransitionfilter屬性的組合來創建額外的事務類型。然而,直到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()
        })
    }
    
}
圖8.5 使用renderInContext:實現自定義事務

有一點需要注意:-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");
    }
    
}
圖8.6 用開始和結束按鈕控制的旋轉動畫

譯者注:不難發現,原圖像仍然存在,譯者會在解決問題后再次更新此版本。

總結

這一章中,我們講解了屬性動畫(這允許你控制獨立圖層屬性動畫),動畫組(這允許將多個屬性動畫組合成單一單元),事務(影響整個圖層并可用于對圖層內容的任何改變添加動畫,包括子圖層的添加和移除)。

在第9章中,我們將學習CAMediaTiming協議并解釋Core Animation如何處理時間流逝。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內容