二、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不会自动重绘它的内容,而是把重绘的决定权交给了开发者。

  寄宿图就了解到这里,未完待续。

 

posted @ 2016-07-30 14:38  Silktree  阅读(587)  评论(0编辑  收藏  举报