時間和空間最大的區(qū)別在于,時間不能被復用 -- 弗斯特梅里克
在上面兩章中,我們探討了可以用CAAnimation
和它的子類實現(xiàn)的多種圖層動畫。動畫的發(fā)生是需要持續(xù)一段時間的,所以計時對整個概念來說至關(guān)重要。在這一章中,我們來看看CAMediaTiming
,看看Core Animation是如何跟蹤時間的。
CAMediaTiming
協(xié)議CAMediaTiming
協(xié)議定義了在一段動畫內(nèi)用來控制逝去時間的屬性的集合,CALayer
和CAAnimation
都實現(xiàn)了這個協(xié)議,所以時間可以被任意基于一個圖層或者一段動畫的類控制。
我們在第八章“顯式動畫”中簡單提到過duration
(CAMediaTiming
的屬性之一),duration
是一個CFTimeInterval
的類型(類似于NSTimeInterval
的一種雙精度浮點類型),對將要進行的動畫的一次迭代指定了時間。
這里的一次迭代是什么意思呢?CAMediaTiming
另外還有一個屬性叫做repeatCount
,代表動畫重復的迭代次數(shù)。如果duration
是2,repeatCount
設(shè)為3.5(三個半迭代),那么完整的動畫時長將是7秒。
duration
和repeatCount
默認都是0。但這不意味著動畫時長為0秒,或者0次,這里的0僅僅代表了“默認”,也就是0.25秒和1次,你可以用一個簡單的測試來嘗試為這兩個屬性賦多個值,如清單9.1,圖9.1展示了程序的結(jié)果。
清單9.1 測試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 演示duration
和repeatCount
的測試程序
創(chuàng)建重復動畫的另一種方式是使用repeatDuration
屬性,它讓動畫重復一個指定的時間,而不是指定次數(shù)。你甚至設(shè)置一個叫做autoreverses
的屬性(BOOL類型)在每次間隔交替循環(huán)過程中自動回放。這對于播放一段連續(xù)非循環(huán)的動畫很有用,例如打開一扇門,然后關(guān)上它(圖9.2)。
圖9.2 擺動門的動畫
對門進行擺動的代碼見清單9.2。我們用了autoreverses
來使門在打開后自動關(guān)閉,在這里我們把repeatDuration
設(shè)置為INFINITY
,于是動畫無限循環(huán)播放,設(shè)置repeatCount
為INFINITY
也有同樣的效果。注意repeatCount
和repeatDuration
可能會相互沖突,所以你只要對其中一個指定非零值。對兩個屬性都設(shè)置非0值的行為沒有被定義。
清單9.2 使用autoreverses
屬性實現(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
每次討論到Core Animation,時間都是相對的,每個動畫都有它自己描述的時間,可以獨立地加速,延時或者偏移。
beginTime
指定了動畫開始之前的的延遲時間。這里的延遲從動畫添加到可見圖層的那一刻開始測量,默認是0(就是說動畫會立刻執(zhí)行)。
speed
是一個時間的倍數(shù),默認1.0,減少它會減慢圖層/動畫的時間,增加它會加快速度。如果2.0的速度,那么對于一個duration
為1的動畫,實際上在0.5秒的時候就已經(jīng)完成了。
timeOffset
和beginTime
類似,但是和增加beginTime
導致的延遲動畫不同,增加timeOffset
只是讓動畫快進到某一點,例如,對于一個持續(xù)1秒的動畫來說,設(shè)置timeOffset
為0.5意味著動畫將從一半的地方開始。
和beginTime
不同的是,timeOffset
并不受speed
的影響。所以如果你把speed
設(shè)為2.0,把timeOffset
設(shè)置為0.5,那么你的動畫將從動畫最后結(jié)束的地方開始,因為1秒的動畫實際上被縮短到了0.5秒。然而即使使用了timeOffset
讓動畫從結(jié)束的地方開始,它仍然播放了一個完整的時長,這個動畫僅僅是循環(huán)了一圈,然后從頭開始播放。
可以用清單9.3的測試程序驗證一下,設(shè)置speed
和timeOffset
滑塊到隨意的值,然后點擊播放來觀察效果(見圖9.3)
清單9.3 測試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
圖9.3 測試時間偏移和速度的簡單的應(yīng)用程序
fillMode
對于beginTime
非0的一段動畫來說,會出現(xiàn)一個當動畫添加到圖層上但什么也沒發(fā)生的狀態(tài)。類似的,removeOnCompletion
被設(shè)置為NO
的動畫將會在動畫結(jié)束的時候仍然保持之前的狀態(tài)。這就產(chǎn)生了一個問題,當動畫開始之前和動畫結(jié)束之后,被設(shè)置動畫的屬性將會是什么值呢?
一種可能是屬性和動畫沒被添加之前保持一致,也就是在模型圖層定義的值(見第七章“隱式動畫”,模型圖層和呈現(xiàn)圖層的解釋)。
另一種可能是保持動畫開始之前那一幀,或者動畫結(jié)束之后的那一幀。這就是所謂的填充,因為動畫開始和結(jié)束的值用來填充開始之前和結(jié)束之后的時間。
這種行為就交給開發(fā)者了,它可以被CAMediaTiming
的fillMode
來控制。fillMode
是一個NSString
類型,可以接受如下四種常量:
kCAFillModeForwards
kCAFillModeBackwards
kCAFillModeBoth
kCAFillModeRemoved
默認是kCAFillModeRemoved
,當動畫不再播放的時候就顯示圖層模型指定的值剩下的三種類型向前,向后或者即向前又向后去填充動畫狀態(tài),使得動畫在開始前或者結(jié)束后仍然保持開始和結(jié)束那一刻的值。
這就對避免在動畫結(jié)束的時候急速返回提供另一種方案(見第八章)。但是記住了,當用它來解決這個問題的時候,需要把removeOnCompletion
設(shè)置為NO
,另外需要給動畫添加一個非空的鍵,于是可以在不需要動畫的時候把它從圖層上移除。
在第三章“圖層幾何學”中,你已經(jīng)了解到每個圖層是如何相對在圖層樹中的父圖層定義它的坐標系的。動畫時間和它類似,每個動畫和圖層在時間上都有它自己的層級概念,相對于它的父親來測量。對圖層調(diào)整時間將會影響到它本身和子圖層的動畫,但不會影響到父圖層。另一個相似點是所有的動畫都被按照層級組合(使用CAAnimationGroup
實例)。
對CALayer
或者CAGroupAnimation
調(diào)整duration
和repeatCount
/repeatDuration
屬性并不會影響到子動畫。但是beginTime
,timeOffset
和speed
屬性將會影響到子動畫。然而在層級關(guān)系中,beginTime
指定了父圖層開始動畫(或者組合關(guān)系中的父動畫)和對象將要開始自己動畫之間的偏移。類似的,調(diào)整CALayer
和CAGroupAnimation
的speed
屬性將會對動畫以及子動畫速度應(yīng)用一個縮放的因子。
CoreAnimation有一個全局時間的概念,也就是所謂的馬赫時間(“馬赫”實際上是iOS和Mac OS系統(tǒng)內(nèi)核的命名)。馬赫時間在設(shè)備上所有進程都是全局的--但是在不同設(shè)備上并不是全局的--不過這已經(jīng)足夠?qū)赢嫷膮⒖键c提供便利了,你可以使用CACurrentMediaTime
函數(shù)來訪問馬赫時間:
CFTimeInterval time = CACurrentMediaTime();
這個函數(shù)返回的值其實無關(guān)緊要(它返回了設(shè)備自從上次啟動后的秒數(shù),并不是你所關(guān)心的),它真實的作用在于對動畫的時間測量提供了一個相對值。注意當設(shè)備休眠的時候馬赫時間會暫停,也就是所有的CAAnimations
(基于馬赫時間)同樣也會暫停。
因此馬赫時間對長時間測量并不有用。比如用CACurrentMediaTime
去更新一個實時鬧鐘并不明智。(可以用[NSDate date]
代替,就像第三章例子所示)。
每個CALayer
和CAAnimation
實例都有自己本地時間的概念,是根據(jù)父圖層/動畫層級關(guān)系中的beginTime
,timeOffset
和speed
屬性計算。就和轉(zhuǎn)換不同圖層之間坐標關(guān)系一樣,CALayer
同樣也提供了方法來轉(zhuǎn)換不同圖層之間的本地時間。如下:
- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;
當用來同步不同圖層之間有不同的speed
,timeOffset
和beginTime
的動畫,這些方法會很有用。
設(shè)置動畫的speed
屬性為0可以暫停動畫,但在動畫被添加到圖層之后不太可能再修改它了,所以不能對正在進行的動畫使用這個屬性。給圖層添加一個CAAnimation
實際上是給動畫對象做了一個不可改變的拷貝,所以對原始動畫對象屬性的改變對真實的動畫并沒有作用。相反,直接用-animationForKey:
來檢索圖層正在進行的動畫可以返回正確的動畫對象,但是修改它的屬性將會拋出異常。
如果移除圖層正在進行的動畫,圖層將會急速返回動畫之前的狀態(tài)。但如果在動畫移除之前拷貝呈現(xiàn)圖層到模型圖層,動畫將會看起來暫停在那里。但是不好的地方在于之后就不能再恢復動畫了。
一個簡單的方法是可以利用CAMediaTiming
來暫停圖層本身。如果把圖層的speed
設(shè)置成0,它會暫停任何添加到圖層上的動畫。類似的,設(shè)置speed
大于1.0將會快進,設(shè)置成一個負值將會倒回動畫。
通過增加主窗口圖層的speed
,可以暫停整個應(yīng)用程序的動畫。這對UI自動化提供了好處,我們可以加速所有的視圖動畫來進行自動化測試(注意對于在主窗口之外的視圖并不會被影響,比如UIAlertview
)。可以在app delegate設(shè)置如下進行驗證:
self.window.layer.speed = 100;
你也可以通過這種方式來減速,但其實也可以在模擬器通過切換慢速動畫來實現(xiàn)。
timeOffset
一個很有用的功能在于你可以它可以讓你手動控制動畫進程,通過設(shè)置speed
為0,可以禁用動畫的自動播放,然后來使用timeOffset
來來回顯示動畫序列。這可以使得運用手勢來手動控制動畫變得很簡單。
舉個簡單的例子:還是之前關(guān)門的動畫,修改代碼來用手勢控制動畫。我們給視圖添加一個UIPanGestureRecognizer
,然后用timeOffset
左右搖晃。
因為在動畫添加到圖層之后不能再做修改了,我們來通過調(diào)整layer
的timeOffset
達到同樣的效果(清單9.4)。
清單9.4 通過觸摸手勢手動控制動畫
@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è)置個動畫然后每次顯示一幀而言,用移動手勢來直接設(shè)置門的transform
會更簡單。
在這個例子中的確是這樣,但是對于比如說關(guān)鍵這這樣更加復雜的情況,或者有多個圖層的動畫組,相對于實時計算每個圖層的屬性而言,這就顯得方便的多了。
在這一章,我們了解了CAMediaTiming
協(xié)議,以及Core Animation用來操作時間控制動畫的機制。在下一章,我們將要接觸緩沖
,另一個用來使動畫更加真實的操作時間的技術(shù)。
更多建議: