每次讨论到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用来操做时间控制动画的机制。在下一章,咱们将要接触缓冲
,另外一个用来使动画更加真实的操做时间的技术。