我能够指导你,可是你必须按照我说的作。 -- 骇客帝国git
在第10章“缓冲”中,咱们研究了CAMediaTimingFunction
,它是一个经过控制动画缓冲来模拟物理效果例如加速或者减速来加强现实感的东西,那么若是想更加真实地模拟物理交互或者实时根据用户输入修改动画改怎么办呢?在这一章中,咱们将继续探索一种可以容许咱们精确地控制一帧一帧展现的基于定时器的动画。github
动画看起来是用来显示一段连续的运动过程,但实际上当在固定位置上展现像素的时候并不能作到这一点。通常来讲这种显示都没法作到连续的移动,能作的仅仅是足够快地展现一系列静态图片,只是看起来像是作了运动。数组
咱们以前提到过iOS按照每秒60次刷新屏幕,而后CAAnimation
计算出须要展现的新的帧,而后在每次屏幕更新的时候同步绘制上去,CAAnimation
最机智的地方在于每次刷新须要展现的时候去计算插值和缓冲。网络
在第10章中,咱们解决了如何自定义缓冲函数,而后根据须要展现的帧的数组来告诉CAKeyframeAnimation
的实例如何去绘制。全部的Core Animation实际上都是按照必定的序列来显示这些帧,那么咱们能够本身作到这些么?app
NSTimer
实际上,咱们在第三章“图层几何学”中已经作过相似的东西,就是时钟那个例子,咱们用了NSTimer
来对钟表的指针作定时动画,一秒钟更新一次,可是若是咱们把频率调整成一秒钟更新60次的话,原理是彻底相同的。ide
咱们来试着用NSTimer
来修改第十章中弹性球的例子。因为如今咱们在定时器启动以后连续计算动画帧,咱们须要在类中添加一些额外的属性来存储动画的fromValue
,toValue
,duration
和当前的timeOffset
(见清单11.1)。函数
清单11.1 使用NSTimer
实现弹性球动画oop
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic, strong) UIImageView *ballView; @property (nonatomic, strong) NSTimer *timer; @property (nonatomic, assign) NSTimeInterval duration; @property (nonatomic, assign) NSTimeInterval timeOffset; @property (nonatomic, strong) id fromValue; @property (nonatomic, strong) id toValue;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; //add ball image view UIImage *ballImage = [UIImage imageNamed:@"Ball.png"]; self.ballView = [[UIImageView alloc] initWithImage:ballImage]; [self.containerView addSubview:self.ballView]; //animate [self animate]; }- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ //replay animation on tap [self animate]; }float interpolate(float from, float to, float time) { return (to - from) * time + from; }- (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time { if ([fromValue isKindOfClass:[NSValue class]]) { //get type const char *type = [(NSValue *)fromValue objCType]; if (strcmp(type, @encode(CGPoint)) == 0) { CGPoint from = [fromValue CGPointValue]; CGPoint to = [toValue CGPointValue]; CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time)); return [NSValue valueWithCGPoint:result]; } } //provide safe default implementation return (time < 0.5)? fromValue: toValue; }float bounceEaseOut(float t) { if (t < 4/11.0) { return (121 * t * t)/16.0; } else if (t < 8/11.0) { return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0; } else if (t < 9/10.0) { return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0; } return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0; }- (void)animate { //reset ball to top of screen self.ballView.center = CGPointMake(150, 32); //configure the animation self.duration = 1.0; self.timeOffset = 0.0; self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; //stop the timer if it's already running [self.timer invalidate]; //start the timer self.timer = [NSTimer scheduledTimerWithTimeInterval:1/60.0 target:self selector:@selector(step:) userInfo:nil repeats:YES]; }- (void)step:(NSTimer *)step { //update time offset self.timeOffset = MIN(self.timeOffset + 1/60.0, self.duration); //get normalized time offset (in range 0 - 1) float time = self.timeOffset / self.duration; //apply easing time = bounceEaseOut(time); //interpolate position id position = [self interpolateFromValue:self.fromValue toValue:self.toValue time:time]; //move ball view to new position self.ballView.center = [position CGPointValue]; //stop the timer if we've reached the end of the animation if (self.timeOffset >= self.duration) { [self.timer invalidate]; self.timer = nil; } }@end
很赞,并且和基于关键帧例子的代码同样不少,可是若是想一次性在屏幕上对不少东西作动画,很明显就会有不少问题。优化
NSTimer
并非最佳方案,为了理解这点,咱们须要确切地知道NSTimer
是如何工做的。iOS上的每一个线程都管理了一个NSRunloop
,字面上看就是经过一个循环来完成一些任务列表。可是对主线程,这些任务包含以下几项:动画
处理触摸事件
发送和接受网络数据包
执行使用gcd的代码
处理计时器行为
屏幕重绘
当你设置一个NSTimer
,他会被插入到当前任务列表中,而后直到指定时间过去以后才会被执行。可是什么时候启动定时器并无一个时间上限,并且它只会在列表中上一个任务完成以后开始执行。这一般会致使有几毫秒的延迟,可是若是上一个任务过了好久才完成就会致使延迟很长一段时间。
屏幕重绘的频率是一秒钟六十次,可是和定时器行为同样,若是列表中上一个执行了很长时间,它也会延迟。这些延迟都是一个随机值,因而就不能保证定时器精准地一秒钟执行六十次。有时候发生在屏幕重绘以后,这就会使得更新屏幕会有个延迟,看起来就是动画卡壳了。有时候定时器会在屏幕更新的时候执行两次,因而动画看起来就跳动了。
咱们能够经过一些途径来优化:
咱们能够用CADisplayLink
让更新频率严格控制在每次屏幕刷新以后。
基于真实帧的持续时间而不是假设的更新频率来作动画。
调整动画计时器的run loop
模式,这样就不会被别的事件干扰。
CADisplayLink
CADisplayLink 是CoreAnimation提供的另外一个相似于 NSTimer 的类,它老是在屏幕完成一次更新以前启动,它的接口设计的和 NSTimer 很相似,因此它实际上就是一个内置实现的替代,可是和 timeInterval 以秒为单位不一样, CADisplayLink 有一个整型的 frameInterval 属性,指定了间隔多少帧以后才执行。默认值是1,意味着每次屏幕更新以前都会执行一次。可是若是动画的代码执行起来超过了六十分之一秒,你能够指定frameInterval
为2,就是说动画每隔一帧执行一次(一秒钟30帧)或者3,也就是一秒钟20次,等等。
用 CADisplayLink 而不是 NSTimer ,会保证帧率足够连续,使得动画看起来更加平滑,但即便 CADisplayLink 也不能保证每一帧都按计划执行,一些失去控制的离散的任务或者事件(例如资源紧张的后台程序)可能会致使动画偶尔地丢帧。当使用NSTimer
的时候,一旦有机会计时器就会开启,可是 CADisplayLink 却不同:若是它丢失了帧,就会直接忽略它们,而后在下一次更新的时候接着运行。
不管是使用NSTimer
仍是 CADisplayLink ,咱们仍然须要处理一帧的时间超出了预期的六十分之一秒。因为咱们不可以计算出一帧真实的持续时间,因此须要手动测量。咱们能够在每帧开始刷新的时候用 CACurrentMediaTime() 记录当前时间,而后和上一帧记录的时间去比较。
经过比较这些时间,咱们就能够获得真实的每帧持续的时间,而后代替硬编码的六十分之一秒。咱们来更新一下上个例子(见清单11.2)。
清单11.2 经过测量没帧持续的时间来使得动画更加平滑
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic, strong) UIImageView *ballView; @property (nonatomic, strong) CADisplayLink *timer; @property (nonatomic, assign) CFTimeInterval duration; @property (nonatomic, assign) CFTimeInterval timeOffset; @property (nonatomic, assign) CFTimeInterval lastStep; @property (nonatomic, strong) id fromValue; @property (nonatomic, strong) id toValue;@end@implementation ViewController ...- (void)animate { //reset ball to top of screen self.ballView.center = CGPointMake(150, 32); //configure the animation self.duration = 1.0; self.timeOffset = 0.0; self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; //stop the timer if it's already running [self.timer invalidate]; //start the timer self.lastStep = CACurrentMediaTime(); self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)]; [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; }- (void)step:(CADisplayLink *)timer { //calculate time delta CFTimeInterval thisStep = CACurrentMediaTime(); CFTimeInterval stepDuration = thisStep - self.lastStep; self.lastStep = thisStep; //update time offset self.timeOffset = MIN(self.timeOffset + stepDuration, self.duration); //get normalized time offset (in range 0 - 1) float time = self.timeOffset / self.duration; //apply easing time = bounceEaseOut(time); //interpolate position id position = [self interpolateFromValue:self.fromValue toValue:self.toValue time:time]; //move ball view to new position self.ballView.center = [position CGPointValue]; //stop the timer if we've reached the end of the animation if (self.timeOffset >= self.duration) { [self.timer invalidate]; self.timer = nil; } }@end