iOS Core Animation - Setting Things in Motion
前言
iOS Core Animation 读书笔记(二) Setting Things in Motion
笔记
Implicit Animations
隐式的动画由framework自动执行,除非你叫他不执行。
Transcations
Core Animation会去假设你所有创建在屏幕上的东西都会animation。所以animation并不用你刻意的去启动。这也是Core Animation比OpenGL慢的一点,OpenGL不用去考虑这些东西,就不需要花费额外的性能代价。
当你改变一个CALayer的属性时,它会自动平滑的执行动画从当前属性到新的属性,而不是瞬变。不需要你额外的做些什么。这种动画就叫做隐式动画。
Transcations是Core Animation将一系列动画汇集成一个的机制。一旦transaction提交(committed)之后,这些动画就会开始执行。
CATransaction类控制着transaction的行为。该类没有alloc init这样的初始化,也并不是如名字那样代表一个transaction,它表示一个transaction的栈。可以使用
+begin
和-commit
方法来将一个transaction进栈或者出栈。使用
+setAnimationDuration:
方法来设置当前transaction的动画时间。没有指定的话,默认是0.25s。使用该方法时,最好显示的开始一个新的transaction,将其压入栈中,表示只改变这个transaction的duration;不然可能改变同时发生的一些animation,比如旋转屏幕。看一下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16- (IBAction)changeColor {
//begin a new transaction
[CATransaction begin];
//set the animation duration to 1 second
[CATransaction setAnimationDuration:1.0];
//randomize the layer background color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
//commit the transaction
[CATransaction commit];
}这里使用
+begin
方法,表示显式的开始一个transaction,transaction其实就是一个属性改变的动画的集合;显式开始一个transaction可以精准的控制这一系列动画的执行时间等等,而且不予其他的动画搞混。当执行+commit
方法时,这些动画变开始执行。而对于一些没有使用begin,commit的动画,其实他们也是属于不同的transaction,只是这些transaction没有被显式的压入(begin)或推出(commit)transaction的数组而已;他们被隐式的执行罢了。其实UIView的
+animateWithDuration:animations:
方法,也是在内部调用上述6中的方法而已。UIView的
+beginAnimations:context:
和+commitAnimations
和CALayer的+begin
以及+commit
相同。
Completion Blocks
- UIView中的animation的completion block实际上就是调用CATransaction的
+setCompletionBlock:
方法。
Layer Actions
CALayer在属性变化时执行的动画叫做actions,来看看整个的流程。
- 当CALayer的一个属性变化时,调用
-actionsForKey:
方法,这里传入的key就是这个属性的名字。 - 这时layer先查看他是否有delegate,也就是CALayerDelegate;再看看这个delegate里面是否实现了
-actionForLayer:forKey:
方法。如果实现了这个方法,那么就会调用这个方法并且返回。 - 如果没有这个delegate或者delegate里面没有实现上述的方法,那么layer就会去检查他的actions属性。这个字典包含了属性名到action的映射。
- 如果还是没找到这个要动画的属性名的话,就去style字典里面找。
- 最后的最后,还是没有找到的话,就会调用
-defaultActionForKey:
方法;这个方法定义了标准的动画为那些已知的属性。
如果上述步骤的第二步中的-actionForLayer:forKey:
方法返回nil的话就不会有动画发生了;如果返回的是一个符合CAAction协议的对象,比如返回CABasicAnimation,那么就会执行动画。
因为UIView的backing layer的delegate是UIView本身。UIView实现-actionForLayer:forKey:
方法的方式就是:如果UIView的属性改变不发生在一个显式的动画工程之中就返回nil;如果在动画过程之中,就返回可动画的对象。测试代码如下:
1 | @interface ViewController () |
打印的结果如下:
1 | $ LayerTest[21215:c07] Outside: <null> |
所以说UIView是disable隐式动画的。
[CATransaction setDisableActions:YES];
这个方法也可以 禁用动画。
在layer的actions字典属性里面设置如下:
1 | CATransition *transition = [CATransition animation]; |
Presentation Versus Model
- 当你更改一个CALayer的属性时,这个属性是立刻发生改变的,但是并不在屏幕上立即显示出来。
- 当改变CALayer的属性时,CALayer相当于一个model,用来存储动画结束时该呈现何种状态;而Core Animation则相当于controller,用来控制view的变化。这也是一个MVC结构啊。所以有时候layer tree也叫作model layer tree。
- iOS中,屏幕1/60秒重新绘制一次。所以如果动画时间超过这一时长的话,那么Core Animation就需要从设置动画开始到动画结束内每1/60秒更新一次界面。所以这也需要CALayer能够记住当前的属性值和最终的属性值。
- 动画过程中,当前显示的layer的属性值,存储在一个presentation layer中,它可以通过
-presentationLayer;
方法取得。这个layer实际上就是model layer的复制,唯一的区别就是当前动画的属性值不同。当在presentation layer上调用-modelLayer
方法时,返回的是presentation layer的原始的model layer。
Explicit Animations
使用显式动画的好处是可以完全控制动画的行为,比如运动轨迹,动画时间等等。
Property Animations
CAAnimation类是所有Core Animation支持的动画类的共有的父类;它有一个子类是CAPropertyAnimation,也就是属性动画。
CABasicAnimation是CAPropertyAnimation的一个子类。CAPropertyAnimation指定一个属性通过指定keypath。CABasicAnimation有3个属性:fromValue, toValue, byValue。这三个都是id类型,因为他们可以是数值,颜色,图片等等。当然只需要指定3个中的两个就可以确定一个动画。
使用CABasicAnimation就是显式动画了,来看一个例子:
1
2
3
4
5
6//create a basic animation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"backgroundColor";
animation.toValue = (__bridge id)color.CGColor;
//apply animation to layer
[self.colorLayer addAnimation:animation forKey:nil];使用上述的代码在动画结束后,颜色还是会回到以前的颜色,是因为动画只是改变了presentation layer,并没有改变model layer。实际上,隐式动画改变属性时,动画使用的也是CABasicAnimation,属性改变时,会在
-actionForLayer:forKey:
方法中返回一个CABasicAnimation。所以我们可以在动画执行之前改变属性,如下:1
animation.fromValue = (__bridge id)self.colorLayer.backgroundColor; self.colorLayer.backgroundColor = color.CGColor;
选择在动画之前改变属性值,而不是在动画结束之后。是因为,在动画结束之后,会先跳回原始值,在跳到最终值。
使用CAAnimationDelegate可以解决上述问题。它可以知道动画准确的结束时间。实现
-animationDidStop:finished:
方法即可。代码如下:1
2
3
4
5
6
7
8
9
10// first specify delegate
animation.delegate = self;
// implement delegate method
- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag {
//set the backgroundColor property to match animation toValue
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.colorLayer.backgroundColor = (__bridge CGColorRef)anim.toValue; [CATransaction commit];
}CABasicAnimation在使用
-addAnimation:forKey:
方法添加给CALayer时,可以指定一个NSString类型的Key。这个Key可以唯一的标示一个animation。所以在-animationDidStop:finished:
方法中可以使用key来辨别是否是某个特定的animation。CAAnimation还有一套自己的方法,来标示一个animation。可以使用
-setValue:forKey:
以及-valueForKey:
方法。代码如下:1
2
3
4
5
6
7
8
9// when set up animation
[animation setValue:handView forKey:@"handView"];
// when finish animation
- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag {
// get value that set last step
UIView *handView = [anim valueForKey:@"handView"];
// do something later
}注意:这个动画结束的delegate被调用,可能发生在其值回退之前。所以,为什么要说在动画之前就设置动画结束的值。
CAKeyframeAnimation很强大,和CABasicAnimation一样,也是CAPropertyAnimation的一个子类。它和CABasicAnimation一样,也只对一个属性进行动画。CAKeyframeAnimation只需要你来提供关键帧,然后Core Animation会自动帮你填充关键帧之间的空白。看一个例子:
1 | CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; animation.keyPath = @"backgroundColor"; |
通过values属性(是一个NSArray),指定关键帧。必须指定开始和结束的帧,因为CAKeyframeAnimation不会默认使用这个属性当前的值来作为第一帧。
- CAKeyframeAnimation还可以使用CGPath。来看看例子:
1 | //create a path |
指定CAKeyframeAnimation的path属性为一个CGPath。然后就可以沿着这个path运动。另外,CAKeyframeAnimation还有一个rotationMode属性,该属性指定动画的layer的旋转特性。将该属性指定为kCAAnimationRotationAuto的话,上面的shipLayer就会一直旋转,以调整角度去对齐bezierPath的切线方向。
- CAPropertyAnimation不单单可以作用于实际存在的属性,还可以作用于虚拟的属性。是一个keys path:比如
animation.keyPath = @"transform.rotation";
。
Animation Groups
- CAAnimationGroup是CAAnimation的一个子类,他可以聚集一堆动画。
- 他有一个animations属性,是一个包含CAAnimation的数组。
Transitions
CATransition也是CAAnimation的一个子类,它影响整个layer。它会先对原先的layer照个快照,然后创建新的layer。之后以动画的形式,将新的layer快照,替代原先的layer快照,比方说从左向右推入。它主要用来动画一些不可动画的属性,比如UIImage的image属性。看看代码:
1 | @interface ViewController () |
CALayer的contents属性,自动应用CATransition动画,当改变一个不是backing layer的contents时,会自动应用动画。
自己实现CATransaction效果:
- CALayer的
-renderInContext:
方法可以用来获取将当前的layer的contents画到一个context里面,也就实现了获取当前的快照。 - 将这个快照放在当前屏幕的最上面,掩盖住原来的view;之后就可以在原来的view上面进行变化了。
- 再使用动画推出这个快照,就完成了transition。
看看代码实现:
1 | - (void)viewDidLoad { |
Canceling an Animation in Progress
- 当使用CALayer的
-addAnimation:forKey:
方法给一个layer添加了动画之后,可以使用-(CAAnimation *)animationForKey:(NSString *)key;
方法来根据key重新获取这个animation。 - 修改正在运行的动画的属性,是没有效果的。
- 可以使用
- (void)removeAnimationForKey:(NSString *)key;
方法来删除layer的某个animation。 - 可以使用
- (void)removeAllAnimations;
来删除layer的所有动画。 - 只要一删除动画,屏幕就会根据model layer来重新绘制。
Layer Time
The CAMediaTiming Protocol
CAMediaTiming协议定义了多种属性及方法用来控制动画工程中的时间,CALayer和CAAnimation都符合该协议。
duration属性:一次动画执行的时间。
repeatCount属性:动画重复执行的次数。可以是小数,比如3.5次。。
duration和repeatCount的默认值都是0,但不表示0次和0秒。表示的是默认的0.25s和1次。
还可以使用repeatDuration属性,指定整个重复动画的时间;autoreverses属性指定动画是否要原路径反着来一遍。
每个动画的时间都是各成体系的。每个动画内的时间可以被延迟,加速,减速等等。
beginTime属性:如果一个animation是在一个animation group中,则beginTime就是其parent object——animation group 开始的一个偏移。如果一个animation 的 beginTime为5,则此动画在group aniamtion开始之后的5s在开始动画。如果一个animation是直接添加在layer上,beginTime同样是是其parent object——layer 开始的一个偏移,但是一个layer的beginning是一个过去的时间(猜想layer的beginning可能是其被添加到layer tree上的时间),因此不能简单的设置beginTime为5去延迟动画5s之后开始,因为有可能layer的beginning加上5s之后也是一个过去的时间(很有可能),因此,当要延迟一个添加到layer上的动画的时候,需要定义一个addTime,因此:
1
2addTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil]; // 取得layer时间系下的现在时间
animation.beginTime = addTime + delay; // 设置animation的时间系的原点,要参照layer的时间系。如果一个layer他自己的beginTime已经设置,则animation的addTime的计算必须在layer的beginTime设置之后,因为要有一个时间的转移,具体看下面的例子:
1
2
3
4
5
6
7
8
9
10
11CFTimeInterval currentTime = CACurrentMediaTime();
CFTimeInterval currentTimeInSuperLayer = [superLayer convertTime:currentTime fromLayer:nil];
layer.beginTime = currentTimeInSuperLayer + 2; // 设置layer的时间系 从layer的superLayer进行设置。也就是说layer的时间系的原点是现在的Global时间在superLayer时间系里面加2s。 但是要把现在的global时间换成superLayer的时间。
CFTimeInterval currentTimeInLayer = [layer convertTime:currentTimeInSuperLayer fromLayer:superLayer];
CFTimeInterval addTime = currentTimeInLayer;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.beginTime = addTime + 1; // 设置groupAnimation的时间系, 是现在时间的Global时间在layer的时间系里面加1s。(若speed都一样的话,那么就是layer时间系的-1s的位置)。
group.animations = [NSArray arrayWithObject:anim];
group.duration = 2;
anim.beginTime = 0.5;
[layer addAnimation:group forKey:nil];
speed属性:表示的是时间的系数。默认为1,表示1倍速。如果duration是1s,但是speed是2,那么执行时间只有0.5s,也就是2倍速。
timeOffset属性:表示动画从timeOffset的位置开始,动画到终点结束后回到起点再动画到timeOffset的位置。它计算出来的开始位置不受speed的影响。
当设置removeOnCompletion属性为NO时,说明动画完成时,这个动画没有从layer上面删除。这时候可以设置fillMode属性。这个属性默认为kCAFillModeRemoved,表示被动画的属性在model layer上面都不曾变化。可以将fillMode属性设置成其他的,那么model layer上面的属性会在动画结束时自动更新将其设置成presentation layer上面一样的值。这个时候要注意,在添加animation的时候要指定一个key,以确保之后可以方便删除这个动画。
Hierarchical Time
layer的动画时间也是有相对坐标系的,类似layer的位置,时间也是相对于super layer的。
改变CALayer或者CAGroupAnimation的beginTime,timeOffset以及speed属性会影响到sublayer的动画时间。
Core Animation有一个global time的概念,也就是一个设备的各个程序间共同的一个时间体系。它是一个mach time,可以通过下列代码获得当前的global time:
1
CFTimeInterval time = CACurrentMediaTime();
这个函数返回的值很奇怪,它表示设备从上一次reboot到现在的秒数。
每个CALayer和CAAnimation还有自己的一个local time的概念。可以将2个layer之间的时间进行转换:
1
2- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;使用
self.window.layer.speed = 100;
会使你的整个app以100倍速运行。来看一个自己写的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIView *baseView1 = [[UIView alloc]initWithFrame:CGRectMake(50, 50, 50, 50)];
baseView1.backgroundColor = [UIColor redColor];
[self.view addSubview:baseView1];
UIView *baseView2 = [[UIView alloc]initWithFrame:CGRectMake(50, 250, 50, 50)];
baseView2.backgroundColor = [UIColor redColor];
[self.view addSubview:baseView2];
UIView *baseView3 = [[UIView alloc]initWithFrame:CGRectMake(50, 450, 50, 50)];
baseView3.backgroundColor = [UIColor redColor];
[self.view addSubview:baseView3];
UIView *baseView = [[UIView alloc]initWithFrame:CGRectMake(50, 50, 50, 50)];
baseView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:baseView];
CABasicAnimation *animation = [CABasicAnimation animation];
animation.duration = 4;
//animation.repeatCount = 2;
//baseView.layer.speed = 0.5;
animation.keyPath = @"position";
animation.toValue = [NSValue valueWithCGPoint:CGPointMake(75, 475)];
animation.timeOffset = 2;
//animation.removedOnCompletion = NO;
//animation.fillMode = kCAFillModeBoth;
animation.beginTime = [baseView.layer convertTime:CACurrentMediaTime() fromLayer:nil] + 2;
[baseView.layer addAnimation:animation forKey:@"base"];
}
Manual Animation
看段代码:
1 | @interface ViewController () |
由于在动画执行过程中无法改变animation的属性,所以可以更改它的上一级,也就是layer的属性。将speed设置成0,所以动画暂停,相当于动画一直在执行却始终是在第一帧上面执行。根据手势来改变layer的timeOffset,那么在下一次动画渲染屏幕的时候(也就是画下一帧presentation layer的时候),动画发现还处在第一帧,但是timeOffset已经变了,所以就跟着手指移动了。
Easing
Animation Velocity
- Easing是为了模仿现实中的有加速度的这种运动。
- 为了使用easing,可以设置CAAnimation的timingFunction属性;也可以使用CATransaction的
+setAnimationTimingFunction:
方法将easing应用到隐式动画上面。这个属性是一个CAMediaTimingFunction对象。 - 创建CAMediaTimingFunction对象的方法很多。简单的就是使用
+timingFunctionWithName:
方法。在UIView的动画中,kCAMediaTimingFunctionEaseInEaseOut是默认的,但是CAAnimation就要你自己去指定一个。注意隐式动画中的默认值是kCAMediaTimingFunctionDefault,但是显式动画中没有默认的值。 - CAKeyframeAnimation有一个timingFunctions属性,它是一个数组。数组的长度是keyframes数组的长度减一。它表示每个关键帧之间的easing属性。
Custom Easing Functions
可以使用CAMediaTimingFunction的
+functionWithControlPoints::::
方法来创建自定义的easing效果。这个方法的命名不太符合apple的规范。上面函数的4个点用来构成一个bezierPath,这个bezierPath可以表示这个动画的easing效果。bezierPath的切线就是速度。todo 图片
apple定义的几个easing效果也是这样实现的,可以使用CAMediaTimingFunction的
-getControlPointAtIndex:values:
方法来获取4个点。第一个点是起点,第四个点是终点,中间两个点是控制点1和控制点2。来看看将几种系统实现的easing效果的bezierPath画出来的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23- (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;
}奇怪的是这个函数的调用,它不传CGPoint,而是传递的float。如下:
1
[CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];
其中(1,0)表示第一个控制点,(0.75, 1)表示第二个控制点。
至于更加复杂的easing效果可以使用Keyframe-Based Easing。就是将复杂的easing分成多个keyframe,每个keyframe之间由一个简单的自定义easing效果来实现。这个方法需要繁琐的计算量。 todo 复杂的bezierPath的图片
还有一个简单的方法就是:每1/60s定义一个keyframe,在每个keyframe之间都是线性变化的。只要求得每个keyframe的位置就可以做出流畅的动画。取1/60s是因为iOS1/60s渲染一次屏幕,这个值已经是最佳的了,每次渲染屏幕的时候都有一个自己设的值,已经是到达最精确的控制了。其实这个时候两个关键帧之间用什么变化都无所谓的,他只是渲染两次而已,不存在什么变化。至于每个关键帧的位置如何求得,可以在http://robertpenner.com/easing找到算法。
Timer-Based Animation
Frame Timing
上面讲到的1/60s定义一个keyframe,其实还可以使用NSTimer来实现。也就是1/60s就执行一次更新属性的操作,然后就会重新绘制出来。但是使用NSTimer会不太精确。
NSTimer和屏幕重新绘制都是主线程的任务,主线程中的任务都要等到之前的任务执行完成后再开始执行。所以当到了执行NSTimer或者当需要开始重新绘制屏幕时,如果这时候之前的任务还没有执行结束,则需要等待。这时无论NSTimer还是重新绘制都会有一些延迟。所以可能出现,需要绘制时,NSTimer还没更新,或者在一次绘制时NSTimer已经更新了多次的情况。这时候就会出现animation的迟滞或者跳帧。看看这个的解决方案。
CADisplayLink和NSTimer类似,但是他能保证在屏幕重新绘制之前被触发。但是他不是通过指定一个timeInterval来触发,而是通过frameInterval来指定两次触发之间相隔多少帧。但是它还是无法保证重新绘制屏幕能按时发生。
根据上面的问题,这时就需要精确计算上一帧发生的时间,和当前帧发生的时间。两者相减就可以知道间隔时间,就能算出现在这一帧该动画到什么程度。来看看代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34- (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];
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;
}
}
Physical Simulation
看的晕头转向。。。
参考
- CAMediaTiming 协议属性详解
- [Robert Penner`s Easing Functions](http://robertpenner.com/easing)