若是想讓事情變得順利,只有靠本身 -- 夏爾·紀堯姆 html
上一章介紹了隱式動畫的概念。隱式動畫是在iOS平臺建立動態用戶界面的一種直接方式,也是UIKit動畫機制的基礎,不過它並不能涵蓋全部的動畫類型。在這一章中,咱們將要研究一下顯式動畫,它可以對一些屬性作指定的自定義動畫,或者建立非線性動畫,好比沿着任意一條曲線移動。 git
首先咱們來探討一下屬性動畫。屬性動畫做用於圖層的某個單一屬性,並指定了它的一個目標值,或者一連串將要作動畫的值。屬性動畫分爲兩種:基礎和關鍵幀。 github
動畫其實就是一段時間內發生的改變,最簡單的形式就是從一個值改變到另外一個值,這也是 CABasicAnimation 最主要的功能。 CABasicAnimation 是 CAPropertyAnimation 的一個子類,而 CAPropertyAnimation 的父類是 CAAnimation , CAAnimation 同時也是Core Animation全部動畫類型的抽象基類。做爲一個抽象類,CAAnimation自己並無作多少工做,它提供了一個計時函數(見第十章「緩衝」),一個委託(用於反饋動畫狀態)以及一個 removedOnCompletion ,用於標識動畫是否該在結束後自動釋放(默認YES,爲了防止內存泄露)。CAAnimation同時實現了一些協議,包括CAAction(容許CAAnimation的子類能夠提供圖層行爲),以及 CAMediaTiming (第九章「圖層時間」將會詳細解釋)。 安全
CAPropertyAnimation經過指定動畫的 keyPath 做用於一個單一屬性,CAAnimation一般應用於一個指定的CALayer,因而這裏指的也就是一個圖層的keyPath了。實際上它是一個關鍵路徑(一些用點表示法能夠在層級關係中指向任意嵌套的對象),而不只僅是一個屬性的名稱,由於這意味着動畫不只能夠做用於圖層自己的屬性,並且還包含了它的子成員的屬性,甚至是一些虛擬的屬性(後面會詳細解釋)。 app
CABasicAnimation繼承於CAPropertyAnimation,並添加了以下屬性: dom
id fromValue; id toValue; id byValue;
從命名就能夠獲得很好的解釋:fromValue表明了動畫開始以前屬性的值,toValue表明了動畫結束以後的值,byValue表明了動畫執行過程當中改變的值。 函數
經過組合這三個屬性就能夠有不少種方式來指定一個動畫的過程。它們被定義成id類型而不是一些具體的類型是由於屬性動畫能夠用做不少不一樣種的屬性類型,包括數字類型,矢量,變換矩陣,甚至是顏色或者圖片。 性能
id類型能夠包含任意由NSObject派生的對象,但有時候你會但願對一些不直接從NSObject繼承的屬性類型作動畫,這意味着你須要把這些值用一個對象來封裝,或者強轉成一個對象,就像某些功能和Objective-C對象相似的Core Foundation類型。可是如何從一個具體的數據類型轉換成id看起來並不明顯,一些普通的例子見表8.1。 測試
表8.1 用於CAPropertyAnimation的一些類型轉換 動畫
Type | Object Type | Code Example |
---|---|---|
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(fromValue + byValue)了。他們的用法在CABasicAnimation頭文件中已經描述的很清楚了,因此在這裏就不重複了。總的說來,就是隻須要指定toValue或者byValue,剩下的值均可以經過上下文自動計算出來。
舉個例子:咱們修改一下第七章中的顏色漸變的動畫,用顯式的CABasicAnimation來取代以前的隱式動畫,代碼見清單8.1。
清單8.1 經過CABasicAnimation來設置圖層背景色
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *layerView; @property (nonatomic, strong) IBOutlet CALayer *colorLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //create sublayer self.colorLayer = [CALayer layer]; self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); self.colorLayer.backgroundColor = [UIColor blueColor].CGColor; //add it to our view [self.layerView.layer addSublayer:self.colorLayer]; } - (IBAction)changeColor { //create a new random color CGFloat red = arc4random() / (CGFloat)INT_MAX; CGFloat green = arc4random() / (CGFloat)INT_MAX; CGFloat blue = arc4random() / (CGFloat)INT_MAX; UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0]; //create a basic animation CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"backgroundColor"; animation.toValue = (__bridge id)color.CGColor; //apply animation to layer [self.colorLayer addAnimation:animation forKey:nil]; } @end
運行程序,結果有點差強人意,點擊按鈕,的確可使圖層動畫過渡到一個新的顏色,然動畫結束以後又馬上變回原始值。
這是由於動畫並無改變圖層的模型,而只是呈現(第七章)。一旦動畫結束並從圖層上移除以後,圖層就馬上恢復到以前定義的外觀狀態。咱們從沒改變過backgroundColor屬性,因此圖層就返回到原始的顏色。
當以前在使用隱式動畫的時候,實際上它就是用例子中CABasicAnimation來實現的(回憶第七章,咱們在-actionForLayer:forKey:委託方法打印出來的結果就是CABasicAnimation)。可是在那個例子中,咱們經過設置屬性來打開動畫。在這裏咱們作了相同的動畫,可是並無設置任何屬性的值(這就是爲何會馬上變回初始狀態的緣由)。
把動畫設置成一個圖層的行爲(而後經過改變屬性值來啓動動畫)是到目前爲止同步屬性值和動畫狀態最簡單的方式了,假設因爲某些緣由咱們不能這麼作(一般由於UIView關聯的圖層不能這麼作動畫),那麼有兩種能夠更新屬性值的方式:在動畫開始以前或者動畫結束以後。
動畫以前改變屬性的值是最簡單的辦法,但這意味着咱們不能使用fromValue這麼好的特性了,並且要手動將fromValue設置成圖層當前的值。
因而在動畫建立以前插入以下代碼,就能夠解決問題了
animation.fromValue = (__bridge id)self.colorLayer.backgroundColor; self.colorLayer.backgroundColor = color.CGColor;
這的確是可行的,但仍是有些問題,若是這裏已經正在進行一段動畫,咱們須要從呈現圖層那裏去得到fromValue,而不是模型圖層。另外,因爲這裏的圖層並非UIView關聯的圖層,咱們須要用CATransaction來禁用隱式動畫行爲,不然默認的圖層行爲會干擾咱們的顯式動畫(實際上,顯式動畫一般會覆蓋隱式動畫,但在文章中並無提到,因此爲了安全最好這麼作)。
更新以後的代碼以下:
CALayer *layer = self.colorLayer.presentationLayer ?: self.colorLayer; animation.fromValue = (__bridge id)layer.backgroundColor; [CATransaction begin]; [CATransaction setDisableActions:YES]; self.colorLayer.backgroundColor = color.CGColor; [CATransaction commit];
若是給每一個動畫都添加這些,代碼會顯得特別臃腫。幸運的是,咱們能夠從CABasicAnimation去自動設置這些。因而能夠建立一個可複用的代碼。清單8.2修改了以前的示例,經過使用CABasicAnimation的一個函數來避免在每次動畫時候都重複那些臃腫的代碼。
清單8.2 修改動畫馬上恢復到原始狀態的一個可複用函數
- (void)applyBasicAnimation:(CABasicAnimation *)animation toLayer:(CALayer *)layer { //set the from value (using presentation layer if available) animation.fromValue = [layer.presentationLayer ?: layer valueForKeyPath:animation.keyPath]; //update the property in advance //note: this approach will only work if toValue != nil [CATransaction begin]; [CATransaction setDisableActions:YES]; [layer setValue:animation.toValue forKeyPath:animation.keyPath]; [CATransaction commit]; //apply animation to layer [layer addAnimation:animation forKey:nil]; } - (IBAction)changeColor { //create a new random color CGFloat red = arc4random() / (CGFloat)INT_MAX; CGFloat green = arc4random() / (CGFloat)INT_MAX; CGFloat blue = arc4random() / (CGFloat)INT_MAX; UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0]; //create a basic animation CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"backgroundColor"; animation.toValue = (__bridge id)color.CGColor; //apply animation without snap-back [self applyBasicAnimation:animation toLayer:self.colorLayer]; }
這種簡單的實現方式經過toValue而不是byValue來處理動畫,不過這已是朝更好的解決方案邁出一大步了。你能夠把它添加給CALayer做爲一個分類,以方便更好地使用。
解決看起來如此簡單的一個問題都着實麻煩,可是別的方案會更加複雜。若是不在動畫開始以前去更新目標屬性,那麼就只能在動畫徹底結束或者取消的時候更新它。這意味着咱們須要精準地在動畫結束以後,圖層返回到原始值以前更新屬性。那麼該如何找到這個點呢?
在第七章使用隱式動畫的時候,咱們能夠在CATransaction完成塊中檢測到動畫的完成。可是這種方式並不適用於顯式動畫,由於這裏的動畫和事務並沒太多關聯。
那麼爲了知道一個顯式動畫在什麼時候結束,咱們須要使用一個實現了CAAnimationDelegate協議的delegate。
CAAnimationDelegate在任何頭文件中都找不到,可是能夠在CAAnimation頭文件或者蘋果開發者文檔中找到相關函數。在這個例子中,咱們用-animationDidStop:finished:方法在動畫結束以後來更新圖層的backgroundColor。
當更新屬性的時候,咱們須要設置一個新的事務,而且禁用圖層行爲。不然動畫會發生兩次,一個是由於顯式的CABasicAnimation,另外一次是由於隱式動畫,具體實現見訂單8.3。
清單8.3 動畫完成以後修改圖層的背景色
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //create sublayer self.colorLayer = [CALayer layer]; self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); self.colorLayer.backgroundColor = [UIColor blueColor].CGColor; //add it to our view [self.layerView.layer addSublayer:self.colorLayer]; } - (IBAction)changeColor { //create a new random color CGFloat red = arc4random() / (CGFloat)INT_MAX; CGFloat green = arc4random() / (CGFloat)INT_MAX; CGFloat blue = arc4random() / (CGFloat)INT_MAX; UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0]; //create a basic animation CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"backgroundColor"; animation.toValue = (__bridge id)color.CGColor; animation.delegate = self; //apply animation to layer [self.colorLayer addAnimation:animation forKey:nil]; } - (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag { //set the backgroundColor property to match animation toValue [CATransaction begin]; [CATransaction setDisableActions:YES]; self.colorLayer.backgroundColor = (__bridge CGColorRef)anim.toValue; [CATransaction commit]; } @end
對CAAnimation而言,使用委託模式而不是一個完成塊會帶來一個問題,就是當你有多個動畫的時候,沒法在在回調方法中區分。在一個視圖控制器中建立動畫的時候,一般會用控制器自己做爲一個委託(如清單8.3所示),可是全部的動畫都會調用同一個回調方法,因此你就須要判斷究竟是那個圖層的調用。
考慮一下第三章的鬧鐘,「圖層幾何學」,咱們經過簡單地每秒更新指針的角度來實現一個鐘,但若是指針動態地轉向新的位置會更加真實。
咱們不能經過隱式動畫來實現由於這些指針都是UIView的實例,因此圖層的隱式動畫都被禁用了。咱們能夠簡單地經過UIView的動畫方法來實現。但若是想更好地控制動畫時間,使用顯式動畫會更好(更多內容見第十章)。使用CABasicAnimation來作動畫可能會更加複雜,由於咱們須要在 -animationDidStop:finished: 中檢測指針狀態(用於設置結束的位置)。
動畫自己會做爲一個參數傳入委託的方法,也許你會認爲能夠控制器中把動畫存儲爲一個屬性,而後在回調用比較,但實際上並不起做用,由於委託傳入的動畫參數是原始值的一個深拷貝,從而不是同一個值。
當使用 -addAnimation:forKey: 把動畫添加到圖層,這裏有一個到目前爲止咱們都設置爲nil的key參數。這裏的鍵是 -animationForKey: 方法找到對應動畫的惟一標識符,而當前動畫的全部鍵均可以用animationKeys獲取。若是咱們對每一個動畫都關聯一個惟一的鍵,就能夠對每一個圖層循環全部鍵,而後調用 -animationForKey: 來比對結果。儘管這不是一個優雅的實現。
幸運的是,還有一種更加簡單的方法。像全部的NSObject子類同樣,CAAnimation實現了KVC(鍵-值-編碼)協議,因而你能夠用 -setValue:forKey: 和 -valueForKey: 方法來存取屬性。可是CAAnimation有一個不一樣的性能:它更像一個NSDictionary,可讓你隨意設置鍵值對,即便和你使用的動畫類所聲明的屬性並不匹配。
這意味着你能夠對動畫用任意類型打標籤。在這裏,咱們給UIView類型的指針添加的動畫,因此能夠簡單地判斷動畫到底屬於哪一個視圖,而後在委託方法中用這個信息正確地更新鐘的指針(清單8.4)。
清單8.4 使用KVC對動畫打標籤
@interface ViewController () @property (nonatomic, weak) IBOutlet UIImageView *hourHand; @property (nonatomic, weak) IBOutlet UIImageView *minuteHand; @property (nonatomic, weak) IBOutlet UIImageView *secondHand; @property (nonatomic, weak) NSTimer *timer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //adjust anchor points self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); //start timer self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES]; //set initial hand positions [self updateHandsAnimated:NO]; } - (void)tick { [self updateHandsAnimated:YES]; } - (void)updateHandsAnimated:(BOOL)animated { //convert time to hours, minutes and seconds NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit; NSDateComponents *components = [calendar components:units fromDate:[NSDate date]]; CGFloat hourAngle = (components.hour / 12.0) * M_PI * 2.0; //calculate hour hand angle //calculate minute hand angle CGFloat minuteAngle = (components.minute / 60.0) * M_PI * 2.0; //calculate second hand angle CGFloat secondAngle = (components.second / 60.0) * M_PI * 2.0; //rotate hands [self setAngle:hourAngle forHand:self.hourHand animated:animated]; [self setAngle:minuteAngle forHand:self.minuteHand animated:animated]; [self setAngle:secondAngle forHand:self.secondHand animated:animated]; } - (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated { //generate transform CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1); if (animated) { //create transform animation CABasicAnimation *animation = [CABasicAnimation animation]; [self updateHandsAnimated:NO]; animation.keyPath = @"transform"; animation.toValue = [NSValue valueWithCATransform3D:transform]; animation.duration = 0.5; animation.delegate = self; [animation setValue:handView forKey:@"handView"]; [handView.layer addAnimation:animation forKey:nil]; } else { //set transform directly handView.layer.transform = transform; } } - (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag { //set final position for hand view UIView *handView = [anim valueForKey:@"handView"]; handView.layer.transform = [anim.toValue CATransform3DValue]; }
咱們成功的識別出每一個圖層中止動畫的時間,而後更新它的變換到一個新值,很好。
不幸的是,即便作了這些,仍是有個問題,清單8.4在模擬器上運行的很好,但當真正跑在iOS設備上時,咱們發如今-animationDidStop:finished:委託方法調用以前,指針會迅速返回到原始值,這個清單8.3圖層顏色發生的狀況同樣。
問題在於回調方法在動畫完成以前已經被調用了,但不能保證這發生在屬性動畫返回初始狀態以前。這同時也很好地說明了爲何要在真實的設備上測試動畫代碼,而不只僅是模擬器。
咱們能夠用一個fillMode屬性來解決這個問題,下一章會詳細說明,這裏知道在動畫以前設置它比在動畫結束以後更新屬性更加方便。