每次討論到Core Animation,時間都是相對的,每一個動畫都有它本身描述的時間,能夠獨立地加速,延時或者偏移。git
beginTime
指定了動畫開始以前的的延遲時間。這裏的延遲從動畫添加到可見圖層的那一刻開始測量,默認是0(就是說動畫會馬上執行)。github
speed
是一個時間的倍數,默認1.0,減小它會減慢圖層/動畫的時間,增長它會加快速度。若是2.0的速度,那麼對於一個duration
爲1的動畫,實際上在0.5秒的時候就已經完成了。app
timeOffset
和beginTime
相似,可是和增長beginTime
致使的延遲動畫不一樣,增長timeOffset
只是讓動畫快進到某一點,例如,對於一個持續1秒的動畫來講,設置timeOffset
爲0.5意味着動畫將從一半的地方開始。ide
和beginTime
不一樣的是,timeOffset
並不受speed
的影響。因此若是你把speed
設爲2.0,把timeOffset
設置爲0.5,那麼你的動畫將從動畫最後結束的地方開始,由於1秒的動畫實際上被縮短到了0.5秒。然而即便使用了timeOffset
讓動畫從結束的地方開始,它仍然播放了一個完整的時長,這個動畫僅僅是循環了一圈,而後從頭開始播放。函數
能夠用清單9.3的測試程序驗證一下,設置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 測試時間偏移和速度的簡單的應用程序atom
fillMode
對於beginTime
非0的一段動畫來講,會出現一個當動畫添加到圖層上但什麼也沒發生的狀態。相似的,removeOnCompletion
被設置爲NO
的動畫將會在動畫結束的時候仍然保持以前的狀態。這就產生了一個問題,當動畫開始以前和動畫結束以後,被設置動畫的屬性將會是什麼值呢?spa
一種多是屬性和動畫沒被添加以前保持一致,也就是在模型圖層定義的值(見第七章「隱式動畫」,模型圖層和呈現圖層的解釋)。code
另外一種多是保持動畫開始以前那一幀,或者動畫結束以後的那一幀。這就是所謂的填充,由於動畫開始和結束的值用來填充開始以前和結束以後的時間。
這種行爲就交給開發者了,它能夠被CAMediaTiming
的fillMode
來控制。fillMode
是一個NSString
類型,能夠接受以下四種常量:
kCAFillModeForwards kCAFillModeBackwards kCAFillModeBoth kCAFillModeRemoved
默認是kCAFillModeRemoved
,當動畫再也不播放的時候就顯示圖層模型指定的值剩下的三種類型向前,向後或者即向前又向後去填充動畫狀態,使得動畫在開始前或者結束後仍然保持開始和結束那一刻的值。
這就對避免在動畫結束的時候急速返回提供另外一種方案(見第八章)。可是記住了,當用它來解決這個問題的時候,須要把removeOnCompletion
設置爲NO
,另外須要給動畫添加一個非空的鍵,因而能夠在不須要動畫的時候把它從圖層上移除。
在第三章「圖層幾何學」中,你已經瞭解到每一個圖層是如何相對在圖層樹中的父圖層定義它的座標系的。動畫時間和它相似,每一個動畫和圖層在時間上都有它本身的層級概念,相對於它的父親來測量。對圖層調整時間將會影響到它自己和子圖層的動畫,但不會影響到父圖層。另外一個類似點是全部的動畫都被按照層級組合(使用CAAnimationGroup
實例)。
對CALayer
或者CAGroupAnimation
調整duration
和repeatCount
/repeatDuration
屬性並不會影響到子動畫。可是beginTime
,timeOffset
和speed
屬性將會影響到子動畫。然而在層級關係中,beginTime
指定了父圖層開始動畫(或者組合關係中的父動畫)和對象將要開始本身動畫之間的偏移。相似的,調整CALayer
和CAGroupAnimation
的speed
屬性將會對動畫以及子動畫速度應用一個縮放的因子。
CoreAnimation有一個全局時間的概念,也就是所謂的馬赫時間(「馬赫」其實是iOS和Mac OS系統內核的命名)。馬赫時間在設備上全部進程都是全局的--可是在不一樣設備上並非全局的--不過這已經足夠對動畫的參考點提供便利了,你可使用CACurrentMediaTime
函數來訪問馬赫時間:
CFTimeInterval time = CACurrentMediaTime();
這個函數返回的值其實可有可無(它返回了設備自從上次啓動後的秒數,並非你所關心的),它真實的做用在於對動畫的時間測量提供了一個相對值。注意當設備休眠的時候馬赫時間會暫停,也就是全部的CAAnimations
(基於馬赫時間)一樣也會暫停。
所以馬赫時間對長時間測量並不有用。好比用CACurrentMediaTime
去更新一個實時鬧鐘並不明智。(能夠用[NSDate date]
代替,就像第三章例子所示)。
每一個CALayer
和CAAnimation
實例都有本身本地時間的概念,是根據父圖層/動畫層級關係中的beginTime
,timeOffset
和speed
屬性計算。就和轉換不一樣圖層之間座標關係同樣,CALayer
一樣也提供了方法來轉換不一樣圖層之間的本地時間。以下:
- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l; - (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;
當用來同步不一樣圖層之間有不一樣的speed
,timeOffset
和beginTime
的動畫,這些方法會頗有用。
設置動畫的speed
屬性爲0能夠暫停動畫,但在動畫被添加到圖層以後不太可能再修改它了,因此不能對正在進行的動畫使用這個屬性。給圖層添加一個CAAnimation
其實是給動畫對象作了一個不可改變的拷貝,因此對原始動畫對象屬性的改變對真實的動畫並無做用。相反,直接用-animationForKey:
來檢索圖層正在進行的動畫能夠返回正確的動畫對象,可是修改它的屬性將會拋出異常。
若是移除圖層正在進行的動畫,圖層將會急速返回動畫以前的狀態。但若是在動畫移除以前拷貝呈現圖層到模型圖層,動畫將會看起來暫停在那裏。可是很差的地方在於以後就不能再恢復動畫了。
一個簡單的方法是能夠利用CAMediaTiming
來暫停圖層自己。若是把圖層的speed
設置成0,它會暫停任何添加到圖層上的動畫。相似的,設置speed
大於1.0將會快進,設置成一個負值將會倒回動畫。
經過增長主窗口圖層的speed
,能夠暫停整個應用程序的動畫。這對UI自動化提供了好處,咱們能夠加速全部的視圖動畫來進行自動化測試(注意對於在主窗口以外的視圖並不會被影響,好比UIAlertview
)。能夠在app delegate設置以下進行驗證:
self.window.layer.speed = 100;
你也能夠經過這種方式來減速,但其實也能夠在模擬器經過切換慢速動畫來實現。
timeOffset
一個頗有用的功能在於你能夠它可讓你手動控制動畫進程,經過設置speed
爲0,能夠禁用動畫的自動播放,而後來使用timeOffset
來來回顯示動畫序列。這可使得運用手勢來手動控制動畫變得很簡單。
舉個簡單的例子:仍是以前關門的動畫,修改代碼來用手勢控制動畫。咱們給視圖添加一個UIPanGestureRecognizer
,而後用timeOffset
左右搖晃。
由於在動畫添加到圖層以後不能再作修改了,咱們來經過調整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
這實際上是個小詭計,也許相對於設置個動畫而後每次顯示一幀而言,用移動手勢來直接設置門的transform
會更簡單。
在這個例子中的確是這樣,可是對於好比說關鍵這這樣更加複雜的狀況,或者有多個圖層的動畫組,相對於實時計算每一個圖層的屬性而言,這就顯得方便的多了。
在這一章,咱們瞭解了CAMediaTiming
協議,以及Core Animation用來操做時間控制動畫的機制。在下一章,咱們將要接觸緩衝
,另外一個用來使動畫更加真實的操做時間的技術。