時(shí)間和空間最大的區(qū)別在于,時(shí)間不能被復(fù)用 -- 弗斯特梅里克
在上面兩章中脯爪,我們探討了可以用CAAnimation和它的子類實(shí)現(xiàn)的多種圖層動(dòng)畫则北。動(dòng)畫的發(fā)生是需要持續(xù)一段時(shí)間的,所以計(jì)時(shí)對(duì)整個(gè)概念來(lái)說至關(guān)重要痕慢。在這一章中尚揣,我們來(lái)看看CAMediaTiming,看看Core Animation是如何跟蹤時(shí)間的掖举。
CAMediaTiming協(xié)議
CAMediaTiming協(xié)議定義了在一段動(dòng)畫內(nèi)用來(lái)控制逝去時(shí)間的屬性的集合快骗,CALayer和CAAnimation都實(shí)現(xiàn)了這個(gè)協(xié)議,所以時(shí)間可以被任意基于一個(gè)圖層或者一段動(dòng)畫的類控制塔次。
持續(xù)和重復(fù)
我們?cè)诘诎苏隆帮@式動(dòng)畫”中簡(jiǎn)單提到過duration(CAMediaTiming的屬性之一)方篮,duration是一個(gè)CFTimeInterval的類型(類似于NSTimeInterval的一種雙精度浮點(diǎn)類型),對(duì)將要進(jìn)行的動(dòng)畫的一次迭代指定了時(shí)間励负。
這里的一次迭代是什么意思呢藕溅?CAMediaTiming另外還有一個(gè)屬性叫做repeatCount,代表動(dòng)畫重復(fù)的迭代次數(shù)继榆。如果duration是2巾表,repeatCount設(shè)為3.5(三個(gè)半迭代),那么完整的動(dòng)畫時(shí)長(zhǎng)將是7秒略吨。
duration和repeatCount默認(rèn)都是0集币。但這不意味著動(dòng)畫時(shí)長(zhǎng)為0秒,或者0次翠忠,這里的0僅僅代表了“默認(rèn)”鞠苟,也就是0.25秒和1次,你可以用一個(gè)簡(jiǎn)單的測(cè)試來(lái)嘗試為這兩個(gè)屬性賦多個(gè)值负间,如清單9.1偶妖,圖9.1展示了程序的結(jié)果。
清單9.1 測(cè)試duration和repeatCount
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UITextField *durationField;
@property (nonatomic, weak) IBOutlet UITextField *repeatField;
@property (nonatomic, weak) IBOutlet UIButton *startButton;
@property (nonatomic, strong) CALayer *shipLayer;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//add the ship
self.shipLayer = [CALayer layer];
self.shipLayer.frame = CGRectMake(0, 0, 128, 128);
self.shipLayer.position = CGPointMake(150, 150);
self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:self.shipLayer];
}
- (void)setControlsEnabled:(BOOL)enabled
{
for (UIControl *control in @[self.durationField, self.repeatField, self.startButton]) {
control.enabled = enabled;
control.alpha = enabled? 1.0f: 0.25f;
}
}
- (IBAction)hideKeyboard
{
[self.durationField resignFirstResponder];
[self.repeatField resignFirstResponder];
}
- (IBAction)start
{
CFTimeInterval duration = [self.durationField.text doubleValue];
float repeatCount = [self.repeatField.text floatValue];
//animate the ship rotation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation";
animation.duration = duration;
animation.repeatCount = repeatCount;
animation.byValue = @(M_PI * 2);
animation.delegate = self;
[self.shipLayer addAnimation:animation forKey:@"rotateAnimation"];
//disable controls
[self setControlsEnabled:NO];
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
//reenable controls
[self setControlsEnabled:YES];
}
@end
```
![9.1.png](http://upload-images.jianshu.io/upload_images/1694376-96df4a0597b27c80.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
創(chuàng)建重復(fù)動(dòng)畫的另一種方式是使用repeatDuration屬性政溃,它讓動(dòng)畫重復(fù)一個(gè)指定的時(shí)間趾访,而不是指定次數(shù)。你甚至設(shè)置一個(gè)叫做autoreverses的屬性(BOOL類型)在每次間隔交替循環(huán)過程中自動(dòng)回放董虱。這對(duì)于播放一段連續(xù)非循環(huán)的動(dòng)畫很有用扼鞋,例如打開一扇門申鱼,然后關(guān)上它(圖9.2)。
![9.2.png](http://upload-images.jianshu.io/upload_images/1694376-275ccb9e9af8fc04.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
對(duì)門進(jìn)行擺動(dòng)的代碼見清單9.2云头。我們用了autoreverses來(lái)使門在打開后自動(dòng)關(guān)閉捐友,在這里我們把repeatDuration設(shè)置為INFINITY,于是動(dòng)畫無(wú)限循環(huán)播放溃槐,設(shè)置repeatCount為INFINITY也有同樣的效果匣砖。注意repeatCount和repeatDuration可能會(huì)相互沖突,所以你只要對(duì)其中一個(gè)指定非零值昏滴。對(duì)兩個(gè)屬性都設(shè)置非0值的行為沒有被定義猴鲫。
#####清單9.2 使用autoreverses屬性實(shí)現(xiàn)門的搖擺
@interface ViewController ()
@property (nonatomic, weak) UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//add the door
CALayer *doorLayer = [CALayer layer];
doorLayer.frame = CGRectMake(0, 0, 128, 256);
doorLayer.position = CGPointMake(150 - 64, 150);
doorLayer.anchorPoint = CGPointMake(0, 0.5);
doorLayer.contents = (__bridge id)[UIImage imageNamed: @"Door.png"].CGImage;
[self.containerView.layer addSublayer:doorLayer];
//apply perspective transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
//apply swinging animation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation.y";
animation.toValue = @(-M_PI_2);
animation.duration = 2.0;
animation.repeatDuration = INFINITY;
animation.autoreverses = YES;
[doorLayer addAnimation:animation forKey:nil];
}
@end
```
相對(duì)時(shí)間
每次討論到Core Animation,時(shí)間都是相對(duì)的谣殊,每個(gè)動(dòng)畫都有它自己描述的時(shí)間拂共,可以獨(dú)立地加速,延時(shí)或者偏移姻几。
beginTime指定了動(dòng)畫開始之前的的延遲時(shí)間宜狐。這里的延遲從動(dòng)畫添加到可見圖層的那一刻開始測(cè)量,默認(rèn)是0(就是說動(dòng)畫會(huì)立刻執(zhí)行)蛇捌。
speed是一個(gè)時(shí)間的倍數(shù)抚恒,默認(rèn)1.0,減少它會(huì)減慢圖層/動(dòng)畫的時(shí)間豁陆,增加它會(huì)加快速度柑爸。如果2.0的速度,那么對(duì)于一個(gè)duration為1的動(dòng)畫盒音,實(shí)際上在0.5秒的時(shí)候就已經(jīng)完成了表鳍。
timeOffset和beginTime類似,但是和增加beginTime導(dǎo)致的延遲動(dòng)畫不同祥诽,增加timeOffset只是讓動(dòng)畫快進(jìn)到某一點(diǎn)譬圣,例如,對(duì)于一個(gè)持續(xù)1秒的動(dòng)畫來(lái)說雄坪,設(shè)置timeOffset為0.5意味著動(dòng)畫將從一半的地方開始厘熟。
和beginTime不同的是,timeOffset并不受speed的影響维哈。所以如果你把speed設(shè)為2.0绳姨,把timeOffset設(shè)置為0.5,那么你的動(dòng)畫將從動(dòng)畫最后結(jié)束的地方開始阔挠,因?yàn)?秒的動(dòng)畫實(shí)際上被縮短到了0.5秒飘庄。然而即使使用了timeOffset讓動(dòng)畫從結(jié)束的地方開始,它仍然播放了一個(gè)完整的時(shí)長(zhǎng)购撼,這個(gè)動(dòng)畫僅僅是循環(huán)了一圈跪削,然后從頭開始播放谴仙。
可以用清單9.3的測(cè)試程序驗(yàn)證一下,設(shè)置speed和timeOffset滑塊到隨意的值碾盐,然后點(diǎn)擊播放來(lái)觀察效果(見圖9.3)
清單9.3 測(cè)試timeOffset和speed屬性
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UILabel *speedLabel;
@property (nonatomic, weak) IBOutlet UILabel *timeOffsetLabel;
@property (nonatomic, weak) IBOutlet UISlider *speedSlider;
@property (nonatomic, weak) IBOutlet UISlider *timeOffsetSlider;
@property (nonatomic, strong) UIBezierPath *bezierPath;
@property (nonatomic, strong) CALayer *shipLayer;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create a path
self.bezierPath = [[UIBezierPath alloc] init];
[self.bezierPath moveToPoint:CGPointMake(0, 150)];
[self.bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
//draw the path using a CAShapeLayer
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.path = self.bezierPath.CGPath;
pathLayer.fillColor = [UIColor clearColor].CGColor;
pathLayer.strokeColor = [UIColor redColor].CGColor;
pathLayer.lineWidth = 3.0f;
[self.containerView.layer addSublayer:pathLayer];
//add the ship
self.shipLayer = [CALayer layer];
self.shipLayer.frame = CGRectMake(0, 0, 64, 64);
self.shipLayer.position = CGPointMake(0, 150);
self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:self.shipLayer];
//set initial values
[self updateSliders];
}
- (IBAction)updateSliders
{
CFTimeInterval timeOffset = self.timeOffsetSlider.value;
self.timeOffsetLabel.text = [NSString stringWithFormat:@"%0.2f", timeOffset];
float speed = self.speedSlider.value;
self.speedLabel.text = [NSString stringWithFormat:@"%0.2f", speed];
}
- (IBAction)play
{
//create the keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.timeOffset = self.timeOffsetSlider.value;
animation.speed = self.speedSlider.value;
animation.duration = 1.0;
animation.path = self.bezierPath.CGPath;
animation.rotationMode = kCAAnimationRotateAuto;
animation.removedOnCompletion = NO;
[self.shipLayer addAnimation:animation forKey:@"slide"];
}
@end
**fillMode**
對(duì)于beginTime非0的一段動(dòng)畫來(lái)說晃跺,會(huì)出現(xiàn)一個(gè)當(dāng)動(dòng)畫添加到圖層上但什么也沒發(fā)生的狀態(tài)。類似的毫玖,removeOnCompletion被設(shè)置為NO
的動(dòng)畫將會(huì)在動(dòng)畫結(jié)束的時(shí)候仍然保持之前的狀態(tài)掀虎。這就產(chǎn)生了一個(gè)問題,當(dāng)動(dòng)畫開始之前和動(dòng)畫結(jié)束之后孕豹,被設(shè)置動(dòng)畫的屬性將會(huì)是什么值呢涩盾?
一種可能是屬性和動(dòng)畫沒被添加之前保持一致十气,也就是在模型圖層定義的值(見第七章“隱式動(dòng)畫”励背,模型圖層和呈現(xiàn)圖層的解釋)。
另一種可能是保持動(dòng)畫開始之前那一幀砸西,或者動(dòng)畫結(jié)束之后的那一幀叶眉。這就是所謂的填充,因?yàn)閯?dòng)畫開始和結(jié)束的值用來(lái)填充開始之前和結(jié)束之后的時(shí)間芹枷。
這種行為就交給開發(fā)者了衅疙,它可以被CAMediaTiming的fillMode
來(lái)控制。fillMode是一個(gè)NSString類型鸳慈,可以接受如下四種常量:
kCAFillModeForwards
kCAFillModeBackwards
kCAFillModeBoth
kCAFillModeRemoved
默認(rèn)是kCAFillModeRemoved饱溢,當(dāng)動(dòng)畫不再播放的時(shí)候就顯示圖層模型指定的值剩下的三種類型向前,向后或者即向前又向后去填充動(dòng)畫狀態(tài)走芋,使得動(dòng)畫在開始前或者結(jié)束后仍然保持開始和結(jié)束那一刻的值绩郎。
這就對(duì)避免在動(dòng)畫結(jié)束的時(shí)候急速返回提供另一種方案(見第八章)。但是記住了翁逞,當(dāng)用它來(lái)解決這個(gè)問題的時(shí)候肋杖,需要把removeOnCompletion設(shè)置為NO,另外需要給動(dòng)畫添加一個(gè)非空的鍵挖函,于是可以在不需要?jiǎng)赢嫷臅r(shí)候把它從圖層上移除状植。
層級(jí)關(guān)系時(shí)間
在第三章“圖層幾何學(xué)”中,你已經(jīng)了解到每個(gè)圖層是如何相對(duì)在圖層樹中的父圖層定義它的坐標(biāo)系的怨喘。動(dòng)畫時(shí)間和它類似津畸,每個(gè)動(dòng)畫和圖層在時(shí)間上都有它自己的層級(jí)概念,相對(duì)于它的父親來(lái)測(cè)量必怜。對(duì)圖層調(diào)整時(shí)間將會(huì)影響到它本身和子圖層的動(dòng)畫肉拓,但不會(huì)影響到父圖層。另一個(gè)相似點(diǎn)是所有的動(dòng)畫都被按照層級(jí)組合(使用CAAnimationGroup實(shí)例)棚赔。
對(duì)CALayer或者CAGroupAnimation調(diào)整duration和repeatCount/repeatDuration屬性并不會(huì)影響到子動(dòng)畫帝簇。但是beginTime徘郭,timeOffset和speed屬性將會(huì)影響到子動(dòng)畫。然而在層級(jí)關(guān)系中丧肴,beginTime指定了父圖層開始動(dòng)畫(或者組合關(guān)系中的父動(dòng)畫)和對(duì)象將要開始自己動(dòng)畫之間的偏移残揉。類似的,調(diào)整CALayer和CAGroupAnimation的speed屬性將會(huì)對(duì)動(dòng)畫以及子動(dòng)畫速度應(yīng)用一個(gè)縮放的因子芋浮。
全局時(shí)間和本地時(shí)間
CoreAnimation有一個(gè)全局時(shí)間的概念抱环,也就是所謂的馬赫時(shí)間(“馬赫”實(shí)際上是iOS和Mac OS系統(tǒng)內(nèi)核的命名)。馬赫時(shí)間在設(shè)備上所有進(jìn)程都是全局的--但是在不同設(shè)備上并不是全局的--不過這已經(jīng)足夠?qū)?dòng)畫的參考點(diǎn)提供便利了纸巷,你可以使用CACurrentMediaTime函數(shù)來(lái)訪問馬赫時(shí)間:
CFTimeInterval time = CACurrentMediaTime();
這個(gè)函數(shù)返回的值其實(shí)無(wú)關(guān)緊要(它返回了設(shè)備自從上次啟動(dòng)后的秒數(shù)镇草,并不是你所關(guān)心的),它真實(shí)的作用在于對(duì)動(dòng)畫的時(shí)間測(cè)量提供了一個(gè)相對(duì)值瘤旨。注意當(dāng)設(shè)備休眠的時(shí)候馬赫時(shí)間會(huì)暫停梯啤,也就是所有的CAAnimations(基于馬赫時(shí)間)同樣也會(huì)暫停。
因此馬赫時(shí)間對(duì)長(zhǎng)時(shí)間測(cè)量并不有用存哲。比如用CACurrentMediaTime
去更新一個(gè)實(shí)時(shí)鬧鐘并不明智因宇。(可以用[NSDate date]代替,就像第三章例子所示)祟偷。
每個(gè)CALayer和CAAnimation實(shí)例都有自己本地時(shí)間的概念察滑,是根據(jù)父圖層/動(dòng)畫層級(jí)關(guān)系中的beginTime,timeOffset和speed屬性計(jì)算修肠。就和轉(zhuǎn)換不同圖層之間坐標(biāo)關(guān)系一樣贺辰,CALayer同樣也提供了方法來(lái)轉(zhuǎn)換不同圖層之間的本地時(shí)間。如下:
(CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;
當(dāng)用來(lái)同步不同圖層之間有不同的speed嵌施,timeOffset和beginTime的動(dòng)畫饲化,這些方法會(huì)很有用。
暫停艰管,倒回和快進(jìn)
設(shè)置動(dòng)畫的speed屬性為0可以暫停動(dòng)畫滓侍,但在動(dòng)畫被添加到圖層之后不太可能再修改它了,所以不能對(duì)正在進(jìn)行的動(dòng)畫使用這個(gè)屬性牲芋。給圖層添加一個(gè)CAAnimation實(shí)際上是給動(dòng)畫對(duì)象做了一個(gè)不可改變的拷貝撩笆,所以對(duì)原始動(dòng)畫對(duì)象屬性的改變對(duì)真實(shí)的動(dòng)畫并沒有作用。相反缸浦,直接用-animationForKey:來(lái)檢索圖層正在進(jìn)行的動(dòng)畫可以返回正確的動(dòng)畫對(duì)象夕冲,但是修改它的屬性將會(huì)拋出異常。
如果移除圖層正在進(jìn)行的動(dòng)畫裂逐,圖層將會(huì)急速返回動(dòng)畫之前的狀態(tài)歹鱼。但如果在動(dòng)畫移除之前拷貝呈現(xiàn)圖層到模型圖層,動(dòng)畫將會(huì)看起來(lái)暫停在那里卜高。但是不好的地方在于之后就不能再恢復(fù)動(dòng)畫了弥姻。
一個(gè)簡(jiǎn)單的方法是可以利用CAMediaTiming來(lái)暫停圖層本身南片。如果把圖層的speed設(shè)置成0,它會(huì)暫停任何添加到圖層上的動(dòng)畫庭敦。類似的疼进,設(shè)置speed大于1.0將會(huì)快進(jìn),設(shè)置成一個(gè)負(fù)值將會(huì)倒回動(dòng)畫秧廉。
通過增加主窗口圖層的speed伞广,可以暫停整個(gè)應(yīng)用程序的動(dòng)畫。這對(duì)UI自動(dòng)化提供了好處疼电,我們可以加速所有的視圖動(dòng)畫來(lái)進(jìn)行自動(dòng)化測(cè)試(注意對(duì)于在主窗口之外的視圖并不會(huì)被影響嚼锄,比如UIAlertview
)”尾颍可以在app delegate設(shè)置如下進(jìn)行驗(yàn)證:
self.window.layer.speed = 100;
你也可以通過這種方式來(lái)減速区丑,但其實(shí)也可以在模擬器通過切換慢速動(dòng)畫來(lái)實(shí)現(xiàn)
手動(dòng)動(dòng)畫
timeOffset一個(gè)很有用的功能在于你可以它可以讓你手動(dòng)控制動(dòng)畫進(jìn)程,通過設(shè)置speed為0茫虽,可以禁用動(dòng)畫的自動(dòng)播放刊苍,然后來(lái)使用timeOffset來(lái)來(lái)回顯示動(dòng)畫序列。這可以使得運(yùn)用手勢(shì)來(lái)手動(dòng)控制動(dòng)畫變得很簡(jiǎn)單濒析。
舉個(gè)簡(jiǎn)單的例子:還是之前關(guān)門的動(dòng)畫,修改代碼來(lái)用手勢(shì)控制動(dòng)畫啥纸。我們給視圖添加一個(gè)UIPanGestureRecognizer号杏,然后用timeOffset左右搖晃。
因?yàn)樵趧?dòng)畫添加到圖層之后不能再做修改了斯棒,我們來(lái)通過調(diào)整layer的timeOffset達(dá)到同樣的效果(清單9.4)盾致。
清單9.4 通過觸摸手勢(shì)手動(dòng)控制動(dòng)畫
@interface ViewController ()
@property (nonatomic, weak) UIView *containerView;
@property (nonatomic, strong) CALayer *doorLayer;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//add the door
self.doorLayer = [CALayer layer];
self.doorLayer.frame = CGRectMake(0, 0, 128, 256);
self.doorLayer.position = CGPointMake(150 - 64, 150);
self.doorLayer.anchorPoint = CGPointMake(0, 0.5);
self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Door.png"].CGImage;
[self.containerView.layer addSublayer:self.doorLayer];
//apply perspective transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
//add pan gesture recognizer to handle swipes
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init];
[pan addTarget:self action:@selector(pan:)];
[self.view addGestureRecognizer:pan];
//pause all layer animations
self.doorLayer.speed = 0.0;
//apply swinging animation (which won't play because layer is paused)
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation.y";
animation.toValue = @(-M_PI_2);
animation.duration = 1.0;
[self.doorLayer addAnimation:animation forKey:nil];
}
- (void)pan:(UIPanGestureRecognizer *)pan
{
//get horizontal component of pan gesture
CGFloat x = [pan translationInView:self.view].x;
//convert from points to animation duration //using a reasonable scale factor
x /= 200.0f;
//update timeOffset and clamp result
CFTimeInterval timeOffset = self.doorLayer.timeOffset;
timeOffset = MIN(0.999, MAX(0.0, timeOffset - x));
self.doorLayer.timeOffset = timeOffset;
//reset pan gesture
[pan setTranslation:CGPointZero inView:self.view];
}
@end
這其實(shí)是個(gè)小詭計(jì),也許相對(duì)于設(shè)置個(gè)動(dòng)畫然后每次顯示一幀而言荣暮,用移動(dòng)手勢(shì)來(lái)直接設(shè)置門的transform會(huì)更簡(jiǎn)單庭惜。
在這個(gè)例子中的確是這樣,但是對(duì)于比如說關(guān)鍵這這樣更加復(fù)雜的情況穗酥,或者有多個(gè)圖層的動(dòng)畫組护赊,相對(duì)于實(shí)時(shí)計(jì)算每個(gè)圖層的屬性而言,這就顯得方便的多了砾跃。
總結(jié)
在這一章骏啰,我們了解了CAMediaTiming協(xié)議,以及Core Animation用來(lái)操作時(shí)間控制動(dòng)畫的機(jī)制抽高。在下一章判耕,我們將要接觸緩沖,另一個(gè)用來(lái)使動(dòng)畫更加真實(shí)的操作時(shí)間的技術(shù)翘骂。