iOS CoreAnimation专题
原理篇
一、CALayer与UIView之间的关系
观点
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。
二、UIView block动画实现原理
CALayer的可动画属性
CALayer拥有大量的属性,如果大家按住cmd点进CALayer的头文件中看的话,会发现很多的属性的注释中,最后会有一个词叫做Animatable,直译过来是可动画的。下面的截图只是CALayer众多可动画属性中的一部分(注意frame并不是可动画的属性):
/** Geometry and layer hierarchy properties. **/
/* The bounds of the layer. Defaults to CGRectZero. Animatable. */
@property CGRect bounds;
/* The position in the superlayer that the anchor point of the layer's
* bounds rect is aligned to. Defaults to the zero point. Animatable. */
@property CGPoint position;
/* The Z component of the layer's position in its superlayer. Defaults
* to zero. Animatable. */
@property CGFloat zPosition;
/* Defines the anchor point of the layer's bounds rect, as a point in
* normalized layer coordinates - '(0, 0)' is the bottom left corner of
* the bounds rect, '(1, 1)' is the top right corner. Defaults to
* '(0.5, 0.5)', i.e. the center of the bounds rect. Animatable. */
@property CGPoint anchorPoint;
/* The Z component of the layer's anchor point (i.e. reference point for
* position and transform). Defaults to zero. Animatable. */
@property CGFloat anchorPointZ;
/* A transform applied to the layer relative to the anchor point of its
* bounds rect. Defaults to the identity transform. Animatable. */
@property CATransform3D transform;
....
如果一个属性被标记为Animatable,那么它具有以下两个特点:
1、直接对它赋值可能产生隐式动画;
2、我们的CAAnimation的keyPath可以设置为这个属性的名字。
当我们直接对可动画属性赋值的时候,由于有隐式动画存在的可能,CALayer首先会判断此时有没有隐式动画被触发。它会让它的delegate(没错CALayer拥有一个属性叫做delegate)调用actionForLayer:forKey:来获取一个返回值,这个返回值在声明的时候是一个id对象,当然在运行时它可能是任何对象。这时CALayer拿到返回值,将进行判断:如果返回的对象是一个nil,则进行默认的隐式动画;如果返回的对象是一个[NSNull null] ,则CALayer不会做任何动画;如果是一个正确的实现了CAAction协议的对象,则CALayer用这个对象来生成一个CAAnimation,并加到自己身上进行动画。
根据上面的描述,我们可以进一步完善我们上一章中重写的CALayer的属性的setter方法,拿position作例子:
- (void)setPosition:(CGPoint)position{
// [super setPosition:position];
if ([self.delegate respondsToSelector:@selector(actionForLayer:forKey:)]) {
id obj = [self.delegate actionForLayer:self forKey:@"position"];
if (!obj) {
// 隐式动画
} else if ([obj isKindOfClass:[NSNull class]]) {
// 直接重绘(无动画)
} else {
// 使用obj生成CAAnimation
CAAnimation * animation;
[self addAnimation:animation forKey:nil];
}
}
// 隐式动画
}
UIView的block动画
有趣的是,如果这个CALayer被一个UIView所持有,那么这个CALayer的delegate就是持有它的那个UIView,结合上一章讲的CALayer的各个属性是如何与UIView交互的,大家应该可以思考出这样的问题:为什么同样的一行代码在block里面就有动画在block外面就没动画,就像下面这样:
// 这样写没有动画
view.center = CGPointMake(80, 80);
[UIView animateWithDuration:1.25 animations:^{
// 写在block里面就有动画
view.center = CGPointMake(80, 80);
}];
既然UIView就是CALayer的delegate,那么actionForLayer:forKey:方法就是由UIView来实现的。所以UIView可以相当灵活的控制动画的产生。
当我们对UIView的一个属性赋值的时候,它只是简单的调用了它持有的那个CALayer的对应的属性的setter方法而已,根据上面的可动画属性的特点,CALayer会让它的delegate(也就是这个UIView)调用actionForLayer:forKey:方法。实际上结果大家都应该能想得到:在UIView的动画block外面,UIView的这个方法将返回NSNull,而在block里面,UIView将返回一个正确的CAAction对象(这里将不深究UIView是如何判断此时setter的调用是在动画block外面还是里面的)。
为了证明这个结论,我们将继续进行实验:
NSLog(@"%@",[view.layer.delegate actionForLayer:view.layer forKey:@"position"]);
[UIView animateWithDuration:1.25 animations:^{
NSLog(@"%@",[view.layer.delegate actionForLayer:view.layer forKey:@"position"]);
}];
我们分别在block外面和block里面打印actionForLayer:forKey:方法的返回值,看看它究竟是什么玩意:
打印发现,我们的结论是正确的:在block外面,这个方法将返回一个NSNull(是尖括号的null,nil打印出来是圆括号的null),而在block里面返回了一个叫做UIViewAdditiveAnimationAction类的对象,这个类是一个私有类,遵循了苹果一罐的命名规范: xxAction,一定就是一个实现了CAAction协议的对象了。
这也就说明了为什么我们对一个view的center赋值,如果这行代码在动画block里面,就会有动画,在block外面则没有动画。
注意
1、 如果你的代码大概是这样的:
view.center = CGPointMake(80, 80);
[UIView animateWithDuration:1.25 animations:^{
view.center = CGPointMake(80, 80);
} completion:^(BOOL finished) {
NSLog(@"aaa");
}];
那么completion里面的block将会瞬间被调用而不是1.25秒之后调用,因为你这样写的动画是没有意义的(从一个地方移动到这个地方),所以就没有动画产生,也就是动画一开始就结束了。
2、 如果你的代码大概是这样的:
[UIView animateWithDuration:1.25 animations:^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
view.center = CGPointMake(80, 80);
});
} completion:^(BOOL finished) {
NSLog(@"aaa");
}];
也就是在动画block里面延迟调用一段代码,同样是没有卵用的,completionBlock将会直接被调用,因为当animateWithDuration…这个类方法被调用的时候animationBlock里面没有任何与动画相关的代码(view.center = CGPointMake(80, 80);这行代码被延迟调用了)