在第八章中,咱们给时钟项目添加了动画。看起来很赞,可是若是有合适的缓冲函数就更好了。在显示世界中,钟表指针转动的时候,一般起步很慢,而后迅速啪地一声,最后缓冲到终点。可是标准的缓冲函数在这里每个适合它,那该如何建立一个新的呢?git
除了+functionWithName:
以外,CAMediaTimingFunction
一样有另外一个构造函数,一个有四个浮点参数的+functionWithControlPoints::::
(注意这里奇怪的语法,并无包含具体每一个参数的名称,这在objective-C中是合法的,可是却违反了苹果对方法命名的指导方针,并且看起来是一个奇怪的设计)。github
使用这个方法,咱们能够建立一个自定义的缓冲函数,来匹配咱们的时钟动画,为了理解如何使用这个方法,咱们要了解一些CAMediaTimingFunction
是如何工做的。编程
CAMediaTimingFunction
函数的主要原则在于它把输入的时间转换成起点和终点之间成比例的改变。咱们能够用一个简单的图标来解释,横轴表明时间,纵轴表明改变的量,因而线性的缓冲就是一条从起点开始的简单的斜线(图10.1)。app
图10.1 线性缓冲函数的图像编程语言
这条曲线的斜率表明了速度,斜率的改变表明了加速度,原则上来讲,任何加速的曲线均可以用这种图像来表示,可是CAMediaTimingFunction
使用了一个叫作三次贝塞尔曲线的函数,它只能够产出指定缓冲函数的子集(咱们以前在第八章中建立CAKeyframeAnimation
路径的时候提到过三次贝塞尔曲线)。ide
你或许会回想起,一个三次贝塞尔曲线经过四个点来定义,第一个和最后一个点表明了曲线的起点和终点,剩下中间两个点叫作控制点,由于它们控制了曲线的形状,贝塞尔曲线的控制点实际上是位于曲线以外的点,也就是说曲线并不必定要穿过它们。你能够把它们想象成吸引通过它们曲线的磁铁。函数
图10.2展现了一个三次贝塞尔缓冲函数的例子动画
图10.2 三次贝塞尔缓冲函数atom
实际上它是一个很奇怪的函数,先加速,而后减速,最后快到达终点的时候又加速,那么标准的缓冲函数又该如何用图像来表示呢?spa
CAMediaTimingFunction
有一个叫作-getControlPointAtIndex:values:
的方法,能够用来检索曲线的点,这个方法的设计的确有点奇怪(或许也就只有苹果能回答为何不简单返回一个CGPoint
),可是使用它咱们能够找到标准缓冲函数的点,而后用UIBezierPath
和CAShapeLayer
来把它画出来。
曲线的起始和终点始终是{0, 0}和{1, 1},因而咱们只须要检索曲线的第二个和第三个点(控制点)。具体代码见清单10.4。全部的标准缓冲函数的图像见图10.3。
清单10.4 使用UIBezierPath
绘制CAMediaTimingFunction
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *layerView;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; //create timing function CAMediaTimingFunction *function = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut]; //get control points CGPoint controlPoint1, controlPoint2; [function getControlPointAtIndex:1 values:(float *)&controlPoint1]; [function getControlPointAtIndex:2 values:(float *)&controlPoint2]; //create curve UIBezierPath *path = [[UIBezierPath alloc] init]; [path moveToPoint:CGPointZero]; [path addCurveToPoint:CGPointMake(1, 1) controlPoint1:controlPoint1 controlPoint2:controlPoint2]; //scale the path up to a reasonable size for display [path applyTransform:CGAffineTransformMakeScale(200, 200)]; //create shape layer CAShapeLayer *shapeLayer = [CAShapeLayer layer]; shapeLayer.strokeColor = [UIColor redColor].CGColor; shapeLayer.fillColor = [UIColor clearColor].CGColor; shapeLayer.lineWidth = 4.0f; shapeLayer.path = path.CGPath; [self.layerView.layer addSublayer:shapeLayer]; //flip geometry so that 0,0 is in the bottom-left self.layerView.layer.geometryFlipped = YES; }@end
图10.3 标准CAMediaTimingFunction
缓冲曲线
那么对于咱们自定义时钟指针的缓冲函数来讲,咱们须要初始微弱,而后迅速上升,最后缓冲到终点的曲线,经过一些实验以后,最终结果以下:
[CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];
若是把它转换成缓冲函数的图像,最后如图10.4所示,若是把它添加到时钟的程序,就造成了以前一直期待的很是赞的效果(见代清单10.5)。
图10.4 自定义适合时钟的缓冲函数
清单10.5 添加了自定义缓冲函数的时钟程序
- (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated { //generate transform CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1); if (animated) { //create transform animation CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"transform"; animation.fromValue = [handView.layer.presentationLayer valueForKey:@"transform"]; animation.toValue = [NSValue valueWithCATransform3D:transform]; animation.duration = 0.5; animation.delegate = self; animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1]; //apply animation handView.layer.transform = transform; [handView.layer addAnimation:animation forKey:nil]; } else { //set transform directly handView.layer.transform = transform; } }
考虑一个橡胶球掉落到坚硬的地面的场景,当开始下落的时候,它会持续加速知道落到地面,而后通过几回反弹,最后停下来。若是用一张图来讲明,它会如图10.5所示。
图10.5 一个无法用三次贝塞尔曲线描述的反弹的动画
这种效果无法用一个简单的三次贝塞尔曲线表示,因而不能用CAMediaTimingFunction
来完成。但若是想要实现这样的效果,能够用以下几种方法:
用CAKeyframeAnimation
建立一个动画,而后分割成几个步骤,每一个小步骤使用本身的计时函数(具体下节介绍)。
使用定时器逐帧更新实现动画(见第11章,“基于定时器的动画”)。
为了使用关键帧实现反弹动画,咱们须要在缓冲曲线中对每个显著的点建立一个关键帧(在这个状况下,关键点也就是每次反弹的峰值),而后应用缓冲函数把每段曲线链接起来。同时,咱们也须要经过keyTimes
来指定每一个关键帧的时间偏移,因为每次反弹的时间都会减小,因而关键帧并不会均匀分布。
清单10.6展现了实现反弹球动画的代码(见图10.6)
清单10.6 使用关键帧实现反弹球的动画
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic, strong) UIImageView *ballView;@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]; }- (void)animate { //reset ball to top of screen self.ballView.center = CGPointMake(150, 32); //create keyframe animation CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; animation.keyPath = @"position"; animation.duration = 1.0; animation.delegate = self; animation.values = @[ [NSValue valueWithCGPoint:CGPointMake(150, 32)], [NSValue valueWithCGPoint:CGPointMake(150, 268)], [NSValue valueWithCGPoint:CGPointMake(150, 140)], [NSValue valueWithCGPoint:CGPointMake(150, 268)], [NSValue valueWithCGPoint:CGPointMake(150, 220)], [NSValue valueWithCGPoint:CGPointMake(150, 268)], [NSValue valueWithCGPoint:CGPointMake(150, 250)], [NSValue valueWithCGPoint:CGPointMake(150, 268)] ]; animation.timingFunctions = @[ [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn], [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut], [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn], [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut], [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn], [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut], [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn] ]; animation.keyTimes = @[@0.0, @0.3, @0.5, @0.7, @0.8, @0.9, @0.95, @1.0]; //apply animation self.ballView.layer.position = CGPointMake(150, 268); [self.ballView.layer addAnimation:animation forKey:nil]; }@end
图10.6 使用关键帧实现的反弹球动画
这种方式还算不错,可是实现起来略显笨重(由于要不停地尝试计算各类关键帧和时间偏移)而且和动画强绑定了(由于若是要改变更画的一个属性,那就意味着要从新计算全部的关键帧)。那该如何写一个方法,用缓冲函数来把任何简单的属性动画转换成关键帧动画呢,下面咱们来实现它。
在清单10.6中,咱们把动画分割成至关大的几块,而后用Core Animation的缓冲进入和缓冲退出函数来大约造成咱们想要的曲线。但若是咱们把动画分割成更小的几部分,那么咱们就能够用直线来拼接这些曲线(也就是线性缓冲)。为了实现自动化,咱们须要知道如何作以下两件事情:
自动把任意属性动画分割成多个关键帧
用一个数学函数表示弹性动画,使得能够对帧作便宜
为了解决第一个问题,咱们须要复制Core Animation的插值机制。这是一个传入起点和终点,而后在这两个点之间指定时间点产出一个新点的机制。对于简单的浮点起始值,公式以下(假设时间从0到1):
value = (endValue – startValue) × time + startValue;
那么若是要插入一个相似于CGPoint
,CGColorRef
或者CATransform3D
这种更加复杂类型的值,咱们能够简单地对每一个独立的元素应用这个方法(也就CGPoint
中的x和y值,CGColorRef
中的红,蓝,绿,透明值,或者是CATransform3D
中独立矩阵的坐标)。咱们一样须要一些逻辑在插值以前对对象拆解值,而后在插值以后在从新封装成对象,也就是说须要实时地检查类型。
一旦咱们能够用代码获取属性动画的起始值之间的任意插值,咱们就能够把动画分割成许多独立的关键帧,而后产出一个线性的关键帧动画。清单10.7展现了相关代码。
注意到咱们用了60 x 动画时间(秒作单位)做为关键帧的个数,这时由于Core Animation按照每秒60帧去渲染屏幕更新,因此若是咱们每秒生成60个关键帧,就能够保证动画足够的平滑(尽管实际上极可能用更少的帧率就能够达到很好的效果)。
咱们在示例中仅仅引入了对CGPoint
类型的插值代码。可是,从代码中很清楚能看出如何扩展成支持别的类型。做为不能识别类型的备选方案,咱们仅仅在前一半返回了fromValue
,在后一半返回了toValue
。
清单10.7 使用插入的值建立一个关键帧动画
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 = [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); //set up animation parameters NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; CFTimeInterval duration = 1.0; //generate keyframes NSInteger numFrames = duration * 60; NSMutableArray *frames = [NSMutableArray array]; for (int i = 0; i < numFrames; i++) { float time = 1 / (float)numFrames * i; [frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]]; } //create keyframe animation CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; animation.keyPath = @"position"; animation.duration = 1.0; animation.delegate = self; animation.values = frames; //apply animation [self.ballView.layer addAnimation:animation forKey:nil]; }
这能够起到做用,但效果并非很好,到目前为止咱们所完成的只是一个很是复杂的方式来使用线性缓冲复制CABasicAnimation
的行为。这种方式的好处在于咱们能够更加精确地控制缓冲,这也意味着咱们能够应用一个彻底定制的缓冲函数。那么该如何作呢?
缓冲背后的数学并不很简单,可是幸运的是咱们不须要一一实现它。罗伯特·彭纳有一个网页关于缓冲函数(http://www.robertpenner.com/easing),包含了大多数广泛的缓冲函数的多种编程语言的实现的连接,包括C。这里是一个缓冲进入缓冲退出函数的示例(实际上有不少不一样的方式去实现它)。
float quadraticEaseInOut(float t) { return (t < 0.5)? (2 * t * t): (-2 * t * t) + (4 * t) - 1; }
对咱们的弹性球来讲,咱们可使用bounceEaseOut
函数:
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; }
若是修改清单10.7的代码来引入bounceEaseOut
方法,咱们的任务就是仅仅交换缓冲函数,如今就能够选择任意的缓冲类型建立动画了(见清单10.8)。
清单10.8 用关键帧实现自定义的缓冲函数
- (void)animate { //reset ball to top of screen self.ballView.center = CGPointMake(150, 32); //set up animation parameters NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; CFTimeInterval duration = 1.0; //generate keyframes NSInteger numFrames = duration * 60; NSMutableArray *frames = [NSMutableArray array]; for (int i = 0; i < numFrames; i++) { float time = 1/(float)numFrames * i; //apply easing time = bounceEaseOut(time); //add keyframe [frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]]; } //create keyframe animation CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; animation.keyPath = @"position"; animation.duration = 1.0; animation.delegate = self; animation.values = frames; //apply animation [self.ballView.layer addAnimation:animation forKey:nil]; }
在这一章中,咱们了解了有关缓冲和CAMediaTimingFunction
类,它能够容许咱们建立自定义的缓冲函数来完善咱们的动画,一样了解了如何用CAKeyframeAnimation
来避开CAMediaTimingFunction
的限制,建立彻底自定义的缓冲函数。