前言
本文,我们将深入CALayer内部,通过简单的CABasicAnimation动画来探究CALayer的两个非常重要的属性:presentationLayer和modelLayer。
从一个改变位置的动画开始
我们可以使用CABasicAnimation来实现各种动画,假如我们想让一个视图从一个位置动画地移动到另一个位置,我们的代码可能会这样写:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
UIView * view = [[UIView alloc] initWithFrame:CGRectMake(80, 80, 100, 100)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];
// 动画开始
CABasicAnimation * animation = [CABasicAnimation animation];
animation.keyPath = @"position";
animation.duration = 2;
animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(80, 80)];
animation.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 300)];
[view.layer addAnimation:animation forKey:nil];
}
也就是指定了动画的四要素:
- 动什么(keyPath)
- 动多久(duration)
- 从什么样子开始(fromValue)
- 动到什么样子(toValue)
这样CABasicAnimation就能在duration内对keyPath指定的属性从fromValue到toValue之间进行插值。然后我们将动画施加于view的layer,这样它就会按照我们的四要素进行动画了。
我们运行一下将会有如下效果:
注意:动画结束后,视图又回去了?
瞎子和瘸子
从前有一个瞎子和一个瘸子,瘸子看得见路,但是不能自己走,瞎子能自己走,但是他看不见路。于是他们想到了一个办法:瞎子背着瘸子,这样瘸子就指挥瞎子如何走路,瞎子负责走路,每走一步,瞎子都会停下来听从瘸子的指挥,这样他们就不紧不慢的走向了目的地。
在CALayer内部也有一个瞎子和一个瘸子:presentationLayer(以下简称P)和modelLayer(以下简称M)。presentationLayer负责走路(绘制内容),而modelLayer负责看路(如何绘制)。
P有这样的特点:
1、我们看到的一切,都是P的内容;
2、P只在下次屏幕刷新时才会进行绘制。
M有这样的特点:
1、我们我们对CALayer的各种绘图属性进行赋值和访问实际上都是访问的M的属性,比如bounds、backgroundColor、position等;
2、对这些属性进行赋值,不会影响P,也就是不会影响绘制内容。
你可以把M理解成一个隐身的家伙,只有P才能感知它的存在。
那么为什么我们对M的属性进行赋值,“与此同时”视图的显示状态就会发生改变呢?
如果我们用t0表示我们对属性进行赋值的时刻,t1表示下次屏幕刷新的时刻,t0到t1之间的间隔相当短(小于1/60秒),我们的人眼根本察觉不到这样的间隔时间存在,所以我们的感觉就是:赋值和界面绘制是同时进行的。然而实际上,赋值和界面绘制之间有一个t1-t0的时间间隔。
所以总结以上信息,我们可以知道P和M是这样进行交互的:当下次屏幕刷新信号到来时(屏幕重绘时),P为了绘制内容到屏幕上,它会去找M要各种它需要的用来绘制的属性,然后用这些属性的信息来绘制。
直到屏幕刷新信号到来才进行绘制,这种方式能够极大提高绘制效率。考虑这样一种情况,如果连续写了以下三行代码:
view.frame = …;
view.backgounrdColor = …;
view.center = …;,
如果每次赋值就会导致屏幕的重绘,这样就会有三次重绘,也就是GPU会执行三次绘制操作。而如果我们先把绘制信息存起来,当需要绘制的时候(屏幕刷新)再用这些信息去重绘,GPU就只会执行一次绘制操作。如果不能理解这样做的优势,我们将会在实战篇讲述如何提高性能的时候再次带大家了解绘图机制,也就是你看到的一切究竟是如何被显示出来的。
上面三行代码,只是对M的属性赋值了而已,没有任何的绘制操作,当下次屏幕刷新信号到来时,P才会去找M要这些属性,进行一次绘制。执行这三次赋值(执行三行代码)所需的时间远远小于两次屏幕刷新的间隔,所以这样三行代码是一定能够在下次绘制开始前执行完的。也就是说,当我们写了这三行代码后,视图的显示内容不会改变三次,而只会改变一次。
如果我们继续按照瞎子和瘸子的理论进行下去,P背着M,如果我们在下次屏幕刷新前(P走下一步之前)对M进行了三次操作:把M移动到点1,把M移动到点2,把M移动到点3。那么下次屏幕刷新时,P会直接问M“你在哪啊”,M说我在点3呢,你这样这样走过来,于是P就屁颠屁颠的按照M的指挥跑到点3去了,它是不会先跑到点1再跑到点2再跑到点3的。
CAAnimation对presentationLayer的控制
P这个瞎子背着M这个瘸子,他们互帮互助,完成各种显示的逻辑。每当屏幕刷新的时候,如果没有其他的信息告诉P它应该如何绘制,那么P就只能去问M,然后回到M的状态。
这样一个平衡会在一个CAAnimation被加到CALayer上后会被打破。当我们调用了layer的addAnimation方法后,一个CAAnimation(以下简称A)就控制了P,就相当于A一脚把M从P身上踹了下来然后自己爬了上去(不要想歪 )。
现在P要如何显示就完全由A来控制了,因为A被指定了在duration中从fromValue变到toValue。所以在动画的持续时间内,M被丢在了原地,P则背着A到处跑(毕竟P是个瞎子,根本不知道他脑袋顶上的家伙是谁),每一步该如何走都是A通过插值计算出来的(通过duration、from、to就能计算任意时刻P的状态了)。
动画结束后,默认情况A就被移除了,这时候P身上啥也没有了。那么在下一次屏幕刷新的时候,没有其他的信息告诉P它应该如何绘制,所以它就又开口了“M你在哪里啊”,M说“劳资在这里”(在动画过程当中它就没被动过,除非你手动设置了M的值),“你怎么跑那里去了”,“要你管,快给我滚过来”(真是一对好CP)。于是P就屁颠屁颠的滚到M那里去了,所以动画结束后,你会看到视图又回去了。
如果想要P在动画结束后就停在当前状态而不回到M的状态,我们就需要给A设置两个属性,一个是A.removedOnCompletion = NO;表示动画结束后A依然影响着P,另一个是A.fillMode = kCAFillModeForwards,(关于fillMode到底干了什么我们将在下一章详细进行讲解),这两行代码将会让A控制住P在动画结束后保持不变(不会回到M的状态),我们一开始的代码就会写成这个样子:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
UIView * view = [[UIView alloc] initWithFrame:CGRectMake(80, 80, 100, 100)];
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];
CABasicAnimation * animation = [CABasicAnimation animation];
animation.keyPath = @"position";
animation.duration = 2;
animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(80, 80)];
animation.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 300)];
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
[view.layer addAnimation:animation forKey:nil];
}
模型与显示的同步
我们可以通过设置removedOnCompletion = NO以及fillMode来让动画结束后保持状态,但是此时我们的P和M不同步,我们看到的P是toValue的状态,而M则还是自己原来的状态。举个栗子,我们初始化一个view,它的状态为1,我们给它的layer加个动画,from是0,to是2,设置fillMode为kCAFillModeForewards,则动画结束后P的状态是2,M的状态是1,这可能会导致一些问题出现。比如你点P所在的位置点不动,因为响应点击的是M。所以我们应该让P和M同步。
在CABasicAnimation的文档中写了这样一句话:如果不设置toValue,则CABasicAnimation会从fromValue到M的值之间进行插值。也就是说,如果不设置toValue,则CABasicAnimation会把M的值作为toValue,所以我们就可以在加动画的时候只设置fromValue,再手动修改M的值到你想要动画停止的那个状态就保持同步了。
我们可以扩展到任意的CAAnimation对象,比如CAKeyFrameAnimation,都可以通过设置M的值到动画结束的状态来保持P和M的同步。
所以我们的代码可能就会写成这样:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
UIView * view = [[UIView alloc] initWithFrame:CGRectMake(80, 80, 100, 100)];
view.center = CGPointMake(200, 300);
view.backgroundColor = [UIColor blueColor];
[self.view addSubview:view];
CABasicAnimation * animation = [CABasicAnimation animation];
animation.keyPath = @"position";
animation.duration = 2;
animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(80, 80)];
// animation.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 300)];
// animation.removedOnCompletion = NO;
// animation.fillMode = kCAFillModeForwards;
[view.layer addAnimation:animation forKey:nil];
}
当我们写view.center = CGPointMake(200, 300);的时候,只是对M赋值,再次强调,它不会影响P的显示,而当P想要显示的时候,它已经被A控制了,所以P会从一开始就在80,80那里然后动画地移动到200,300,不会出现先在200,300那里闪一下的情况。
运行一下,效果就是这样: