这一章虽然叫做动画时间控制,然而我们并不会去深入到一般的动画时间中,我们将讨论的是CoreAnimation框架是如何来控制时间的。
这一章的大部分内容来自http://ronnqvi.st/controlling-animation-timing/,大家可以看看英文原版来加深理解,毕竟翻译能力有限。
动画所有跟时间相关的属性(duration, beginTime, repeatCount等)都来自于CAMediaTiming协议,它由CABasicAnimation和CAKeyframeAnimation的父类CAAnimation实现。协议一共定义了8个属性,通过这8个属性就能完全地控制动画时间。每个属性的文档只有短短几句话,当然你也可以通过阅读这些文档并且手动进行试验来进行学习,不过我认为更容易让人理解的方式是将时间可视化。
为了向你们展示不同的时间相关属性,包括这个属性自己单独使用的效果以及和其他属性混合使用的效果,我将执行一个从橘黄色到蓝色转换的动画。下图展示了从动画开始到动画结束的进程(橘色到蓝色),每一格代表一秒,时间线上任意一点对应到图上的颜色就是视图在这一瞬间的颜色。比如,duration这个属性将被如下进行可视化展示:
duration设置为1.5秒,所以动画将耗费1秒加上1秒的一半来从橘色完全变为蓝色。
图一. 将duration设为1.5秒
默认地,CAAnimation将会在动画完成后被移除,这在上面同样被可视化出来了,一旦动画到达了结束值,它就会被从layer上移除,所以layer的背景色将会返回到modelLayer的状态(见上一章:CALayer的模型层与展示层)。在这个可视化例子中,layer本身的背景色是白色,所以你看到的上图的可视化效果中,在1.5秒后的额外的2.5秒钟的时间里layer的背景色回到了白色。
如果我们将动画的beginTime加入到可视化效果中就能看到更多的情形。
图二. 将duration设为1.5秒,将开始时间设为1.0秒
将动画持续时间设为1.5秒,开始时间设为当前时间(CACurrentMediaTime())加上1秒所以动画将在2.5秒后结束。在动画被加到layer上之后它将等待1秒然后再开始(相当于动画延迟时间为1秒)。
如果要让动画在开始之前(延迟的这段时间内)显示fromValue的状态,你可以设置动画向后填充:设置fillMode为kCAFillModeBackwards。
图三、填充模式可以用来在动画开始之前显示fromValue。
将使动画先正常走,完了以后反着从结束值回到起始值(所有动画属性都会反过来,比如动画速度,如果正常的是先快后慢,则反过来后变成先慢后快)。
图四. Autoreverse将使动画在结束后又回到动画开始的状态。
相比之下,repeatCount可以让动画重复执行两次(首次动画结束后再执行一次,正如你下面将看到的)或者任意多次(你甚至可以将重复次数设置为小数,比如设置为1.5,这样第二次动画只执行到一半)。一旦动画到达结束值,它将立即返回到起始值并且重新开始。
图五、repeatCount让动画在结束后再次执行
类似于repeatCount,但是极少会使用。它简单地在给定的持续时间内重复执行动画(下面设置为2秒)。如果repeatDuration比动画持续时间小,那么动画将提前结束(repeatDuration到达后就结束)
图六. repeatDuration使动画在给定时间内不停重复播放
这些属性可以结合到一起使用来实现一些动画多次重复反向(也可以在制定时间内重复反向)的效果。如下图所示:
图七. 把几个属性结合起来用
这是一个非常有意思的时间相关的属性。如果把动画的duration设置为3秒,而speed设置为2,动画将会在1.5秒结束,因为它以两倍速在执行。
图八. Speed设置为2将会使动画的速度变为2倍所以3秒的动画将只用1.5秒就能执行完
speed属性的强大之处来自以下两个特点:
1、 动画速度是有层级关系的
2、 CAAnimation并不是唯一的实现了CAMediaTiming协议的类。
一个动画的speed为1.5,它同时是一个speed为2的动画组的一个动画成员,则它将以3倍速度被执行。
CAAnimation实现了CAMediaTiming协议,然而CoreAnimation最基本的类:CALayer也实现了CAMediaTiming协议。这意味着你可以给一个CALayer设置speed为2,那么所有加到它上面的动画都将以两倍速执行。这同样符合动画时间层级,比如你把一个speed为3的动画加到一个speed为0.5的layer上,则这个动画将以1.5倍速度执行。
控制动画和layer的速度同样可以用来暂停动画,你只需要把speed设为0就行了。结合timeOffset属性,就可以通过一个外部的控制器(比如一个UISlider)来控制动画了,我们将在这一章较后的内容中进行讲解。
timeOffset这个属性啊,一开始看起来挺奇怪的,光看名字的话,看起来应该是一个用来控制动画时间进程(计算动画当前状态)的属性。下面这个可视化展示了一个持续时间为3秒,动画时间偏移量为1秒的动画。
图9.你可以偏移整个动画但是动画还是会走完全部过程。
这个动画将从正常动画(timeOffset为0的状态)的第一秒开始执行,直到两秒后它完全变蓝,然后它一下子跳回最开始的状态(橙色)再执行一秒。就像是我们把正常动画的第一秒给剪下来粘贴到动画最后一样。
这个属性实际上并不会自己单独使用,而会结合一个暂停动画(speed=0)一起使用来控制动画的“当前时间”。暂停的动画将会在第一帧卡住,然后通过改变timeOffset来随意控制动画进程,因为如上图所示,动画的第一帧就是timeOffset指定的那一帧。
举个例子:比如一个改变位置的动画,让一个视图从(0,0)移动到(100,100),持续时间为1秒。如果先暂停动画,然后设置timeOffset为0.5,那么首先动画会卡在“第一帧”,而第一帧由timeOffset决定,也就是动画正常运作时间进行到0.5秒的那一帧就是“第一帧”,这时候动画就会停在(50,50)的地方。注意timeOffset是具体的秒数而不是百分比。
结合使用speed和timeOffset属性可以轻松控制一个动画当前显示的内容。为了方便起见,我将把动画持续时间设为1秒。timeOffset/duration这个分数的值表示动画进行的百分比,把duration设为1的话,在数值上timeOffset就等于动画进程百分比了。
我们首先创建一个CABasicAnimation来创建一个改变layer背景颜色的动画并把它添加到layer上,然后把layer的speed属性设为0来暂停动画。
CABasicAnimation *changeColor =
[CABasicAnimation animationWithKeyPath:@"backgroundColor"];
changeColor.fromValue = (id)[UIColor orangeColor].CGColor;
changeColor.toValue = (id)[UIColor blueColor].CGColor;
changeColor.duration = 1.0; // For convenience
[self.myLayer addAnimation:changeColor
forKey:@"Change color"];
self.myLayer.speed = 0.0; // Pause the animation
然后在slider被拖动的action方法中,我们把slider的当前值(默认0到1,刚好也是动画timeOffset的范围)设为layer的timeOffset的值。
- (IBAction)sliderChanged:(UISlider *)sender {
self.myLayer.timeOffset = sender.value; // Update "current time"
}
这样的效果就像我们通过拖动一个slider来改变一个layer的背景颜色。
以上对于fillMode的说明比较简单,而且在动画的时间控制中还有一个比较重要的类:CAMediaTimingFunction,将在这里比较详细为大家进行讲解
这个概念如果用文字来描述是比较难理解的,所以我将启用我的灵魂画板来讲解这个概念。
为了更好的理解,我们首先定义四个时间点:t0表示动画被加到layer上的一刻;t1表示动画开始的一刻;t2表示动画结束的一刻;t3表示动画从layer上移除的一刻(这四个时间点也可以叫做动画的生命周期)。
这里有少年可能会问,呐,动画加到layer上不就是动画开始的时候么?你忘记考虑延迟了!如果没有延迟,那么t0和t1确实是同一个时刻,但是一旦动画有了延迟,那么t0和t1就相差一个延迟了。同样的道理,默认情况下,动画一旦结束就会从layer上自动移除,也就是默认情况下t2和t3也是同一时刻,但是如果我们设置了removedOnCompletion = false,那么t3就会无限向前延伸直到我们手动调用layer的removeAnimation方法。
我们来写一个简单的动画,比如修改透明度的动画。modelLayer的属性一开始是0.5,然后我们写一个动画把透明度从0修改为1,持续时间1秒并设置一个1秒的延迟(t0-t1、t1到t2都相差1秒),动画结束后不立即移除动画。
如果你想在视图一出现就开始动画,请把动画写到viewDidAppear里面,不要写到viewDidLoad里面。
- (void)viewDidAppear:(BOOL)animated
{
CALayer * layer = [CALayer layer];
layer.frame = CGRectMake(100, 100, 200, 200);
layer.opacity = 0.5;
layer.backgroundColor = [UIColor yellowColor].CGColor;
[self.view.layer addSublayer:layer];
CABasicAnimation * animation = [CABasicAnimation animation];
animation.keyPath = @"opacity";
animation.fromValue = @0;
animation.toValue = @1;
animation.duration = 1;
animation.beginTime = CACurrentMediaTime() + 1;
animation.removedOnCompletion = false;
[layer addAnimation:animation forKey:@"opacity"];
}
好了,那么我们启用灵魂画板来表示动画过程中P(presentationLayer)和M(modelLayer)的属性的值。在灵魂画板中,因为0.5这个数字画起来太麻烦了,我就同时扩大两倍,在灵魂画板中的1就是0.5,2就是1。根据P和M的规则,M肯定在整个过程中的值都是0.5(在灵魂画板中表现为1),而P在t0到t1由于动画并没有被告知如何影响P,所以会保持M的状态也就是0.5,然后在t1到t2(动画开始到动画结束)从0到1进行插值,到了t2动画结束,此时动画不知道如何影响P,所以P保持M的状态也就是回到0.5:
这是一般情况,大家可以运行看一下结果:在开始的一秒(t0-t1:延迟的过程)整个layer是一个透明度为0.5的状态,延迟结束,开始动画(t1-t2),这一秒中layer会一下子闪到透明度为0的状态然后动画的变到透明度为1的状态。动画结束后(t2-t3),P回到M的状态,变为0.5的透明度状态。
现在我们加上fillMode
我们设置fillMode为向前填充:
animation.fillMode = kCAFillModeForwards;
这里有两个概念:向前、填充。
什么是向前,向前就是朝着时间的正方向。
那么填充又是什么呢?因为t2到t3这段时间动画并不知道如何影响P,所以对于这段时间来讲,P的状态应该是“空”的,如果是空,那么P就会保持M的状态。而填充,就是把P的这些“空”的状态用具体的值填起来。由于我们的动画的keypath是opacity,所以就会对P的opacity在t2-t3这段时间进行填充,而填充的规则是“向前”,也就是“t2向t3填充”,说直白一点,就是t2到t3这个时间段P的opacity的值就一直保持t2的时候P的opacity的值,实际上就是动画的toValue的值。
在灵魂画板中体现为:
这样的话,直到动画被移除,P都会保持toValue也就是透明度为1的状态,效果就是动画结束后不会闪回动画开始之前的那个状态而保持结束值的状态。
这样一来,向后填充就比较好理解了:
向后填充是t0到t1,由于向后是时间的负方向,所以就是P的状态在t0到t1这段时间由t1向t0填充,也就是t0到t1的时间段P保持t1时刻的状态也就是fromValue的状态。这样设置的效果就是在延迟的时间里面P保持fromValue的状态,就避免了动画一开始P从M的状态就闪到fromValue的状态:
animation.fillMode = kCAFillModeBackwards;
在灵魂画板中表现为:
如果既想向前填充又想向后填充,那么久把fillMode设置为both:
animation.fillMode = kCAFillModeBoth;
这样设置后在灵魂画板中表现为:
关于ease效果,在CAAnimation中表现为timingFunction这个属性,它需要设置一个CAMediaTimingFunction对象,实际上是指定了一个曲线,作为s-t函数图像,s是竖轴,代表动画的进程,0表示动画开始,1表示动画结束;t是横轴,代表动画当前的时间,0表示开始的时候,1表示结束的时候。曲线上一点的切线斜率表示这一时刻的动画速度。可以高中物理的直线运动的位移-时间图像。
系统自带了几种ease效果,可以通过代码来实现,使用functionWithName:这个类方法来通过函数名字来指定函数曲线:
CA_EXTERN NSString * const kCAMediaTimingFunctionLinear
__OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseIn
__OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseOut
__OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseInEaseOut
__OSX_AVAILABLE_STARTING (__MAC_10_5, __IPHONE_2_0);
CA_EXTERN NSString * const kCAMediaTimingFunctionDefault
__OSX_AVAILABLE_STARTING (__MAC_10_6, __IPHONE_3_0);
每个名字的函数图像在网上随处可见,如果简单的来理解,那么Linear就表示线性的,也就是s-t图像是一条直线,明显就是匀速运动了;EaseIn表示淡入,也就是匀加速启动,或者理解为先慢后快;EaseOut表示淡出,也就是匀减速停止,或者理解为先快后慢。EaseInEaseOut就是既有淡入效果也有淡出效果。Default是一种平滑启动平滑结束的过程,类似EaseInEaseOut,但是效果没那么显著。
除了functionWithName,系统允许我们使用一条贝塞尔曲线作为函数图像:
+(instancetype)functionWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y;
这个方法有四个参数,前两个参数表示贝塞尔曲线的第一个控制点,后两个参数表示贝塞尔曲线的第二个控制点。起点是(0,0)而终点是(1,1),所以是一条三阶贝塞尔曲线。注意到函数的定义域和值域都是[0,1],所以控制点的x和y的值要计算好。关于贝塞尔曲线如果不太了解,可以在维基百科这里获得公式和表现形式:http://en.wikipedia.org/wiki/Bezier_curve#Generalization
一般来讲系统自带的函数图像已经够用了,如果想要一些奇怪的效果,比如很慢很慢的启动,然后一瞬间加速到很快,就要自己去用贝塞尔曲线来控制了(大概是这样一种图像:╯)。
好了,动画原理部分的内容就到这里为止,希望大家能熟练掌握这些知识,它们能帮助大家理解很多一些效果是怎样产生的,以及一些问题出现的原因,更重要的是能帮助大家在接下来的章节中了解一些高级技巧是如何使用的。
这一章的实践内容比较少,所以需要自己花实践消化并且做充分的实验。
下一章就将进入高级动画技巧,如何使用CoreAnimation提供的简单API来组合实现各种酷炫的效果,大家除了能学到技术以外,还能学到一些思想,掌握了这些思想,以后遇到各种效果的动画都能有思路去实现了,这才是我写这篇专题的目的。