二、CoreAnimation之寄宿图详解
在之前的图层树中我们知道,可以使用CALayer对象创建一些有背景颜色的图层,其实使用CALayer,不仅可以利用其展示背景颜色,还可以展示图片。而这些展示内容,其实就是CALayer的寄宿图。这一节我们将来探索下CALayer寄宿图。
在CALayer中有一个属性叫做contents,这个属性的类型为id,意味着它可以是任何类型的对象,也就意味这即使你给contents属性赋任意对象值,您的项目都可以编译通过。然而,编译通过不代表使用正确,如果您给contents赋的不是CGImage对象,您的图层将展示一片空白。那既然如此,为何还要声明为id类型而不是CGImage类型呢?原因是Mac OS的历史原因造成的。在Mac OS系统中,CALayer的contents属性可以是CGImage和NSImage对象,因此CALayer的contents属性为id类型。
事实上,您真正需要给contents属性赋值的类型应该是CGImageRef,这是一个指向CGImage结构的指针。UIImage有一个CGImage属性,它返回一个CGImageRef指针。如果您想把这个值直接赋值给CALayer的contents属性,那您将得到一个编译错误。因为CGImageRef并不是一个真正的Cocoa对象,而是一个Core Foundation类型。
尽管Core Foundation类型跟Cocoa对象在运行时貌似很像,但他们并不是类型兼容的,不过我们仍然可以通过桥接的方式(ARC环境下),即bridge关键字转换。如果要给图层的寄宿图赋值,可以使用以下转换方法:
UIImage *image = [UIImage imageNamed:@"img01.png"]; layer.contents = (__bridge id)image.CGImage;//注意是两个下划线
这个时候,您可能想起了UIImageView,也是用来承载UIImage对象的,那么CALayer有没有和UIImageView的相似点呢?
答案是有的,在UIImageView中,有一个contenMode属性,用于选择内容填充模式.相似的,在CALayer中,也存在这样一个属性,即contentGravity,它的枚举值如下
- kCAGravityCenter
- kCAGravityTop
- kCAGravityBottom
- kCAGravityLeft
- kCAGravityRight
- kCAGravityTopLeft
- kCAGravityTopRight
- kCAGravityBottomLeft
- kCAGravityBottomRight
- kCAGravityResize
- kCAGravityResizeAspect
- kCAGravityResizeAspectFill
和contentMode一样,contentGravity的目的是为了解决内容在图层中的边界的对齐模式。
layer.contentsGravity = KCAGravityResizeAspect;
contentsScale:该属性定义了寄宿图的像素尺寸和视图大小的比例,默认情况下它是一个值为1.0的浮点数。一般情况下,该属性的表现并不明显,它并不是总对屏幕上的寄宿图有影响,如果您尝试对我们的上面的layer设置不同的contentsScale值,您就会发现根本没有任何的视觉上变化。因为contents由于设置了contentsGravity属性,所以他已经被拉伸以适应图层的边界。如果您只是单纯的想要放大contents的图片,您可以通过使用图层的transform和affineTransform属性来达到这个目的,因为这种放大,并非contentsScale的作用所在。
contentsScale属性其实属于支持高分辨率(Retina)屏幕机制的一部分,它用来判断在绘制图层的时候应该为寄宿图创建的空间打下,和需要显示的图片拉伸度(假设没有设置contentsGravity属性)。UIView有一个类似功能但是非常少用到的contentScaleFactor属性。
如果contentsScale设置为1.0,将会以每个点1像素绘制图片,如果设置为2.0,则会以每个点2像素绘制图片,这就是我们知道的Retina屏幕。这并不会对我们在使用contentsGravity时产生任何影响,因为该属性仅仅是拉伸图片适应图层而已,根本不会考虑到分辨率问题,但是如果我们把contentsGravity设置为KCAGraviryCenter(这种模式不会拉伸图片),那将会有很明显的变化。
没有设置时代码如下:
CALayer *layer = [CALayer layer]; layer.frame = CGRectMake(100, 100, 100, 100); layer.contents = (__bridge id)[UIImage imageNamed:@"001.jpg"].CGImage; [self.view.layer addSublayer:layer];
效果图如下:
当加上下面一句时
layer.contentsGravity = kCAGravityCenter;
效果图如下
如您所见,我们的图片不仅变大而且有像素的颗粒感。这是因为和UIImage不同,CGImage没有拉伸的概念。当我们使用UIImage去读取图片时,它读取了高质量的Retina版本的图片,但是当我们用CGImage来设置图层内容时,拉伸这个属性在转换的时候就丢失了。因此我们需要手动设置contentScale来修复这个问题。
layer.contentsScale = [UIScreen mainScreen].scale;
效果图如下:
contentsRect:CALayer的contentsRect属性允许我们在图层边框里显示寄宿图的一个子域。这涉及到图片是图和显示和拉伸的,所以要比contentsGravity灵活多了。和bounds,frame不同,contentsRect不是按点来计算的,它使用了单位坐标(值在0~1之间),是一个相对值(像素和点是绝对值)。所以他们是相对于寄宿图的尺寸的 。iOS使用了以下的坐标系统:
- 点:在iOS和Mac os中最常见的坐标体系。点就像是虚拟的像素,也被称为逻辑像素。在标准设备上,一个点就是一个像素,但在Retina设备上,一个点就等于2*2个像素。iOS用点作为屏幕的坐标测算体系就是为了在Retina设备和普通设备上能有一致的视觉效果
- 像素:物理像素坐标并不会用来屏幕布局,但是仍然与图片有相对关系。UIImage是一个屏幕分辨率解决方案,所以指定点来度量大小。但是一些底层的图片表示如CGImage就会使用像素,所以您要清楚在Retina屏幕和普通设备上,他们表现出来了不同的大小
- 单位:对于与图片大小或是图层边界相关的显示,单位坐标是一个方便的度量方式,当大小改变的时候,也不需要再次调整。单位坐标在OpenGL这种纹理坐标系统中用得很多,CoreAnimation中也用到了单位坐标
默认的contentsRect是{0,0,1,1},这意味着整个寄宿图默认都是可见的。如果我们指定一个小一点的矩形,
layer.contentsRect = CGRectMake(0, 0, 0.5, 0.5);
图片就会被裁剪,如图:
而contentsRect最有趣的用法在于,它可以进行图片拼合。图片可以在屏幕上独立的变换位置,载入拼合的图片。 具体做法是创建多个CALayer对象,每个对象都添加一个寄宿图,然后将这些layer添加在一个layer上即可。
自定义寄宿图:
给contents赋值CGImage并不是唯一设置寄宿图的方法,我们也可以直接利用Core Graphics直接绘制寄宿图。能够通过继承UIView并实现-drawRect方法来自定义绘制。
-drawRect方法没有默认的实现,因为对于UIView来说,寄宿图并不是必须的,它不在意那到底是单调的颜色背景还是图片实例。如果UIView检测到-drawRect方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以contentsSale的值,这无疑加大了内存的消耗。因此,如果您不需要寄宿图,那就不要创建这个方法了,这会造成CPU和内存的浪费,这也是为什么Apple建议:如果没有自定义绘制的任务就不要在子类中写一个空的drawRect方法。
当视图在屏幕上出现的时候,-drawRect方法就会被调用,该方法利用Core Graphics去绘制一个寄宿图,然后内容就会被缓存起来知道它需要被更新(一般是开发者调用了setNeedsDisplay方法和系统检测调用)。虽然-drawRect方法是一个UIView的方法,事实上都是底层的CALayer安排了重绘工作并保存了因此产生的图片。
CALayer有一个可选的delegate属性。实现了CALayerDelegate协议。当CALayer需要一个内容特定的信息时,就会从协议中请求。CALayerDelegate是一个非正式协议。其实就是说没有CALayerDelegate@protocol可以让你在类里引用啦。你只需要调用你想调用的方法,CALayer就会帮你完成城下的。
当需要被重绘时,CALayer会请求它的代理给他一个寄宿图来显示。它通过调用下面这个方法做到的:
- (void)displayLayer:(CALayer *)layer;
趁着这个机会,如果代理想直接设置contents属性的话,他就可以这么做,不然没有别的方法可以调用了。如果代理不实现该方法,CALayer就会转而尝试调用下面这个方法:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)cox;
在调用这个方法之前,CALayer创建了一个合适尺寸的空寄宿图(尺寸由bounds和contentsScale决定)和一个Core Graphics创建的绘制上下文环境,并为绘制寄宿图做准备,它作为参数传入。而且,在开发中,我们可以让某个CALayer对象显示地调用了-display方法。不同于UIVIew,当图层显示在屏幕上时,CALayer不会自动重绘它的内容,而是把重绘的决定权交给了开发者。
寄宿图就了解到这里,未完待续。