这一篇中,我们将简单的探究动画原理(所以这一篇很多文字内容)。首先我们要知道我们在屏幕上看到的一切是如何被绘制出来的,我们将介绍CALayer与UIView之间的关系,以及layer的一些特性,然后我们会研究UIKit的UIView+block动画实现原理,为什么同样的一行代码在block里面就有动画在block外面就没动画,接下来我们将深入到layer内部,结合CABasicAnimation看看两个非常重要的概念:modelLayer和presentationLayer,以及模型和显示的同步。最后我们将通过进一步研究CABasicAnimation来研究“动画时间”这个概念。
这一章中,我们将探究UIView和它持有的那个CALayer之间的关系是怎样的,并通过一个实验来证明我们的结论。这一章你将学到一些比较理论的东西,所以没有demo,更多的是文字,请放心食用。
结论是最好的开胃菜
UIView是iOS开发的核心类,在我们看来,它负责几乎所有的界面展示和用户交互,这是苹果设计得非常好的一个地方——它完美的将其真正负责绘制界面的那个类封装起来了,对开发者而言,我们几乎只和UIView打交道就能完全控制一个视图的显示而不需要去管它底层究竟如何去处理的,然而我们今天必须要稍微深入进UIView中看看,以便于我们之后对动画原理的学习。
对于CALayer这个类 我相信大家都不太陌生,大多数人第一次接触这个类都是通过一个UIView对象的属性layer认识的。在学习如何给一个视图加圆角、设置边框颜色之类的效果的时候,第一次接触到了CALayer,当时并没有去理会这个东西,因为它似乎并不是UIKit框架中的东西,也不知道它有啥用,只是隐隐觉得这个家伙并不简单,事实的确如此。
实际上CALayer是属于QuartzCore框架,而这个框架是一个跨平台的绘制框架。这里的跨平台指的是在iOS和OS X系统上均能使用,也就是说CALayer能在iOS和OS X上面绘制内容(这也说明了为什么CALayer的很多属性都不是UIKit框架下面的东西,比如它的backgroundColor是CGColorRef,因为OS X中没有UIKit)。但是这两个平台接收用户交互的方式完全不一样:iOS是通过触摸事件(touch event)而OS X则是监听鼠标和键盘事件。
苹果的工程师们考虑到:这两个平台的绘制规则(如何将一个像素显示到屏幕上)都是一样的,但是交互规则完全不一样。所以他们使用了同一种绘制方式和不同的交互方式,具体的,他们将CALayer作为两个平台共同的用来绘制内容的类而针对交互方式,他们在iOS中使用了UIResponder类来响应交互在OS X中则没有UIKit里面的类取而代之是叫做NSView之类的东西。而通常情况下交互是发生在绘制内容之上的(有内容的地方才能点击),所以UIKit将CALayer封装进了UIView中,让开发者们感觉到UIView既能绘制又能处理交互,而实际上负责绘制的是UIView中的CALayer。
UIView就像电视机,CALayer则是它的屏幕,对于观众来讲我们知道电视机能显示东西、能操作(开关机、换台等)、能设置和获取各种显示属性,比如亮度、对比度等。我们对电视机的各种命令都会改变它自身的显示,就像我们修改一个UIView的背景色一样,电视机本身把如何显示的细节隐藏起来了,观众只需要简单的按个按钮(给UIView设置一个属性)就能控制它的显示了。当我们获取和设置电视机当前的亮度时,电视机只是简单的去获取和设置它的显示屏的亮度而已。我们访问和设置UIView的一些负责绘制规则的属性比如frame、center、backgroundColor等,在其持有的CALayer中都存在对应的属性,UIView只是简单的返回它自己CALayer的属性以及对自己的CALayer的这些对应的属性赋值而已。
上面说了一大堆,总结起来就一句话:UIView负责处理用户交互,负责绘制内容的则是它持有的那个CALayer,我们访问和设置UIView的这些负责显示的属性实际上访问和设置的都是这个CALayer对应的属性,UIView只是将这些操作封装起来了而已。
活着就是为了证明自己
为了证实我们上面的结论,我们将通过一个具体的实验来看看UIView和它持有的这个CALayer之间是怎样进行交互的。
我们新建一个类,继承自UIView,取名为TestAnimationView。我们在这个类中重写一些方法来看看系统在我们取值和赋值的时候干了些什么。为了弄清楚UIView和其持有的那个layer之间的关系,我们需要把这个类的layer改为我们自己定义的一个layer,所以我们在这个类中声明一个私有的类TestAnimationLayer,接下来重写TestAnimationView的+layerClass方法:
+ (Class)layerClass {
return [TestAnimationLayer class]; }
这个方法将会指定这个UIView被初始化出来之后其自动创建并持有的这个layer的类。
接下来我们为View和layer重写几个方法:
@interface TestAnimationLayer : CALayer
@end
@implementation TestAnimationLayer
- (void)setFrame:(CGRect)frame
{
[super setFrame:frame];
}
- (void)setPosition:(CGPoint)position
{
[super setPosition:position];
}
- (void)setBounds:(CGRect)bounds
{
[super setBounds:bounds];
}
- (CGPoint)position
{
return [super position];
}
@end
@implementation TestAnimationView
- (instancetype)init
{
self = [super init];
if (self) {
}
return self;
}
- (CGPoint)center
{
return [super center];
}
- (void)setFrame:(CGRect)frame
{
[super setFrame:frame];
}
- (void)setCenter:(CGPoint)center
{
[super setCenter:center];
}
- (void)setBounds:(CGRect)bounds
{
[super setBounds:bounds];
}
+ (Class)layerClass
{
return [TestAnimationLayer class];
}
@end
在各个方法中打好断点,接下来我们在Controller中调用一下:
TestAnimationView * view = [[TestAnimationView alloc] init];
然后运行我们的程序,等待断点进入。程序运行起来后停下来的第一个断点:
此时调用栈中:
我们点击调试的step over执行到第45行(此时刚执行完第44行,也就是调用了super init方法),此时调用栈中:
可以看到super init这个方法里面调用了5个方法依次为:
-[UIView init] -[UIView initWithFrame:] UIViewCommonInitWithFrame
-[UIView _createLayerWithFrame:] -[TestAnimationLayer setBounds:]
可以知道的信息:UIView的子类在调用super init的时候,UIView在它自己的init方法中会调用initWithFrame:方法,这个方法中实际上调用了一个私有函数叫做UIViewCommonInitWithFrame,然后又调用了_createLayerWithFrame:,这个方法读名字的话就知道它是干嘛用的了。创建完layer后又会让这个layer调用setBounds:方法,当然,此时断点会进到我们自己的layer类中的setBounds方法里面:
接着我们点击continue program execution让程序从当前断点继续执行下去,然后停在了UIView的setFrame方法这里:
说明了在UIView的init方法中,它真正的调用顺序是这样的:
-[UIView init] -[UIView initWithFrame:] UIViewCommonInitWithFrame
-[UIView _createLayerWithFrame:] -[TestAnimationLayer setBounds:] -[TestAnimationView setFrame:]
它会先创建layer,然后给layer的bounds赋值,最后才给自己的frame赋值。
我们继续执行,发现程序停在了center的getter这里:
奇怪,为什么setFrame会去调用center,实际上如果你去重写bounds的getter你会发现它还会进到bounds的getter中,说明UIView的frame实际上是由center 和bounds来决定的,可能UIView中并没有frame这个实例变量,frame的getter和setter都是在操作center和bounds而已。
我们继续执行,发现代码居然停在了layer的position的getter方法里面:
你猜怎么着,原来UIView的center的getter方法只是简单的去获取自己持有的那个layer的position然后返回。关于这个,我们待会会做一个更深入的实验。
继续执行,然后断点又停在了layer的setFrame方法里面,然后会发现layer会在setFrame方法中调用自己的setPosition和setBounds。
所以当我们给一个UIView设置frame的时候,这个view首先调用自己layer的setFrame方法,而在layer的setFrame方法里实际上又调用了setBounds和setPosition,说明layer的frame这个属性实际上并没有实例变量,它的setter和getter仅仅是去调用其bounds和position的setter和getter而已,也就是说frame实际上是由bounds和position来决定的(实际上还有anchorPoint,这里没有加到实验中来,大家可以自己试一试)。而UIView的frame并没有调用UIView的center和bounds的setter和getter,它仅仅是去调用其持有的layer的frame的setter和getter而已。
这样我们就证明了UIView只是一个简单的控制器而已,它不负责任何的内容绘制,我们对它的各种负责绘制的属性(Geometry属性和backgroundColor等)访问和赋值实际上都是在跟layer打交道。
为了进一步证明,我们在controller中再加几行代码:
TestAnimationView * view = [[TestAnimationView alloc] init];
view.layer.position = CGPointMake(80, 80);
NSLog(@"%@",NSStringFromCGPoint(view.center));
我们对layer的属性直接赋值,然后去访问view的对应的属性(这里是layer的position对应view的center)。同样的在TestAnimationView中打满断点,然后在外面的这里打上断点:
因为我们调用init方法的时候会调用大量TestAnimationView中的方法,当上面第一个断点进来后再执行代码就保证了接下来的断点断的是我们对Layer的position赋值的操作(第二个断点的意义同第一个断点)。
运行直到断点卡到上面的第一个断点处,然后点击Continue program execution,会发现直接进入layer的setPosition方法,接着就进入了上面的第二个断点。继续运行会发现断点进入了view的center方法,然后又进到了Layer的position方法里。
也就是说,我们在调用setPosition的时候并没有去调用view的任何方法而对view的center进行访问时view直接又去调用了其持有的那个layer的position的getter。继续运行发现打印的结果就是{80,80},我们没有调用view的setCenter方法但是调用getCenter却返回了正确的值,再次证明了我们访问View的属性实际上就是访问了其持有的那个layer对应的属性。
由此我们甚至可以猜测出UIView和CALayer在Geometry类目中的各个属性的setter和getter的实现代码:
@interface TestAnimationLayer : CALayer
@end
@implementation TestAnimationLayer
- (CGRect)frame
{
return frameWithCenterAndBounds([self bounds], [self position]);
}
- (void)setFrame:(CGRect)frame
{
[self setBounds:CGRectMake(self.bounds.origin.x, self.bounds.origin.y, frame.size.width, frame.size.height)];
[self setPosition:CGPointMake(frame.origin.x + frame.size.width/2, frame.origin.y + frame.size.height/2)];
}
CGRect frameWithCenterAndBounds(CGRect bounds, CGPoint center)
{
CGFloat width = CGRectGetWidth(bounds);
CGFloat height = CGRectGetHeight(bounds);
return CGRectMake(center.x - width/2, center.y - height/2, width, height);
}
@end
@implementation TestAnimationView
- (instancetype)init
{
self = [super init];
if (self) {
}
return self;
}
- (CGPoint)center
{
return [[self layer] position];
}
- (CGRect)bounds
{
return [[self layer] bounds];
}
- (CGRect)frame
{
return [[self layer] frame];
}
- (void)setFrame:(CGRect)frame
{
[[self layer] setFrame:frame];
}
- (void)setCenter:(CGPoint)center
{
[[self layer] setPosition:center];
}
- (void)setBounds:(CGRect)bounds
{
[[self layer] setBounds:bounds];
}
+ (Class)layerClass
{
return [TestAnimationLayer class];
}
@end
CALayer作为一个跨平台框架(OS X和iOS)QuatzCore的类,负责MAC和iPhone(ipad等设备)上绘制所有的显示内容。而iOS系统为了处理用户交互事件(触屏操作)用UIView封装了一次CALayer,UIView本身负责处理交互事件,其持有一个Layer,用来负责绘制这个View的内容。而我们对UIView的和绘制相关的属性赋值和访问的时候(frame、backgroundColor等)UIView实际上是直接调用其Layer对应的属性(frame对应frame,center对应position等)的getter和setter。
在下一章中,我们将研究UIKit的UIView+block动画实现原理,为什么同样的一行代码在block里面就有动画在block外面就没动画。