#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`
~~~
@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`的測試程序
創建重復動畫的另一種方式是使用`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
~~~
## 相對時間
每次討論到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`屬性
~~~
@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 測試時間偏移和速度的簡單的應用程序
## fillMode
對于`beginTime`非0的一段動畫來說,會出現一個當動畫添加到圖層上但什么也沒發生的狀態。類似的,`removeOnCompletion`被設置為`NO`的動畫將會在動畫結束的時候仍然保持之前的狀態。這就產生了一個問題,當動畫開始之前和動畫結束之后,被設置動畫的屬性將會是什么值呢?
一種可能是屬性和動畫沒被添加之前保持一致,也就是在模型圖層定義的值(見第七章“隱式動畫”,模型圖層和呈現圖層的解釋)。
另一種可能是保持動畫開始之前那一幀,或者動畫結束之后的那一幀。這就是所謂的*填充*,因為動畫開始和結束的值用來填充開始之前和結束之后的時間。
這種行為就交給開發者了,它可以被`CAMediaTiming`的`fillMode`來控制。`fillMode`是一個`NSString`類型,可以接受如下四種常量:
~~~
kCAFillModeForwards
kCAFillModeBackwards
kCAFillModeBoth
kCAFillModeRemoved
~~~
默認是`kCAFillModeRemoved`,當動畫不再播放的時候就顯示圖層模型指定的值剩下的三種類型向前,向后或者即向前又向后去填充動畫狀態,使得動畫在開始前或者結束后仍然保持開始和結束那一刻的值。
這就對避免在動畫結束的時候急速返回提供另一種方案(見第八章)。但是記住了,當用它來解決這個問題的時候,需要把`removeOnCompletion`設置為`NO`,另外需要給動畫添加一個非空的鍵,于是可以在不需要動畫的時候把它從圖層上移除。
- Introduction
- 1. 圖層樹
- 1.1 圖層與視圖
- 1.2 圖層的能力
- 1.3 使用圖層
- 1.4 總結
- 2. 寄宿圖
- 2.1 contents屬性
- 2.2 Custom Drawing
- 2.3 總結
- 3. 圖層幾何學
- 3.1 布局
- 3.2 錨點
- 3.3 坐標系
- 3.4 Hit Testing
- 3.5 自動布局
- 3.6 總結
- 4. 視覺效果
- 4.1 圓角
- 4.2 圖層邊框
- 4.3 陰影
- 4.4 圖層蒙板
- 4.5 拉伸過濾
- 4.6 組透明
- 4.7 總結
- 5. 變換
- 5.1 仿射變換
- 5.2 3D變換
- 5.3 固體對象
- 5.4 總結
- 6. 專用圖層
- 6.1 CAShapeLayer
- 6.2 CATextLayer
- 6.3 CATransformLayer
- 6.4 CAGradientLayer
- 6.5 CAReplicatorLayer
- 6.6 CAScrollLayer
- 6.7 CATiledLayer
- 6.8 CAEmitterLayer
- 6.9 CAEAGLLayer
- 6.10 AVPlayerLayer
- 6.11 總結
- 7. 隱式動畫
- 7.1 事務
- 7.2 完成塊
- 7.3 圖層行為
- 7.4 呈現與模型
- 7.5 總結
- 8. 顯式動畫
- 8.1 屬性動畫
- 8.2 動畫組
- 8.3 過渡
- 8.4 在動畫過程中取消動畫
- 8.5 總結
- 9. 圖層時間
- 9.1 CAMediaTiming協議
- 9.2 層級關系時間
- 9.3 手動動畫
- 9.4 總結
- 10. 緩沖
- 10.1 動畫速度
- 10.2 自定義緩沖函數
- 10.3 總結
- 11. 基于定時器的動畫
- 11.1 定時幀
- 11.2 物理模擬
- 12. 性能調優
- 12.1. CPU VS GPU
- 12.2 測量,而不是猜測
- 12.3 Instruments
- 12.4 總結
- 13. 高效繪圖
- 13.1 軟件繪圖
- 13.2 矢量圖形
- 13.3 臟矩形
- 13.4 異步繪制
- 13.5 總結
- 14. 圖像IO
- 14.1 加載和潛伏
- 14.2 緩存
- 14.3 文件格式
- 14.4 總結
- 15. 圖層性能
- 15.1 隱式繪制
- 15.2 離屏渲染
- 15.3 混合和過度繪制
- 15.4 減少圖層數量
- 15.5 總結