CAMediaTiming`協議(9.1 圖層時間),camediatiming9.1
#CAMediaTiming`協議
CAMediaTiming
協議定義了在一段動畫內用來控制逝去時間的屬性的集合,CALayer
和CAAnimation
都實現了這個協議,所以時間可以被任意基於一個圖層或者一段動畫的類控制。
持續和重複
我們在第八章“顯式動畫”中簡單提到過duration
(CAMediaTiming
的屬性之一),duration
是一個CFTimeInterval
的類型(類似於NSTimeInterval
的一種雙精確度浮點類型),對將要進行的動畫的一次迭代指定了時間。
這裡的一次迭代是什麼意思呢?CAMediaTiming
另外還有一個屬性叫做repeatCount
,代表動畫重複的迭代次數。如果duration
是2,repeatCount
設為3.5(三個半迭代),那麼完整的動畫時間長度將是7秒。
duration
和repeatCount
預設都是0。但這不意味著動畫時間長度為0秒,或者0次,這裡的0僅僅代表了“預設”,也就是0.25秒和1次,你可以用一個簡單的測試來嘗試為這兩個屬性賦多個值,如清單9.1,圖9.1展示了程式的結果。
清單9.1 測試duration
和repeatCount
1 @interface ViewController () 2 3 @property (nonatomic, weak) IBOutlet UIView *containerView; 4 @property (nonatomic, weak) IBOutlet UITextField *durationField; 5 @property (nonatomic, weak) IBOutlet UITextField *repeatField; 6 @property (nonatomic, weak) IBOutlet UIButton *startButton; 7 @property (nonatomic, strong) CALayer *shipLayer; 8 9 @end10 11 @implementation ViewController12 13 - (void)viewDidLoad14 {15 [super viewDidLoad];16 //add the ship17 self.shipLayer = [CALayer layer];18 self.shipLayer.frame = CGRectMake(0, 0, 128, 128);19 self.shipLayer.position = CGPointMake(150, 150);20 self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;21 [self.containerView.layer addSublayer:self.shipLayer];22 }23 24 - (void)setControlsEnabled:(BOOL)enabled25 {26 for (UIControl *control in @[self.durationField, self.repeatField, self.startButton]) {27 control.enabled = enabled;28 control.alpha = enabled? 1.0f: 0.25f;29 }30 }31 32 - (IBAction)hideKeyboard33 {34 [self.durationField resignFirstResponder];35 [self.repeatField resignFirstResponder];36 }37 38 - (IBAction)start39 {40 CFTimeInterval duration = [self.durationField.text doubleValue];41 float repeatCount = [self.repeatField.text floatValue];42 //animate the ship rotation43 CABasicAnimation *animation = [CABasicAnimation animation];44 animation.keyPath = @"transform.rotation";45 animation.duration = duration;46 animation.repeatCount = repeatCount;47 animation.byValue = @(M_PI * 2);48 animation.delegate = self;49 [self.shipLayer addAnimation:animation forKey:@"rotateAnimation"];50 //disable controls51 [self setControlsEnabled:NO];52 }53 54 - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag55 {56 //reenable controls57 [self setControlsEnabled:YES];58 }59 60 @end
View Code
圖9.1 示範duration
和repeatCount
的測試程式
建立重複動畫的另一種方式是使用repeatDuration
屬性,它讓動畫重複一個指定的時間,而不是指定次數。你甚至設定一個叫做autoreverses
的屬性(BOOL類型)在每次間隔交替迴圈過程中自動回放。這對於播放一段連續非迴圈的動畫很有用,例如開啟一扇門,然後關上它(圖9.2)。
圖9.2 擺動門的動畫
對門進行擺動的代碼見清單9.2。我們用了autoreverses
來使門在開啟後自動關閉,在這裡我們把repeatDuration
設定為INFINITY
,於是動畫無限迴圈播放,設定repeatCount
為INFINITY
也有同樣的效果。注意repeatCount
和repeatDuration
可能會相互衝突,所以你只要對其中一個指定非零值。對兩個屬性都設定非0值的行為沒有被定義。
清單9.2 使用autoreverses
屬性實現門的搖擺
@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
View Code相對時間
每次討論到Core Animation,時間都是相對的,每個動畫都有它自己描述的時間,可以獨立地加速,延時或者位移。
beginTime
指定了動畫開始之前的的延遲時間。這裡的延遲從動畫添加到可見圖層的那一刻開始測量,預設是0(就是說動畫會立刻執行)。
speed
是一個時間的倍數,預設1.0,減少它會減慢圖層/動畫的時間,增加它會加快速度。如果2.0的速度,那麼對於一個duration
為1的動畫,實際上在0.5秒的時候就已經完成了。
timeOffset
和beginTime
類似,但是和增加beginTime
導致的延遲動畫不同,增加timeOffset
只是讓動畫快進到某一點,例如,對於一個持續1秒的動畫來說,設定timeOffset
為0.5意味著動畫將從一半的地方開始。
和beginTime
不同的是,timeOffset
並不受speed
的影響。所以如果你把speed
設為2.0,把timeOffset
設定為0.5,那麼你的動畫將從動畫最後結束的地方開始,因為1秒的動畫實際上被縮短到了0.5秒。然而即使使用了timeOffset
讓動畫從結束的地方開始,它仍然播放了一個完整的時間長度,這個動畫僅僅是迴圈了一圈,然後從頭開始播放。
可以用清單9.3的測試程式驗證一下,設定speed
和timeOffset
滑塊到隨意的值,然後點擊播放來觀察效果(見圖9.3)
清單9.3 測試timeOffset
和speed
屬性
1 @interface ViewController () 2 3 @property (nonatomic, weak) IBOutlet UIView *containerView; 4 @property (nonatomic, weak) IBOutlet UILabel *speedLabel; 5 @property (nonatomic, weak) IBOutlet UILabel *timeOffsetLabel; 6 @property (nonatomic, weak) IBOutlet UISlider *speedSlider; 7 @property (nonatomic, weak) IBOutlet UISlider *timeOffsetSlider; 8 @property (nonatomic, strong) UIBezierPath *bezierPath; 9 @property (nonatomic, strong) CALayer *shipLayer;10 11 @end12 13 @implementation ViewController14 15 - (void)viewDidLoad16 {17 [super viewDidLoad];18 //create a path19 self.bezierPath = [[UIBezierPath alloc] init];20 [self.bezierPath moveToPoint:CGPointMake(0, 150)];21 [self.bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];22 //draw the path using a CAShapeLayer23 CAShapeLayer *pathLayer = [CAShapeLayer layer];24 pathLayer.path = self.bezierPath.CGPath;25 pathLayer.fillColor = [UIColor clearColor].CGColor;26 pathLayer.strokeColor = [UIColor redColor].CGColor;27 pathLayer.lineWidth = 3.0f;28 [self.containerView.layer addSublayer:pathLayer];29 //add the ship30 self.shipLayer = [CALayer layer];31 self.shipLayer.frame = CGRectMake(0, 0, 64, 64);32 self.shipLayer.position = CGPointMake(0, 150);33 self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;34 [self.containerView.layer addSublayer:self.shipLayer];35 //set initial values36 [self updateSliders];37 }38 39 - (IBAction)updateSliders40 {41 CFTimeInterval timeOffset = self.timeOffsetSlider.value;42 self.timeOffsetLabel.text = [NSString stringWithFormat:@"%0.2f", timeOffset];43 float speed = self.speedSlider.value;44 self.speedLabel.text = [NSString stringWithFormat:@"%0.2f", speed];45 }46 47 - (IBAction)play48 {49 //create the keyframe animation50 CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];51 animation.keyPath = @"position";52 animation.timeOffset = self.timeOffsetSlider.value;53 animation.speed = self.speedSlider.value;54 animation.duration = 1.0;55 animation.path = self.bezierPath.CGPath;56 animation.rotationMode = kCAAnimationRotateAuto;57 animation.removedOnCompletion = NO;58 [self.shipLayer addAnimation:animation forKey:@"slide"];59 }60 61 @end
View Code
圖9.3 測試時間位移和速度的簡單的應用程式
fillMode
對於beginTime
非0的一段動畫來說,會出現一個當動畫添加到圖層上但什麼也沒發生的狀態。類似的,removeOnCompletion
被設定為NO
的動畫將會在動畫結束的時候仍然保持之前的狀態。這就產生了一個問題,當動畫開始之前和動畫結束之後,被設定動畫的屬性將會是什麼值呢?
一種可能是屬性和動畫沒被添加之前保持一致,也就是在模型圖層定義的值(見第七章“隱式動畫”,模型圖層和呈現圖層的解釋)。
另一種可能是保持動畫開始之前那一幀,或者動畫結束之後的那一幀。這就是所謂的填充,因為動畫開始和結束的值用來填充開始之前和結束之後的時間。
這種行為就交給開發人員了,它可以被CAMediaTiming
的fillMode
來控制。fillMode
是一個NSString
類型,可以接受如下四種常量:
kCAFillModeForwards kCAFillModeBackwards kCAFillModeBoth kCAFillModeRemoved
預設是kCAFillModeRemoved
,當動畫不再播放的時候就顯示圖層模型指定的值剩下的三種類型向前,向後或者即向前又向後去填充動畫狀態,使得動畫在開始前或者結束後仍然保持開始和結束那一刻的值。
這就對避免在動畫結束的時候急速返回提供另一種方案(見第八章)。但是記住了,當用它來解決這個問題的時候,需要把removeOnCompletion
設定為NO
,另外需要給動畫添加一個非空的鍵,於是可以在不需要動畫的時候把它從圖層上移除。