前面提到的视图动画非常强大,你应该尽量使用,尤其是在做基本布局动画时,它们还提供了少量常见的过渡效果,相关内容参见官方文档https://developer.apple.com/library/ios#documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/Introduction/Introduction.html 中的“Animations”部分。这里有很多极棒的工具可以满足你的基本需求。
但是我们要知道视图动画有很多限制。动画的基本单元是UIView,它是非常重量级的对象,所以不能多用。UIView也不支持三维布局(一些基本的z轴次序除外),所以你无法创建类似封面浏览效果的动画。为了让UI看起来更酷,需要使用Core Animation。
Core Animation 最重要也最基础的部分就是CALayer。
CALayer 在很多方面都与UIView非常相似。它拥有位置、大小、变形和内容。你可以用自定义的代码(通常用Core Graphics)来覆盖绘制方法以绘制定制内容。图层的层级关系与视图的非常接近。那么为什么还会让UIView 和 CALayer 类同时存在呢
最重要的答案是UIView 是一个很重量级的对象,它管理绘制与事件处理(尤其是触摸事件)。CALayer 完全关乎绘制。事实上,UIView依靠CALayer来管理绘制,这样两者就能协作的很好。
每个UIView 都有一个CALayer用于绘制。而且每个CALayer都可以拥有子图层,就像每个UIVIew都可以拥有姿势图一样。
图层会在它的contents属性(即CGImage)中绘制任意东西。你需要负责进行设置,这里有很多方法可用。最简单的一种方法就是直接分配,如下面的代码
UIImage *image = ....;
CALayer *layer = .....;
layer.contents = (__bridge id)[image CGImage];
(1) [CALayer setNeedsDisplay] :你的代码需要调用它。它会将图层标记为需要重绘的,要求通过列表中的步骤来更新contents。除非调用了setNeedsDisplay 方法,否则contents属性永远不会被更新。
(2)[CALayer displayIfNeeded]:绘制系统会再需要时自动调用它。如果图层被通过调用setNeedsDisplay标记为需要重绘的,绘图系统就会接着执行后续步骤。
(3)[CALayer display]:displayIfNeeded 方法会再合适的时候调用它。你不应直接调用它。如果你实现了委托方法,默认实现会调用displayLayer:委托方法。否则,display方法会调用drawInContext:方法。你可以在子类中覆盖display方法以直接设置contents属性
(4)[delegate displayLayer:]:默认的[CALayer display]会再方法实现这个方法时调用它。它的任务是设置contents,如果实现了这个方法(即使没有任何操作),会面就不会运行自定义的绘制代码。
(5)[CALayer drawInContext:]默认的display方法会创建一个视图图形上下文并将其传递给drawInContext:方法。它与[UIView drawRect:]方法相似,但不会自动设置UIKit上下文。为了使用UIKit来绘图,你需要调用UIGraphicsPushContext()方法指定接收到的上下文为当前上下文,否则,它会只使用Core Graphics 在接收到的上下文中绘图。默认的display方法获取最终的上下文,创建一个CGImage并将其分配给contents。默认的[CALayer drawInContext:]会再方法已实现时调用[delegate drawLayer:inContext:]。否则,就不执行任何操作。注意,你可以直接调用这个方法
(6)[delegate drawLayer:inContext:]:如果实现了这个方法,默认的drawInContext:会调用这个方法来更新上下文,以使display方法可以创建CGImage。
如你所见,有多重方法可以设置图层的内容。可以直接调用setContent:方法,也可以通过实现display或displayLayer:方法做到,还可以实现drawInContext:或drawLayer:inContext:方法。
绘图系统几乎不会像UIView一样经常自动更新内容。比如说,UIView在屏幕上第一次出现时会对自身进行绘制,而CALayer则不会。使用setNeedsDisplay方法标记UIView为需要重绘的,这样就可以自动绘制所有子视图了。使用setNeedsDisplay方法标记CALayer为需要重绘的并不会对子图层产生影响。需要记住默认的是:UIView默认会再它认为你需要的时候绘制,而CALayer默认只会在你明确要求时绘制。CALayer是很底层的对象,它经过了优化,除非你明确要求,否则它不会浪费时间执行任何操作。
直接设置内容:
#import <QuartzCore/QuartzCoew>
UIImage *image = [UIImage imageNamed:@"pushing.png"];
self.view.layer.contents = (__bridge id)[image CGImage];
需要使用强制转换符 __bridge id,因为contents是以id类型定义的,不过实际上是要赋值为CGImageRef类型的。为了确保能兼容ARC,需要强制转换。常见的错误是在这里传递的不是CGImageRef而是UIImage。虽然不会遇到编译器错误或运行时错误,但是视图将会是空白。
contents 内容默认填充视图,即便它会扭曲图片,正如UIVIew中的contentMode 和contentStretch,你可以通过contentCenter和contentsGravity设置CALayer以不同的方式对其图片进行缩放。
实现display方法
disp 和displayLayer:方法的任务是把contents属性设置为合适的CGImage。你可以用任何方式做到这一点。默认的实现会创建一个CGContext,并将其传递给drawInContext:方法,绘制结果在CGImage上,然后赋值给contents。一般覆盖方法的原因是图层有许多状态斌并且都有各自的图片。按钮通常就是这样的。通过文件束(bundle)加载、用Core Graphics绘制或其他方法都可以。
自定义绘图
使用drawInContext:方法是设置contents的另一种方法。它是通过display方法调用的,而display方法只有当你通过setNeedsDisplay方法明确标记图层为需要重绘时调用。与直接设置contents的方法相比,这里的优势在于display会自动创建适用于图层的CGContext。特别是坐标系统的翻转。以下代码展示了如何实现委托方法drawLayer:inContext:,以便在图层顶端通过UIKit绘制字符串“pushing The Limits”。因为Core Animation 不会设置UIKit图形上下文的内容,所以你需要再调用UIKit方法之前调用UIGraphicsPushContext方法,并在代码结束之前调用UIGraphicsPopContext方法。
-(id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if(self)
{
[self.layer setNeedsDisplay];
}
return self;
}
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
UIGraphicsPushContext(ctx);
[[UIColor whiteColor] set];
UIRectFill(layer.bounds);
[[UIColor blackColor] set];
UIFont *font = [UIFont systemFontOfSize:48.0];
[@"Pushing The Limits" drawInRect:[layer bounds] withFont:font lineBreakMode:NSLineBreakByWorkWrapping alignment:NSTextAlignmentCenter];
UIGraphicsPopContext();
}
请注意在initWithFrame:方法中对setNeedsDisplay的调用。之前说过,图层在屏幕上显示时不能自动重绘自身。需要使用setNeedsDisplay方法将其标记为需重绘的。
相对于使用BackgroundColor属性,你可能也注意到了背景的手动绘制。这是特意为之。当使用drawLayer:inContext:进行自定义绘制时,大部分自动图层设置(比如backgroundColor和cornerRadius)会被忽略。drawLayer:inContext:中完成的任务就是绘制图层所需的内容。这种方式不像在UIView中时那样有帮助。如果你想要同时实现自定义绘图与圆角效果等图层效果,可以在子图层上进行自定义绘图并在父图层上使用圆角。
在自己的上下文中绘图
与[UIView drawRect:]不同,你完全可以调用[CALayer drawInContext:]方法。只需要生成一个上下文并传递它进去,这样便于捕捉图层内容到位图或PDF,然后你就可以保存或打印它了。如果你想要拼合图层的话,像这样调用drawInContext:会很有帮助,因为如果你想要的知识位图,直接使用contents就可以了。
drawInContext:只有当前图层绘制(不包括其任何子图层)。如果你想要绘制图层及其子图层,可以使用renderInContext:方法,他可以捕捉图层当前动画的状态。renderInContext:使用当前渲染的状态(由Core Animation内部管理),因此它不会调用drawInContext:方法。