iOS Core Animation Advanced Techniques-基于定时器的动画
上十章节:
这篇随笔主要介绍有关基于定时器的动画。
定时帧:
- 示范例子://使用NSTimer实现弹性球动画
//add ball image view UIImage *ballImage = [UIImage imageNamed:@"Ball.png"]; self.ballView = [[UIImageView alloc] initWithImage:ballImage]; [self.containerView addSubview:self.ballView]; //animate float interpolate(float from, float to, float time){ return (to - from) * time + from; } 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; } - (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; } - (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; } }
- /*
- 如果想一次性在屏幕上对很多东西做动画,很明显就会有很多问题。
- NSTimer并不是最佳方案,为了理解这点,我们需要确切地知道NSTimer是如何工作的
- */
NSTimer工作原理:
- iOS上的每个线程都管理了一个NSRunloop,用一个循环来完成一些任务列表
- 对于主线程,这些任务包含如下几项:
- 1.处理触摸事件
- 2.发送和接受网络数据包
- 3.执行使用gcd的代码
- 4.处理计时器行为
- 5.屏幕重绘
- 当设置一个NSTimer,会被插入到当前任务列表中,然后直到指定时间过去之后(他的上一个任务完成之后)才会被执行,这通常会导致有几毫秒的延迟,具体看上个任务结束需要多久。
- 于是就不能保证定时器精准地一秒钟执行六十次,可以通过一些途径来优化:
1.用CADisplayLink让更新频率严格控制在每次屏幕刷新之后
2.基于真实帧的持续时间而不是假设的更新频率来做动画
3.调整动画计时器的run loop模式,这样就不会被别的事件干扰
-
-
- 1.CADisplayLink:
- CoreAnimation提供的另一个类似于NSTimer的类
- 它总是在屏幕完成一次更新之前启动
- 有一个整型的frameInterval属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次
- 如果动画的代码执行起来超过了六十分之一秒,你可以指定frameInterval为2,就是说动画每隔一帧执行一次(一秒钟30帧)或者3,也就是一秒钟20次,等等。
- 2.计算帧的持续时间:
- 在每帧开始刷新的时候用CACurrentMediaTime()记录当前时间,然后和上一帧记录的时间去比较。
- 通过比较这些时间,我们就可以得到真实的每帧持续的时间,然后代替硬编码的六十分之一秒
- 示范例子:// 通过测量没帧持续的时间来使得动画更加平滑
- //add ball image view
- UIImage *ballImage = [UIImage imageNamed:@"Ball.png"];
- self.ballView = [[UIImageView alloc] initWithImage:ballImage];
- [self.containerView addSubview:self.ballView];
- //animate
- float interpolate(float from, float to, float time){
- return (to - from) * time + from;
- }
- 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;
- }
- - (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;
- }
- - (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;
- }
- }
- 3.RunLoop模式:
- 当创建CADisplayLink的时候,我们需要指定一个run loop和run loop mode,使用了主线程的run loop
- 因为任何用户界面的更新都需要在主线程执行
- 一些常见的run loop模式如下:
- NSDefaultRunLoopMode - 标准优先级
- NSRunLoopCommonModes - 高优先级
- UITrackingRunLoopMode - 用于UIScrollView和别的控件的动画
- 对CADisplayLink指定多个run loop模式,于是我们可以同时加入NSDefaultRunLoopMode和UITrackingRunLoopMode来保证它不会被滑动打断,也不会被其他UIKit控件动画影响性能,像这样:
- self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
- [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
- [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];
- 和CADisplayLink类似,NSTimer同样也可以使用不同的run loop模式配置,通过别的函数,而不是+scheduledTimerWithTimeInterval:构造器:
- self.timer = [NSTimer timerWithTimeInterval:1/60.0
- target:self
- selector:@selector(step:)
- userInfo:nil
- repeats:YES];
- [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
- 1.CADisplayLink:
-
- 物理模拟:
- ios有自带的物理引擎UIDynamic。