上一章中我们介绍了CALayer的可动画属性,然后研究了UIView的block动画实现原理。这一章我们将深入CALayer内部,通过简单的CABasicAnimation动画来探究CALayer的两个非常重要的属性:presentationLayer和modelLayer。
我们可以使用CABasicAnimation来实现各种动画,假如我们想让一个视图从一个位置动画地移动到另一个位置,我们的代码可能会这样写:
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之间进行插值(关于插值我们在技巧篇会详细讲解,这里你只需要知道CABasicAnimation是一帧一帧在重新绘制视图的)。然后我们将动画施加于view的layer,这样它就会按照我们的四要素进行动画了。
完整的代码如下:
- (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];
}
注意到fromValue和我们初始化view时的center不一样。
我们运行一下将会有如下效果:
噫,动画结束后,怎么视图又回去了?
从前有一个瞎子和一个瘸子,瘸子看得见路,但是不能自己走,瞎子能自己走,但是他看不见路。于是他们想到了一个办法:瞎子背着瘸子,这样瘸子就指挥瞎子如何走路,瞎子负责走路,每走一步,瞎子都会停下来听从瘸子的指挥,这样他们就不紧不慢的走向了目的地。
在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的。
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那里闪一下的情况。
运行一下,效果就是这样:
在CALayer内部,它控制着两个属性:presentationLayer(以下称为P)和modelLayer(以下称为M)。P只负责显示,M只负责数据的存储和获取。我们对layer的各种属性赋值比如frame,实际上是直接对M的属性赋值,而P将在每一次屏幕刷新的时候回到M的状态。比如此时M的状态是1,P的状态也是1,然后我们把M的状态改为2,那么此时P还没有过去,也就是我们看到的状态P还是1,在下一次屏幕刷新的时候P才变为2。而我们几乎感知不到两次屏幕刷新之间的间隙,所以感觉就是我们一对M赋值,P就过去了。P就像是瞎子,M就像是瘸子,瞎子背着瘸子,瞎子每走一步(也就是每次屏幕刷新的时候)都要去问瘸子应该怎样走(这里的走路就是绘制内容到屏幕上),瘸子没法走,只能指挥瞎子背着自己走。可以简单的理解为:一般情况下,任意时刻P都会回到M的状态。而当一个CAAnimation(以下称为A)加到了layer上面后,A就把M从P身上挤下去了。现在P背着的是A,P同样在每次屏幕刷新的时候去问他背着的那个家伙,A就指挥它从fromValue到toValue来改变值。而动画结束后,A会自动被移除,这时P没有了指挥,就只能大喊“M你在哪”,M说我还在原地没动呢,于是P就顺声回到M的位置了。这就是为什么动画结束后我们看到这个视图又回到了原来的位置,是因为我们看到在移动的是P,而指挥它移动的是A,M永远停在原来的位置没有动,动画结束后A被移除,P就回到了M的怀里。
动画结束后,P会回到M的状态(当然这是有前提的,因为动画已经被移除了,我们可以设置fillMode来继续影响P),但是这通常都不是我们动画想要的效果。我们通常想要的是,动画结束后,视图就停在结束的地方,并且此时我去访问该视图的属性(也就是M的属性),也应该就是当前看到的那个样子。按照官方文档的描述,我们的CAAnimation动画都可以通过设置modelLayer到动画结束的状态来实现P和M的同步。
在下一章中,我们将通过进一步研究CABasicAnimation来研究“动画时间”这个概念。我们将看到如何控制动画进程,如何暂停动画。