时间和空间最大的区别在于,时间不能被复用 -- 弗斯特梅里克面试
在上面两章中,咱们探讨了能够用CAAnimation
和它的子类实现的多种图层动画。动画的发生是须要持续一段时间的,因此计时对整个概念来讲相当重要。在这一章中,咱们来看看CAMediaTiming
,看看Core Animation是如何跟踪时间的。编程
CAMediaTiming
协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayer
和CAAnimation
都实现了这个协议,因此时间能够被任意基于一个图层或者一段动画的类控制。数组
咱们在第八章“显式动画”中简单提到过duration(CAMediaTiming
的属性之一),duration
是一个CFTimeInterval
的类型(相似于NSTimeInterval
的一种双精度浮点类型),对将要进行的动画的一次迭代指定了时间。app
这里的一次迭代是什么意思呢?CAMediaTiming
另外还有一个属性叫作repeatCount
,表明动画重复的迭代次数。若是duration
是2,repeatCount
设为3.5(三个半迭代),那么完整的动画时长将是7秒。编程语言
一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个个人iOS交流群:1012951431, 分享BAT,阿里面试题、面试经验,讨论技术, 你们一块儿交流学习成长!但愿帮助开发者少走弯路。ide
duration
和repeatCount
默认都是0。但这不意味着动画时长为0秒,或者0次,这里的0仅仅表明了“默认”,也就是0.25秒和1次,你能够用一个简单的测试来尝试为这两个属性赋多个值,如清单9.1,图9.1展现了程序的结果。函数
清单9.1 测试duration
和repeatCount
学习
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic, weak) IBOutlet UITextField *durationField; @property (nonatomic, weak) IBOutlet UITextField *repeatField; @property (nonatomic, weak) IBOutlet UIButton *startButton; @property (nonatomic, strong) CALayer *shipLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //add the ship self.shipLayer = [CALayer layer]; self.shipLayer.frame = CGRectMake(0, 0, 128, 128); self.shipLayer.position = CGPointMake(150, 150); self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage; [self.containerView.layer addSublayer:self.shipLayer]; } - (void)setControlsEnabled:(BOOL)enabled { for (UIControl *control in @[self.durationField, self.repeatField, self.startButton]) { control.enabled = enabled; control.alpha = enabled? 1.0f: 0.25f; } } - (IBAction)hideKeyboard { [self.durationField resignFirstResponder]; [self.repeatField resignFirstResponder]; } - (IBAction)start { CFTimeInterval duration = [self.durationField.text doubleValue]; float repeatCount = [self.repeatField.text floatValue]; //animate the ship rotation CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"transform.rotation"; animation.duration = duration; animation.repeatCount = repeatCount; animation.byValue = @(M_PI * 2); animation.delegate = self; [self.shipLayer addAnimation:animation forKey:@"rotateAnimation"]; //disable controls [self setControlsEnabled:NO]; } - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { //reenable controls [self setControlsEnabled:YES]; } @end
图9.2 摆动门的动画测试
对门进行摆动的代码见清单9.2。咱们用了autoreverses
来使门在打开后自动关闭,在这里咱们把repeatDuration
设置为INFINITY
,因而动画无限循环播放,设置repeatCount
为INFINITY
也有一样的效果。注意repeatCount和repeatDuration
可能会相互冲突,因此你只要对其中一个指定非零值。对两个属性都设置非0值的行为没有被定义。动画
清单9.2 使用autoreverses属性实现门的摇摆
@interface ViewController () @property (nonatomic, weak) UIView *containerView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //add the door CALayer *doorLayer = [CALayer layer]; doorLayer.frame = CGRectMake(0, 0, 128, 256); doorLayer.position = CGPointMake(150 - 64, 150); doorLayer.anchorPoint = CGPointMake(0, 0.5); doorLayer.contents = (__bridge id)[UIImage imageNamed: @"Door.png"].CGImage; [self.containerView.layer addSublayer:doorLayer]; //apply perspective transform CATransform3D perspective = CATransform3DIdentity; perspective.m34 = -1.0 / 500.0; self.containerView.layer.sublayerTransform = perspective; //apply swinging animation CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"transform.rotation.y"; animation.toValue = @(-M_PI_2); animation.duration = 2.0; animation.repeatDuration = INFINITY; animation.autoreverses = YES; [doorLayer addAnimation:animation forKey:nil]; } @end
每次讨论到Core Animation,时间都是相对的,每一个动画都有它本身描述的时间,能够独立地加速,延时或者偏移。
beginTime
指定了动画开始以前的的延迟时间。这里的延迟从动画添加到可见图层的那一刻开始测量,默认是0(就是说动画会马上执行)。
speed
是一个时间的倍数,默认1.0,减小它会减慢图层/动画的时间,增长它会加快速度。若是2.0的速度,那么对于一个duration
为1的动画,实际上在0.5秒的时候就已经完成了。
timeOffset
和beginTime
相似,可是和增长beginTime
致使的延迟动画不一样,增长timeOffset
只是让动画快进到某一点,例如,对于一个持续1秒的动画来讲,设置timeOffset
为0.5意味着动画将从一半的地方开始。
和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
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用来操做时间控制动画的机制。在下一章,咱们将要接触缓冲
,另外一个用来使动画更加真实的操做时间的技术。
生活和艺术同样,最美的永远是曲线。 -- 爱德华布尔沃 - 利顿
在第九章“图层时间”中,咱们讨论了动画时间和CAMediaTiming
协议。如今咱们来看一下另外一个和时间相关的机制--所谓的缓冲。Core Animation使用缓冲来使动画移动更平滑更天然,而不是看起来的那种机械和人工,在这一章咱们将要研究如何对你的动画控制和自定义缓冲曲线。
动画实际上就是一段时间内的变化,这就暗示了变化必定是随着某个特定的速率进行。速率由如下公式计算而来:
velocity = change / time
这里的变化能够指的是一个物体移动的距离,时间指动画持续的时长,用这样的一个移动能够更加形象的描述(好比position
和bounds
属性的动画),但实际上它应用于任意能够作动画的属性(好比color
和opacity
)。
上面的等式假设了速度在整个动画过程当中都是恒定不变的(就如同第八章“显式动画”的状况),对于这种恒定速度的动画咱们称之为“线性步调”,并且从技术的角度而言这也是实现动画最简单的方式,但也是彻底不真实的一种效果。
考虑一个场景,一辆车行驶在必定距离内,它并不会一开始就以60mph的速度行驶,而后到达终点后忽然变成0mph。一是由于须要无限大的加速度(即便是最好的车也不会在0秒内从0跑到60),另外否则的话会干死全部乘客。在现实中,它会慢慢地加速到全速,而后当它接近终点的时候,它会慢慢地减速,直到最后停下来。
那么对于一个掉落到地上的物体又会怎样呢?它会首先停在空中,而后一直加速到落到地面,而后忽然中止(而后因为积累的动能转换伴随着一声巨响,砰!)。
现实生活中的任何一个物体都会在运动中加速或者减速。那么咱们如何在动画中实现这种加速度呢?一种方法是使用物理引擎来对运动物体的摩擦和动量来建模,然而这会使得计算过于复杂。咱们称这种类型的方程为缓冲函数,幸运的是,Core Animation内嵌了一系列标准函数提供给咱们使用。
那么该如何使用缓冲方程式呢?首先须要设置CAAnimation的timingFunction属性,是CAMediaTimingFunction
类的一个对象。若是想改变隐式动画的计时函数,一样也可使用CATransaction
的+setAnimationTimingFunction:
方法。
这里有一些方式来建立CAMediaTimingFunction
,最简单的方式是调用+timingFunctionWithName:
的构造方法。这里传入以下几个常量之一:
kCAMediaTimingFunctionLinear
kCAMediaTimingFunctionEaseIn
kCAMediaTimingFunctionEaseOut
kCAMediaTimingFunctionEaseInEaseOut
kCAMediaTimingFunctionDefault
kCAMediaTimingFunctionLinear
选项建立了一个线性的计时函数,一样也是CAAnimation的timingFunction
属性为空时候的默认函数。线性步调对于那些当即加速而且保持匀速到达终点的场景会有意义(例如射出枪膛的子弹),可是默认来讲它看起来很奇怪,由于对大多数的动画来讲确实不多用到。
kCAMediaTimingFunctionEaseIn
常量建立了一个慢慢加速而后忽然中止的方法。对于以前提到的自由落体的例子来讲很适合,或者好比对准一个目标的导弹的发射。
kCAMediaTimingFunctionEaseOut
则偏偏相反,它以一个全速开始,而后慢慢减速中止。它有一个削弱的效果,应用的场景好比一扇门慢慢地关上,而不是砰地一声。
kCAMediaTimingFunctionEaseInEaseOut
建立了一个慢慢加速而后再慢慢减速的过程。这是现实世界大多数物体移动的方式,也是大多数动画来讲最好的选择。若是只能够用一种缓冲函数的话,那就必须是它了。那么你会疑惑为何这不是默认的选择,实际上当使用UIView的动画方法时,他的确是默认的,但当建立CAAnimation
的时候,就须要手动设置它了。
最后还有一个kCAMediaTimingFunctionDefault
,它和kCAMediaTimingFunctionEaseInEaseOut
很相似,可是加速和减速的过程都稍微有些慢。它和kCAMediaTimingFunctionEaseInEaseOut
的区别很难察觉,多是苹果以为它对于隐式动画来讲更适合(而后对UIKit就改变了想法,而是使用kCAMediaTimingFunctionEaseInEaseOut
做为默认效果),虽然它的名字说是默认的,但仍是要记住当建立显式的CAAnimation
它并非默认选项(换句话说,默认的图层行为动画用kCAMediaTimingFunctionDefault
做为它们的计时方法)。
你可使用一个简单的测试工程来实验一下(清单10.1),在运行以前改变缓冲函数的代码,而后点击任何地方来观察图层是如何经过指定的缓冲移动的。
清单10.1 缓冲函数的简单测试
@interface ViewController () @property (nonatomic, strong) CALayer *colorLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //create a red layer self.colorLayer = [CALayer layer]; self.colorLayer.frame = CGRectMake(0, 0, 100, 100); self.colorLayer.position = CGPointMake(self.view.bounds.size.width/2.0, self.view.bounds.size.height/2.0); self.colorLayer.backgroundColor = [UIColor redColor].CGColor; [self.view.layer addSublayer:self.colorLayer]; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { //configure the transaction [CATransaction begin]; [CATransaction setAnimationDuration:1.0]; [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]]; //set the position self.colorLayer.position = [[touches anyObject] locationInView:self.view]; //commit transaction [CATransaction commit]; } @end
UIKit的动画也一样支持这些缓冲方法的使用,尽管语法和常量有些不一样,为了改变UIView动画的缓冲选项,给options参数添加以下常量之一:
UIViewAnimationOptionCurveEaseInOut
UIViewAnimationOptionCurveEaseIn
UIViewAnimationOptionCurveEaseOut
UIViewAnimationOptionCurveLinear
它们和CAMediaTimingFunction
紧密关联,UIViewAnimationOptionCurveEaseInOut
是默认值(这里没有kCAMediaTimingFunctionDefault
相对应的值了)。
具体使用方法见清单10.2(注意到这里再也不使用UIView
额外添加的图层,由于UIKit的动画并不支持这类图层)。
清单10.2 使用UIKit动画的缓冲测试工程
@interface ViewController () @property (nonatomic, strong) UIView *colorView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //create a red layer self.colorView = [[UIView alloc] init]; self.colorView.bounds = CGRectMake(0, 0, 100, 100); self.colorView.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2); self.colorView.backgroundColor = [UIColor redColor]; [self.view addSubview:self.colorView]; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { //perform the animation [UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{ //set the position self.colorView.center = [[touches anyObject] locationInView:self.view]; } completion:NULL]; } @end
或许你会回想起第八章里面颜色切换的关键帧动画因为线性变换的缘由(见清单8.5)看起来有些奇怪,使得颜色变换很是不天然。为了纠正这点,咱们来用更加合适的缓冲方法,例如kCAMediaTimingFunctionEaseIn
,给图层的颜色变化添加一点脉冲效果,让它更像现实中的一个彩色灯泡。
咱们不想给整个动画过程应用这个效果,咱们但愿对每一个动画的过程重复这样的缓冲,因而每次颜色的变换都会有脉冲效果。
CAKeyframeAnimation
有一个NSArray
类型的timingFunctions
属性,咱们能够用它来对每次动画的步骤指定不一样的计时函数。可是指定函数的个数必定要等于keyframes数组的元素个数减一,由于它是描述每一帧之间动画速度的函数。
在这个例子中,咱们自始至终想使用同一个缓冲函数,但咱们一样须要一个函数的数组来告诉动画不停地重复每一个步骤,而不是在整个动画序列只作一次缓冲,咱们简单地使用包含多个相同函数拷贝的数组就能够了(见清单10.3)。
运行更新后的代码,你会发现动画看起来更加天然了。
清单10.3 对CAKeyframeAnimation
使用CAMediaTimingFunction
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *layerView; @property (nonatomic, weak) IBOutlet CALayer *colorLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //create sublayer self.colorLayer = [CALayer layer]; self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); self.colorLayer.backgroundColor = [UIColor blueColor].CGColor; //add it to our view [self.layerView.layer addSublayer:self.colorLayer]; } - (IBAction)changeColor { //create a keyframe animation CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; animation.keyPath = @"backgroundColor"; animation.duration = 2.0; animation.values = @[ (__bridge id)[UIColor blueColor].CGColor, (__bridge id)[UIColor redColor].CGColor, (__bridge id)[UIColor greenColor].CGColor, (__bridge id)[UIColor blueColor].CGColor ]; //add timing function CAMediaTimingFunction *fn = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn]; animation.timingFunctions = @[fn, fn, fn]; //apply animation to layer [self.colorLayer addAnimation:animation forKey:nil]; } @end
在第八章中,咱们给时钟项目添加了动画。看起来很赞,可是若是有合适的缓冲函数就更好了。在显示世界中,钟表指针转动的时候,一般起步很慢,而后迅速啪地一声,最后缓冲到终点。可是标准的缓冲函数在这里每个适合它,那该如何建立一个新的呢?
除了+functionWithName:
以外,CAMediaTimingFunction
一样有另外一个构造函数,一个有四个浮点参数的+functionWithControlPoints::::
(注意这里奇怪的语法,并无包含具体每一个参数的名称,这在objective-C中是合法的,可是却违反了苹果对方法命名的指导方针,并且看起来是一个奇怪的设计)。
使用这个方法,咱们能够建立一个自定义的缓冲函数,来匹配咱们的时钟动画,为了理解如何使用这个方法,咱们要了解一些CAMediaTimingFunction
是如何工做的。
CAMediaTimingFunction
函数的主要原则在于它把输入的时间转换成起点和终点之间成比例的改变。咱们能够用一个简单的图标来解释,横轴表明时间,纵轴表明改变的量,因而线性的缓冲就是一条从起点开始的简单的斜线(图10.1)。
图10.2 三次贝塞尔缓冲函数
实际上它是一个很奇怪的函数,先加速,而后减速,最后快到达终点的时候又加速,那么标准的缓冲函数又该如何用图像来表示呢?
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.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所示。
这能够起到做用,但效果并非很好,到目前为止咱们所完成的只是一个很是复杂的方式来使用线性缓冲复制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
的限制,建立彻底自定义的缓冲函数。
在下一章中,咱们将要研究基于定时器的动画--另外一个给咱们对动画更多控制的选择,而且实现对动画的实时操纵。