有了前兩篇的概念基礎(chǔ),本篇從以下兩點(diǎn)結(jié)合具體代碼探索下CoreAnimation的一些原理。
UIView動(dòng)畫實(shí)現(xiàn)原理
展示層(presentationLayer)和模型層(modelLayer)
1.UIView動(dòng)畫實(shí)現(xiàn)原理
UIView提供了一系列UIViewAnimationWithBlocks,我們只需要把改變可動(dòng)畫屬性的代碼放在animations的block中即可實(shí)現(xiàn)動(dòng)畫效果,比如:
- (void)btnClick:(id)sender
{
[UIView animateWithDuration:1 animations:^(void){
if (_testView.bounds.size.width > 150)
{
_testView.bounds = CGRectMake(0, 0, 100, 100);
}
else
{
_testView.bounds = CGRectMake(0, 0, 200, 200);
}
} completion:^(BOOL finished){
NSLog(@"%d",finished);
}];
}
效果如下:

上一篇說過,UIView對象持有一個(gè)CALayer,真正來做動(dòng)畫的是這個(gè)layer,UIView只是對它做了一層封裝,可以通過一個(gè)簡單的實(shí)驗(yàn)驗(yàn)證一下:我們寫一個(gè)MyTestLayer類繼承CALayer,并重寫它的set方法;再寫一個(gè)MyTestView類繼承UIView,重寫它的layerClass方法指定圖層類為MyTestLayer:
@interface MyTestLayer : CALayer
@end
@implementation MyTestLayer
- (void)setBounds:(CGRect)bounds
{
NSLog(@"----layer setBounds");
[super setBounds:bounds];
NSLog(@"----layer setBounds end");
}
...
@end
@interface MyTestView : UIView
- (void)setBounds:(CGRect)bounds
{
NSLog(@"----view setBounds");
[super setBounds:bounds];
NSLog(@"----view setBounds end");
}
...
+(Class)layerClass
{
return [MyTestLayer class];
}
@end
當(dāng)我們給view設(shè)置bounds時(shí),getter、setter的調(diào)用順序是這樣的:

也就是說,在view的setBounds方法中,會(huì)調(diào)用layer的setBounds;同樣view的getBounds也會(huì)調(diào)用layer的getBounds。其他屬性也會(huì)得到相同的結(jié)論。那么動(dòng)畫又是怎么產(chǎn)生的呢?當(dāng)我們layer的屬性發(fā)生變化時(shí),會(huì)調(diào)用代理方法actionForLayer: forKey: 來獲得這次屬性變化的動(dòng)畫方案,而view就是它所持有的layer的代理:
@interface CALayer : NSObject <NSCoding, CAMediaTiming>
...
@property(nullable, weak) id <CALayerDelegate> delegate;
...
@end
@protocol CALayerDelegate <NSObject>
@optional
...
/* If defined, called by the default implementation of the
* -actionForKey: method. Should return an object implementating the
* CAAction protocol. May return 'nil' if the delegate doesn't specify
* a behavior for the current event. Returning the null object (i.e.
* '[NSNull null]') explicitly forces no further search. (I.e. the
* +defaultActionForKey: method will not be called.) */
- (nullable id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event;
...
@end
注釋中說明,該方法返回一個(gè)實(shí)現(xiàn)了CAAction的對象,通常是一個(gè)動(dòng)畫對象;當(dāng)返回nil時(shí)執(zhí)行默認(rèn)的隱式動(dòng)畫,返回null時(shí)不執(zhí)行動(dòng)畫。還是上面那個(gè)改變bounds的動(dòng)畫,我們在MyTestView中重寫actionForLayer:方法
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
id<CAAction> action = [super actionForLayer:layer forKey:event];
return action;
}
觀察它的返回值:

是一個(gè)內(nèi)部使用的_UIViewAddtiveAnimationAction對象,其中包含一個(gè)CABassicAnimation,默認(rèn)fillMode為both,默認(rèn)時(shí)間函數(shù)為淡入淡出,只包含fromValue(即動(dòng)畫之前的值,會(huì)在這個(gè)值和當(dāng)前值(block中修改過后的值)之間做動(dòng)畫)。我們可以嘗試在重寫的這個(gè)方法中強(qiáng)制返回nil,會(huì)發(fā)現(xiàn)我們不寫任何動(dòng)畫的代碼直接改變屬性也將產(chǎn)生一個(gè)默認(rèn)0.25s的隱式動(dòng)畫,這和上面的注釋描述是一致的。
如果兩個(gè)動(dòng)畫重疊在一起會(huì)是什么效果呢?
還是最開始的例子,我們添加兩個(gè)相同的UIView動(dòng)畫,一個(gè)時(shí)間為3s,一個(gè)時(shí)間為1s,并打印finished的值和兩個(gè)動(dòng)畫的持續(xù)時(shí)間。先執(zhí)行3s的動(dòng)畫,當(dāng)它還沒有結(jié)束時(shí)加上一個(gè)1s的動(dòng)畫,可以先看下實(shí)際效果:


很明顯,兩個(gè)動(dòng)畫的finished都為true且時(shí)間也是我們設(shè)置好的3s和1s。也就是說第二個(gè)動(dòng)畫并不會(huì)打斷第一個(gè)動(dòng)畫的執(zhí)行,而是將動(dòng)畫進(jìn)行了疊加。我們先來觀察一下運(yùn)行效果:
最開始方塊的bounds為(100,100),點(diǎn)擊執(zhí)行3s動(dòng)畫,bounds變?yōu)?200,200),并開始展示變大的動(dòng)畫;
-
動(dòng)畫過程中(假設(shè)到了(120,120)),點(diǎn)擊1s動(dòng)畫,由于這時(shí)真實(shí)bounds已經(jīng)是(200,200)了,所以bounds將變回100,并產(chǎn)生一個(gè)fromValue為(200,200)的動(dòng)畫。
但此時(shí)方塊并沒有從200開始,而是馬上開始變小,并明顯變到一個(gè)比100更小的值。
1s動(dòng)畫結(jié)束,finished為1,耗時(shí)1s。此時(shí)屏幕上的方塊是一個(gè)比100還要小的狀態(tài),又緩緩變回到100—3s動(dòng)畫結(jié)束,finished為1,耗時(shí)3s,方塊最終停在(100,100)的大小。
從這個(gè)現(xiàn)象我們可以猜想U(xiǎn)IView動(dòng)畫的疊加方式:當(dāng)我們通過改變View屬性實(shí)現(xiàn)動(dòng)畫時(shí),這個(gè)屬性的值是會(huì)立即改變的,動(dòng)畫只是展示出來的效果。當(dāng)動(dòng)畫還未結(jié)束時(shí)如果對同個(gè)屬性又加上另一個(gè)動(dòng)畫,兩個(gè)動(dòng)畫會(huì)從當(dāng)前展示的狀態(tài)開始進(jìn)行疊加,并最終停在view的真實(shí)位置。
舉個(gè)通俗點(diǎn)的例子,我們8點(diǎn)從家出發(fā),要在9點(diǎn)到達(dá)學(xué)校,我們按照正常的步速行走,這可以理解為一個(gè)動(dòng)畫;假如我們半路突然想到忘記帶書包了,需要回家拿書包(相當(dāng)于又添加了一個(gè)動(dòng)畫),這時(shí)我們肯定需要加快步速,當(dāng)我們拿到書包時(shí)相當(dāng)于第二個(gè)動(dòng)畫結(jié)束了,但我們上學(xué)這個(gè)動(dòng)畫還要繼續(xù)執(zhí)行,我們要以合適的速度繼續(xù)往學(xué)校趕,保證在9點(diǎn)準(zhǔn)時(shí)到達(dá)終點(diǎn)—學(xué)校。
所以剛才那個(gè)方塊為什么會(huì)有一個(gè)比100還小的過程就不難理解了:當(dāng)?shù)诙€(gè)動(dòng)畫加上去的時(shí)候,由于它是一個(gè)1s由200變?yōu)?00的動(dòng)畫,肯定要比3s動(dòng)畫執(zhí)行的快,而且是從120的位置開始執(zhí)行的,所以一定會(huì)朝反方向變化到比100還??;1s動(dòng)畫結(jié)束后,又會(huì)以適當(dāng)?shù)乃俣仍?s的時(shí)間點(diǎn)回到最終位置(100,100)。當(dāng)然疊加后的整個(gè)過程在內(nèi)部實(shí)現(xiàn)中可能是根據(jù)時(shí)間函數(shù)已經(jīng)計(jì)算好的。
這么做或許是為了讓動(dòng)畫顯得更流暢平滑,那么既然我們設(shè)置屬性值是立即生效的,動(dòng)畫只是看上去的效果,那剛才疊加的時(shí)刻屏幕展示上的位置(120,120)又是什么呢?這就是本篇要討論的下一個(gè)話題。
2.展示層(presentationLayer)和模型層(modelLayer)
我們知道UIView動(dòng)畫其實(shí)是layer層做的,而view是對layer的一層封裝,我們對view的bounds等這些屬性的操作其實(shí)都是對它所持有的layer進(jìn)行操作,我們做一個(gè)簡單的實(shí)驗(yàn)—在UIView動(dòng)畫的block中改變view的bounds后,分別查看下view和layer的bounds的實(shí)際值:
_testView.bounds = CGRectMake(0, 0, 100, 100);
[UIView animateWithDuration:1 animations:^(void){
_testView.bounds = CGRectMake(0, 0, 200, 200);
} completion:nil];
賦值完成后我們分別打印view,layer的bounds:

都已經(jīng)變成了(200,200),這是肯定的,之前已經(jīng)驗(yàn)證過set view的bounds實(shí)際上就是set 它的layer的bounds??蓜?dòng)畫不是layer實(shí)現(xiàn)的么?layer也已經(jīng)到達(dá)終點(diǎn)了,它是怎么將動(dòng)畫展示出來的呢?
這里就要提到CALayer的兩個(gè)實(shí)例方法presentationLayer和modelLayer:
@interface CALayer : NSObject <NSCoding, CAMediaTiming>
...
/* 以下參考官方api注釋 */
/* presentationLayer
* 返回一個(gè)layer的拷貝,如果有任何活動(dòng)動(dòng)畫時(shí),包含當(dāng)前狀態(tài)的所有l(wèi)ayer屬性
* 實(shí)際上是逼近當(dāng)前狀態(tài)的近似值。
* 嘗試以任何方式修改返回的結(jié)果都是未定義的。
* 返回值的sublayers 、mask、superlayer是當(dāng)前l(fā)ayer的這些屬性的presentationLayer
*/
- (nullable instancetype)presentationLayer;
/* modelLayer
* 對presentationLayer調(diào)用,返回當(dāng)前模型值。
* 對非presentationLayer調(diào)用,返回本身。
* 在生成表示層的事務(wù)完成后調(diào)用此方法的結(jié)果未定義。
*/
- (instancetype)modelLayer;
...
從注釋不難看出,這個(gè)presentationLayer即是我們看到的屏幕上展示的狀態(tài),而modelLayer就是我們設(shè)置完立即生效的真實(shí)狀態(tài),我們動(dòng)畫開始后延遲0.1s分別打印layer,layer.presentationLayer,layer.modelLayer和layer.presentationLayer.modelLayer :

明顯,layer.presentationLayer是動(dòng)畫當(dāng)前狀態(tài)的值,而layer.modelLayer 和 layer.presentationLayer.modelLayer 都是layer本身。(關(guān)于modelLayer注釋中兩句話的區(qū)別還請各位指教~)
到這里,CALayer動(dòng)畫的原理基本清晰了,當(dāng)有動(dòng)畫加入時(shí),presentationLayer會(huì)不斷的(從按某種插值或逼近得到的動(dòng)畫路徑上)取值來進(jìn)行展示,當(dāng)動(dòng)畫結(jié)束被移除時(shí)則取modelLayer的狀態(tài)展示。這也是為什么我們用CABasicAnimation時(shí),設(shè)定當(dāng)前值為fromValue時(shí)動(dòng)畫執(zhí)行結(jié)束又會(huì)回到起點(diǎn)的原因,實(shí)際上動(dòng)畫結(jié)束并不是回到起點(diǎn)而是到了modelLayer的位置。
雖然我們可以使用fillMode控制它結(jié)束時(shí)保持狀態(tài),但這種方法在動(dòng)畫執(zhí)行完之后并沒有將動(dòng)畫從渲染樹中移除(因?yàn)槲覀冃枰O(shè)置animation.removedOnCompletion = NO才能讓fillMode生效)。如果我們想讓動(dòng)畫停在終點(diǎn),更合理的辦法是一開始就將layer設(shè)置成終點(diǎn)狀態(tài),其實(shí)前文提到的UIView的block動(dòng)畫就是這么做的。
如果我們一開始就將layer設(shè)置成終點(diǎn)狀態(tài)再加入動(dòng)畫,會(huì)不會(huì)造成動(dòng)畫在終點(diǎn)位置閃一下呢?其實(shí)是不會(huì)的,因?yàn)槲覀兛吹降膶?shí)際上是presentationLayer,而我們修改layer的屬性,presentationLayer是不會(huì)立即改變的:
MyTestView *view = [[MyTestView alloc]initWithFrame:CGRectMake(200, 200, 100, 100)];
[self.view addSubview:view];
view.center = CGPointMake(1000, 1000);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((1/60) * NSEC_PER_SEC)), dispatchQueue, ^{
NSLog(@"presentationLayer %@ y %f",view.layer.presentationLayer, view.layer.presentationLayer.position.y);
NSLog(@"layer.modelLayer %@ y %f",view.layer.modelLayer,view.layer.modelLayer.position.y);
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((1/20) * NSEC_PER_SEC)), dispatchQueue, ^{
NSLog(@"presentationLayer %@ y %f",view.layer.presentationLayer, view.layer.presentationLayer.position.y);
NSLog(@"layer.modelLayer %@ y %f",view.layer.modelLayer,view.layer.modelLayer.position.y);
});
在上面代碼中我們改變view的center,modelLayer是立即改變的因?yàn)樗褪莑ayer本身。但presentationLayer是沒有變的,我們嘗試延遲一定時(shí)間再去取presentationLayer,發(fā)現(xiàn)它是在一個(gè)很短的時(shí)間之后才發(fā)生變化的,這個(gè)時(shí)間跟具體設(shè)備的屏幕刷新頻率有關(guān)。也就是說我們給layer設(shè)置屬性后,當(dāng)下次屏幕刷新時(shí),presentationLayer才會(huì)獲取新值進(jìn)行繪制。因?yàn)槲覀儾豢赡軐γ恳淮螌傩孕薷亩歼M(jìn)行一次繪制,而是將這些修改保存在model層,當(dāng)下次屏幕刷新時(shí)再統(tǒng)一取model層的值重繪。
如果我們添加了動(dòng)畫,并將modelLayer設(shè)置到終點(diǎn)位置,下次屏幕刷新時(shí),presentationLayer會(huì)優(yōu)先從動(dòng)畫中取值來繪制,所以并不會(huì)造成在終點(diǎn)位置閃一下。
總結(jié)
- UIView持有一個(gè)CALayer負(fù)責(zé)展示,view是這個(gè)layer的delegate。改變view的屬性實(shí)際上是在改變它持有的layer的屬性,layer屬性發(fā)生改變時(shí)會(huì)調(diào)用代理方法actionForLayer: forKey: 來得知此次變化是否需要?jiǎng)赢?。對同一個(gè)屬性疊加動(dòng)畫會(huì)從當(dāng)前展示狀態(tài)開始疊加并最終停在modelLayer的真實(shí)位置。
- CALayer內(nèi)部控制兩個(gè)屬性presentationLayer和modelLayer,modelLayer為當(dāng)前l(fā)ayer真實(shí)的狀態(tài),presentationLayer為當(dāng)前l(fā)ayer在屏幕上展示的狀態(tài)。presentationLayer會(huì)在每次屏幕刷新時(shí)更新狀態(tài),如果有動(dòng)畫則根據(jù)動(dòng)畫獲取當(dāng)前狀態(tài)進(jìn)行繪制,動(dòng)畫移除后則取modelLayer的狀態(tài)。
參考資料
對UIView動(dòng)畫和Core Animation的關(guān)系的一點(diǎn)理解
iOS CoreAnimation專題 系列
iOS核心動(dòng)畫高級技巧 系列
iOS動(dòng)畫開發(fā) 系列
