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