动画看起来是用来显示一段连续的运动过程,但实际上当在固定位置上展示像素的时候并不能做到这一点。一般来说这种显示都无法做到连续的移动,能做的仅仅是足够快地展示一系列静态图片,只是看起来像是做了运动。
我们之前提到过iOS按照每秒60次刷新屏幕,然后CAAnimation计算出需要展示的新的帧,然后在每次屏幕更新的时候同步绘制上去,CAAnimation最机智的地方在于每次刷新需要展示的时候去计算插值和缓冲。
在第10章中,我们解决了如何自定义缓冲函数,然后根据需要展示的帧的数组来告诉CAKeyframeAnimation的实例如何去绘制。所有的Core Animation实际上都是按照一定的序列来显示这些帧,那么我们可以自己做到这些么?
NSTimer
实际上,我们在第三章“图层几何学”中已经做过类似的东西,就是时钟那个例子,我们用了NSTimer来对钟表的指针做定时动画,一秒钟更新一次,但是如果我们把频率调整成一秒钟更新60次的话,原理是完全相同的。
我们来试着用NSTimer来修改第十章中弹性球的例子。由于现在我们在定时器启动之后连续计算动画帧,我们需要在类中添加一些额外的属性来存储动画的fromValue,toValue,duration和当前的timeOffset(见清单11.1)。
清单11.1 使用NSTimer实现弹性球动画
1 @interface ViewController () 2 3 @property (nonatomic, weak) IBOutlet UIView *containerView; 4 @property (nonatomic, strong) UIImageView *ballView; 5 @property (nonatomic, strong) NSTimer *timer; 6 @property (nonatomic, assign) NSTimeInterval duration; 7 @property (nonatomic, assign) NSTimeInterval timeOffset; 8 @property (nonatomic, strong) id fromValue; 9 @property (nonatomic, strong) id toValue; 10 11 @end 12 13 @implementation ViewController 14 15 - (void)viewDidLoad 16 { 17 [super viewDidLoad]; 18 //add ball image view 19 UIImage *ballImage = [UIImage imageNamed:@"Ball.png"]; 20 self.ballView = [[UIImageView alloc] initWithImage:ballImage]; 21 [self.containerView addSubview:self.ballView]; 22 //animate 23 [self animate]; 24 } 25 26 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 27 { 28 //replay animation on tap 29 [self animate]; 30 } 31 32 float interpolate(float from, float to, float time) 33 { 34 return (to - from) * time + from; 35 } 36 37 - (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time 38 { 39 if ([fromValue isKindOfClass:[NSValue class]]) { 40 //get type 41 const char *type = [(NSValue *)fromValue objCType]; 42 if (strcmp(type, @encode(CGPoint)) == 0) { 43 CGPoint from = [fromValue CGPointValue]; 44 CGPoint to = [toValue CGPointValue]; 45 CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time)); 46 return [NSValue valueWithCGPoint:result]; 47 } 48 } 49 //provide safe default implementation 50 return (time < 0.5)? fromValue: toValue; 51 } 52 53 float bounceEaseOut(float t) 54 { 55 if (t < 4/11.0) { 56 return (121 * t * t)/16.0; 57 } else if (t < 8/11.0) { 58 return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0; 59 } else if (t < 9/10.0) { 60 return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0; 61 } 62 return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0; 63 } 64 65 - (void)animate 66 { 67 //reset ball to top of screen 68 self.ballView.center = CGPointMake(150, 32); 69 //configure the animation 70 self.duration = 1.0; 71 self.timeOffset = 0.0; 72 self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; 73 self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; 74 //stop the timer if it's already running 75 [self.timer invalidate]; 76 //start the timer 77 self.timer = [NSTimer scheduledTimerWithTimeInterval:1/60.0 78 target:self 79 selector:@selector(step:) 80 userInfo:nil 81 repeats:YES]; 82 } 83 84 - (void)step:(NSTimer *)step 85 { 86 //update time offset 87 self.timeOffset = MIN(self.timeOffset + 1/60.0, self.duration); 88 //get normalized time offset (in range 0 - 1) 89 float time = self.timeOffset / self.duration; 90 //apply easing 91 time = bounceEaseOut(time); 92 //interpolate position 93 id position = [self interpolateFromValue:self.fromValue 94 toValue:self.toValue 95 time:time]; 96 //move ball view to new position 97 self.ballView.center = [position CGPointValue]; 98 //stop the timer if we've reached the end of the animation 99 if (self.timeOffset >= self.duration) { 100 [self.timer invalidate]; 101 self.timer = nil; 102 } 103 } 104 105 @end