在 iOS 中,所有的 view 都是由一個底層的 layer 來驅動的。view 和它的 layer 之間有著緊密的聯(lián)系,view 其實直接從 layer 對象中獲取了絕大多數(shù)它所需要的數(shù)據(jù)。在 iOS 中也有一些單獨的 layer,比如 AVCaptureVideoPreviewLayer
和 CAShapeLayer
,它們不需要附加到 view 上就可以在屏幕上顯示內容。兩種情況下其實都是 layer 在起決定作用。當然了,附加到 view 上的 layer 和單獨的 layer 在行為上還是稍有不同的。
基本上你改變一個單獨的 layer 的任何屬性的時候,都會觸發(fā)一個從舊的值過渡到新值的簡單動畫(這就是所謂的可動畫 animatable
)。然而,如果你改變的是 view 中 layer 的同一個屬性,它只會從這一幀直接跳變到下一幀。盡管兩種情況中都有 layer,但是當 layer 附加在 view 上時,它的默認的隱式動畫的 layer 行為就不起作用了。
animatable;幾乎所有的層的屬性都是隱性可動畫的。你可以在文檔中看到它們的簡介是以 'animatable' 結尾的。這不僅包括了比如位置,尺寸,顏色或者透明度這樣的絕大多數(shù)的數(shù)值屬性,甚至也囊括了像 isHidden 和 doubleSided 這樣的布爾值。 像 paths 這樣的屬性也是 animatable 的,但是它不支持隱式動畫。
在 Core Animation 編程指南的 “How to Animate Layer-Backed Views” 中,對_為什么_會這樣做出了一個解釋:
UIView 默認情況下禁止了 layer 動畫,但是在 animation block 中又重新啟用了它們
這正是我們所看到的行為;當一個屬性在動畫 block 之外被改變時,沒有動畫,但是當屬性在動畫 block 內被改變時,就帶上了動畫。對于這是_如何_發(fā)生的這一問題的答案十分簡單和優(yōu)雅,它優(yōu)美地闡明和揭示了 view 和 layer 之間是如何協(xié)同工作和被精心設計的。
無論何時一個可動畫的 layer 屬性改變時,layer 都會尋找并運行合適的 'action' 來實行這個改變。在 Core Animation 的專業(yè)術語中就把這樣的動畫統(tǒng)稱為動作 (action,或者 CAAction
)。
CAAction:技術上來說,這是一個接口,并可以用來做各種事情。但是實際中,某種程度上你可以只把它理解為用來處理動畫。
layer 將像文檔中所寫的的那樣去尋找動作,整個過程分為五個步驟。第一步中的在 view 和 layer 中交互的部分是最有意思的:
layer 通過向它的 delegate 發(fā)送 actionForLayer:forKey:
消息來詢問提供一個對應屬性變化的 action。delegate 可以通過返回以下三者之一來進行響應:
nil
, 這樣 layer 就會到其他地方繼續(xù)尋找。NSNull
對象,告訴 layer 這里不需要執(zhí)行一個動作,搜索也會就此停止。而讓這一切變得有趣的是,當 layer 在背后支持一個 view 的時候,view 就是它的 delegate;
在 iOS 中,如果 layer 與一個 UIView 對象關聯(lián)時,這個屬性
必須
被設置為持有這個 layer 的那個 view。
理解這些之后,前一分鐘解釋起來還復雜無比的現(xiàn)象瞬間就易如反掌了:屬性改變時 layer 會向 view 請求一個動作,而一般情況下 view 將返回一個 NSNull
,只有當屬性改變發(fā)生在動畫 block 中時,view 才會返回實際的動作。哈,但是請別輕信我的這些話,你可以非常容易地驗證到底是不是這樣。只要對一個一般來說可以動畫的 layer 屬性向 view 詢問動作就可以了,比如對于 'position':
NSLog(@"outside animation block: %@",
[myView actionForLayer:myView.layer forKey:@"position"]);
[UIView animateWithDuration:0.3 animations:^{
NSLog(@"inside animation block: %@",
[myView actionForLayer:myView.layer forKey:@"position"]);
}];
運行上面的代碼,可以看到在 block 外 view 返回的是 NSNull 對象,而在 block 中時返回的是一個 CABasicAnimation。很優(yōu)雅,對吧?值得注意的是打印出的 NSNull 是帶著一對尖括號的 ("<null>
"),這和其他對象一樣,而打印 nil 的時候我們得到的是普通括號((null)
):
outside animation block: <null>
inside animation block: <CABasicAnimation: 0x8c2ff10>
對于 view 中的 layer 來說,對動作的搜索只會到第一步為止(至少我沒有見過 view 返回一個 nil
然后導致繼續(xù)搜索動作的情況)。對于單獨的 layer 來說,剩余的四個步驟可以在 CALayer 的 actionForKey:
文檔中找到。
我很確定我們都會同意 UIView 動畫是一組非常優(yōu)秀的 API,它簡潔明確。實際上,它使用了 Core Animation 來執(zhí)行動畫,這給了我們一個絕佳的機會來深入研究 UIKit 是如何使用 Core Animation 的。在這里甚至還有很多非常棒的實踐和技巧可以讓我們借鑒。:)
當屬性在動畫 block 中改變時,view 將向 layer 返回一個基本的動畫,然后動畫通過通常的 addAnimation:forKey:
方法被添加到 layer 中,就像顯式地添加動畫那樣。再一次,別直接信我,讓我們實踐檢驗一下。
歸功于 UIView 的 +layerClass
類方法,view 和 layer 之間的交互很容易被觀測到。通過這個方法我們可以在為 view 創(chuàng)建 layer 時為其指定要使用的類。通過子類一個 UIView,以及用這個方法返回一個自定義的 layer 類,我們就可以重寫 layer 子類中的 addAnimation:forKey:
并輸出一些東西來驗證它是否確實被調用。唯一要記住的是我們需要調用 super 方法,不然的話我們就把要觀測的行為完全改變了:
@interface DRInspectionLayer : CALayer
@end
@implementation DRInspectionLayer
- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key
{
NSLog(@"adding animation: %@", [anim debugDescription]);
[super addAnimation:anim forKey:key];
}
@end
@interface DRInspectionView : UIView
@end
@implementation DRInspectionView
+ (Class)layerClass
{
return [DRInspectionLayer class];
}
@end
通過輸出動畫的 debug 信息,我們不僅可以驗證它確實如預期一樣被調用了,還可以看到動畫是如何組織構建的:
<CABasicAnimation:0x8c73680;
delegate = <UIViewAnimationState: 0x8e91fa0>;
fillMode = both;
timingFunction = easeInEaseOut;
duration = 0.3;
fromValue = NSPoint: {5, 5};
keyPath = position
>
當動畫剛被添加到 layer 時,屬性的新值還沒有被改變。在構建動畫時,只有 fromValue
(也就是當前值) 被顯式地指定了。CABasicAnimation 的文檔向我們簡單介紹了這么做對于動畫的插值來說的的行為應該是:
只有
fromValue
不是nil
時,在fromValue
和屬性當前顯示層的值之間進行插值。
這也是我在處理顯式動畫時選擇的做法,將一個屬性改變?yōu)樾碌闹担缓髮赢媽ο筇砑拥?layer 上:
CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"];
fadeIn.duration = 0.75;
fadeIn.fromValue = @0;
myLayer.opacity = 1.0; // 更改 model 的值 ...
// ... 然后添加動畫對象
[myLayer addAnimation:fadeIn forKey:@"fade in slowly"];
這很簡潔,你也不需要在動畫被移除的時候做什么額外操作。如果動畫是在一段延遲后才開始的話,你可以使用 backward 填充模式 (或者 'both' 填充模式),就像 UIKit 所創(chuàng)建的動畫那樣。
可能你看見上面輸出中的動畫的 delegate 了,想知道這個類是用來做什么的嗎?我們可以來看看 dump 出來的頭文件,它主要用來維護動畫的一些狀態(tài) (持續(xù)時間,延時,重復次數(shù)等等)。它還負責對一個棧做 push 和 pop,這是為了在多個動畫 block 嵌套時能夠獲取正確的動畫狀態(tài)。這些都是些實現(xiàn)細節(jié),除非你想要寫一套自己的基于 block 的動畫 API,否則可能你不會用到它們 (實際上這是一個很有趣的點子)。
然后真正_有意思_的是這個 delegate 實現(xiàn)了 animationDidStart:
和 animationDidStop:finished:
,并將信息傳給了它自己的 delegate。
編者注 這里不太容易理解,加以說明:從上面的頭文件中可以看出,作為 CAAnimation 的 delegate 的私有類 `UIViewAnimationState` 中還有一個 `_delegate` 成員,并且 `animationDidStart:` 和 `animationDidStop:finished:` 也是典型的 delegate 的實現(xiàn)方法。
通過打印這個 delegate 的 delegate,我們可以發(fā)現(xiàn)它也是一個私有類:UIViewAnimationBlockDelegate。同樣進行 class dump 得到它的頭文件,這是一個很小的類,只負責一件事情:響應動畫的 delegate 回調并且執(zhí)行相應的 block。如果我們使用自己的 Core Animation 代碼,并且選擇 block 而不是 delegate 做回調的話,添加這個是很容易的:
@interface DRAnimationBlockDelegate : NSObject
@property (copy) void(^start)(void);
@property (copy) void(^stop)(BOOL);
+(instancetype)animationDelegateWithBeginning:(void(^)(void))beginning
completion:(void(^)(BOOL finished))completion;
@end
@implementation DRAnimationBlockDelegate
+ (instancetype)animationDelegateWithBeginning:(void (^)(void))beginning
completion:(void (^)(BOOL))completion
{
DRAnimationBlockDelegate *result = [DRAnimationBlockDelegate new];
result.start = beginning;
result.stop = completion;
return result;
}
- (void)animationDidStart:(CAAnimation *)anim
{
if (self.start) {
self.start();
}
self.start = nil;
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
if (self.stop) {
self.stop(flag);
}
self.stop = nil;
}
@end
雖然是我個人的喜好,但是我覺得像這樣的基于 block 的回調風格可能會比實現(xiàn)一個 delegate 回調更適合你的代碼:
fadeIn.delegate = [DRAnimationBlockDelegate animationDelegateWithBeginning:^{
NSLog(@"beginning to fade in");
} completion:^(BOOL finished) {
NSLog(@"did fade %@", finished ? @"to the end" : @"but was cancelled");
}];
一旦你知道了 actionForKey:
的機理之后,UIView 就遠沒有它一開始看起來那么神秘了。實際上我們完全可以按照我們的需求量身定制地寫出一套自己的基于 block 的動畫 APIs。我所設計的動畫將通過在 block 中用一個很激進的時間曲線來做動畫,以吸引用戶對該 view 的注意,之后做一個緩慢的動畫回到原始狀態(tài)。你可以把它看作一種類似 pop (請不要和 Facebook 最新的 Pop 框架弄混了)的行為。與一般使用 UIViewAnimationOptionAutoreverse
的動畫 block 不同,因為動畫設計和概念上的需要,我自己實現(xiàn)了將 model 值改變回原始值的過程。自定義的動畫 API 的使用方法就像這樣:
[UIView DR_popAnimationWithDuration:0.7
animations:^{
myView.transform = CGAffineTransformMakeRotation(M_PI_2);
}];
當我們完成后,效果是這個樣子的 (對四個不同的 view 為位置,尺寸,顏色和旋轉進行動畫):
http://wiki.jikexueyuan.com/project/objc/images/12-23.gif" alt="" />
要開始實現(xiàn)它,我們首先要做的是當一個 layer 屬性變化時獲取 delegate 的回調。因為我們無法事先預測 layer 要改變什么,所以我選擇在一個 UIView 的 category 中 swizzle actionForLayer:forKey:
方法:
@implementation UIView (DR_CustomBlockAnimations)
+ (void)load
{
SEL originalSelector = @selector(actionForLayer:forKey:);
SEL extendedSelector = @selector(DR_actionForLayer:forKey:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method extendedMethod = class_getInstanceMethod(self, extendedSelector);
NSAssert(originalMethod, @"original method should exist");
NSAssert(extendedMethod, @"exchanged method should exist");
if(class_addMethod(self, originalSelector, method_getImplementation(extendedMethod), method_getTypeEncoding(extendedMethod))) {
class_replaceMethod(self, extendedSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, extendedMethod);
}
}
為了保證我們不破壞其他依賴于 actionForLayer:forKey:
回調的代碼,我們使用一個靜態(tài)變量來判斷現(xiàn)在是不是處于我們自己定義的上下文中。對于這個例子來說一個簡單的 BOOL
其實就夠了,但是如果我們之后要寫更多內容的話,上下文的話就要靈活得多了:
static void *DR_currentAnimationContext = NULL;
static void *DR_popAnimationContext = &DR_popAnimationContext;
- (id<CAAction>)DR_actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
if (DR_currentAnimationContext == DR_popAnimationContext) {
// 這里寫我們自定義的代碼...
}
// 調用原始方法
return [self DR_actionForLayer:layer forKey:event]; // 沒錯,你沒看錯。因為它們已經被交換了
}
在我們的實現(xiàn)中,我們要確保在執(zhí)行動畫 block 之前設置動畫的上下文,并且在執(zhí)行后恢復上下文:
+ (void)DR_popAnimationWithDuration:(NSTimeInterval)duration
animations:(void (^)(void))animations
{
DR_currentAnimationContext = DR_popAnimationContext;
// 執(zhí)行動畫 (它將觸發(fā)交換后的 delegate 方法)
animations();
/* 一會兒再添加 */
DR_currentAnimationContext = NULL;
}
如果我們想要做的不過是添加一個從舊的值向新的值過度的動畫的話,我們可以直接在 delegate 的回調中來做。然而因為我們想要更精確地控制動畫,我們需要用一個幀動畫來實現(xiàn)。幀動畫需要所有的值都是已知的,而對我們的情況來說,新的值還沒有被設定,因此我們也就無從知曉。
有意思的是,iOS 添加的一個基于 block 的動畫 API 也遇到了同樣的問題。使用和上面一樣的觀察手段,我們就能知道它是如何繞開這個麻煩的。對于每個關鍵幀,在屬性變化時,view 返回 nil
,但是卻存儲下需要的狀態(tài)。這樣就能在所有關鍵幀 block 執(zhí)行后創(chuàng)建一個 CAKeyframeAnimationz
對象。
受到這種方法的啟發(fā),我們可以創(chuàng)建一個小的類來存儲我們創(chuàng)建動畫時所需要的信息:什么 layer 被更改了,什么 key path 的值被改變了,以及原來的值是什么:
@interface DRSavedPopAnimationState : NSObject
@property (strong) CALayer *layer;
@property (copy) NSString *keyPath;
@property (strong) id oldValue;
+ (instancetype)savedStateWithLayer:(CALayer *)layer
keyPath:(NSString *)keyPath;
@end
@implementation DRSavedPopAnimationState
+ (instancetype)savedStateWithLayer:(CALayer *)layer
keyPath:(NSString *)keyPath
{
DRSavedPopAnimationState *savedState = [DRSavedPopAnimationState new];
savedState.layer = layer;
savedState.keyPath = keyPath;
savedState.oldValue = [layer valueForKeyPath:keyPath];
return savedState;
}
@end
接下來,在我們的交換后的 delegate 回調中,我們簡單地將被變更的屬性的狀態(tài)存入一個靜態(tài)可變數(shù)組中:
if (DR_currentAnimationContext == DR_popAnimationContext) {
[[UIView DR_savedPopAnimationStates] addObject:[DRSavedPopAnimationState savedStateWithLayer:layer
keyPath:event]];
// 沒有隱式的動畫 (稍后添加)
return (id<CAAction>)[NSNull null];
}
在動畫 block 執(zhí)行完畢后,所有的屬性都被變更了,它們的狀態(tài)也被保存了?,F(xiàn)在,創(chuàng)建關鍵幀動畫:
+ (void)DR_popAnimationWithDuration:(NSTimeInterval)duration
animations:(void (^)(void))animations
{
DR_currentAnimationContext = DR_popAnimationContext;
// 執(zhí)行動畫 (它將觸發(fā)交換后的 delegate 方法)
animations();
[[self DR_savedPopAnimationStates] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
DRSavedPopAnimationState *savedState = (DRSavedPopAnimationState *)obj;
CALayer *layer = savedState.layer;
NSString *keyPath = savedState.keyPath;
id oldValue = savedState.oldValue;
id newValue = [layer valueForKeyPath:keyPath];
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:keyPath];
CGFloat easing = 0.2;
CAMediaTimingFunction *easeIn = [CAMediaTimingFunction functionWithControlPoints:1.0 :0.0 :(1.0-easing) :1.0];
CAMediaTimingFunction *easeOut = [CAMediaTimingFunction functionWithControlPoints:easing :0.0 :0.0 :1.0];
anim.duration = duration;
anim.keyTimes = @[@0, @(0.35), @1];
anim.values = @[oldValue, newValue, oldValue];
anim.timingFunctions = @[easeIn, easeOut];
// 不帶動畫地返回原來的值
[CATransaction begin];
[CATransaction setDisableActions:YES];
[layer setValue:oldValue forKeyPath:keyPath];
[CATransaction commit];
// 添加 "pop" 動畫
[layer addAnimation:anim forKey:keyPath];
}];
// 掃除工作 (移除所有存儲的狀態(tài))
[[self DR_savedPopAnimationStates] removeAllObjects];
DR_currentAnimationContext = nil;
}
注意老的 model 值被設到了 layer 上,所以在當動畫結束和移除后,model 的值和 presentation 的值是相符合的。
創(chuàng)建像這樣的你自己的 API 不會對每種情況都很適合,但是如果你需要在你的應用中的很多地方都做同樣的動畫的話,這可以幫助你寫出整潔的代碼,并減少重復。就算你之后從來不會使用這種方法,實際做一遍也能幫助你搞懂 UIView block 動畫的 APIs,特別是你已經在 Core Animation 的舒適區(qū)的時候,這非常有助于你的提高。
UIImageView 動畫是一個完全不同的更高層次的動畫 API 的實現(xiàn)方式,我會把它留給你來探索。表面上,它只不過是重新組裝了一個傳統(tǒng)的動畫 API。你所要做的事情就是指定一個圖片數(shù)組和一段時間,然后告訴 image view 開始動畫。在抽象背后,其實是一個添加在 image view 的 layer 上的 contents 屬性的離散的關鍵幀動畫:
<CAKeyframeAnimation:0x8e5b020;
removedOnCompletion = 0;
delegate = <_UIImageViewExtendedStorage: 0x8e49230>;
duration = 2.5;
repeatCount = 2.14748e+09;
calculationMode = discrete;
values = (
"<CGImage 0x8d6ce80>",
"<CGImage 0x8d6d2d0>",
"<CGImage 0x8d5cd30>"
);
keyPath = contents
>
動畫 APIs 可以以很多不同形式出現(xiàn),而對于你自己寫的動畫 API 來說,也是這樣的。