iOS核心动画高级技巧——寄宿图

475 阅读8分钟

寄宿图

contents属性

CALayer 有一个属性叫做contents,这个属性的类型被定义为id,意味着它可以是任何类型的对象。在这种情况下,你可以给contents属性赋任何值,你的app仍然能够通过编译。但是,在实实践中,如果你给contents赋的不是CGImage,那么你得到的图层将是空白的。

事实上,你真正要赋值的类型应该是CGImageRef,它是一个指向CGImage结构的指针。UIImage有一个CGImage属性,它返回一个“CGImageRef”,如果你把这个值直接赋值给CALayer的contents,那你将会得到一个编译错误。因为CGImageRef并不是一个真正的Cocoa对象,而是一个Core Foundation类型。

尽管Core Foundation类型跟Cocoa对象在运行时貌似很像(被称作toll-free bridging),他们不是类型兼容的,不过你可以通过bridged关键字转换。如果要给图层的寄宿图赋值,你可以按照以下这个方法:

layer.contents = (_bridge id)image.CGImage;

contentGravity

CALayer与contentMode对应的属性叫做contentsGravity,但是它是一个NSString类型,而不是像对应的UIKit部分,那里面的值是枚举。contentsGravity可选的常量值有以下一些:

  • kCAGravityCenter
  • kCAGravityTop
  • kCAGravityBottom
  • kCAGravityLeft
  • kCAGravityRight
  • kCAGravityTopLeft
  • kCAGravityTopRight
  • kCAGravityBottomLeft
  • kCAGravityBottomRight
  • kCAGravityResize
  • kCAGravityResizeAspect
  • kCAGravityResizeAspectFill

contentMode一样,contentsGravity的目的是为了决定内容在图层的边界中怎么对齐,我们将使用kCAGravityResizeAspect,它的效果等同于UIViewContentModeScaleAspectFit,同时它还能在图层中等比例拉伸以适应图层的边界。

contentsScale

contentsScale属性定义了寄宿图的像素尺寸和视图大小的比例,默认情况下它是一个值为1.0的浮点数。

contentsScale属性其实属于支持高分辨率(又称Hi-DPI或Retina)屏幕机制的一部分。它用来判断在绘制图层的时候应该为寄宿图创建的空间大小,和需要显示的图片的拉伸度(假设并没有设置contentsGravity属性)。UIView有一个类似功能但是非常少用到的contentScaleFactor

当用代码的方式来处理寄宿图的时候,一定要记住要手动的设置图层的contentsScale属性,否则,你的图片在Retina设备上就显示得不正确啦。代码如下:

layer.contentsScale = [UIScreen mainScreen].scale;

maskToBounds

默认情况下,UIView仍然会绘制超过边界的内容或是子视图,在CALayer下也是这样的。

UIView有一个叫做clipsToBounds的属性可以用来决定是否显示超出边界的内容,CALayer对应的属性叫做masksToBounds

contentsRect

CALayer的contentsRect属性允许我们在图层边框里显示寄宿图的一个子域。这涉及到图片是如何显示和拉伸的,所以要比contentsGravity灵活多了。

boundsframe不同,contentsRect不是按点来计算的,它使用了单位坐标,单位坐标指定在0到1之间,是一个相对值(像素和点及就是绝对值)。所以他们是相对于寄宿图的尺寸的。iOS使用了以下的坐标系统:

  • 点——在iOS和Mac OS中最常见的坐标体系。点就是虚拟的像素,也被称作逻辑像素。在标准设备上,一个点就是一个像素,但是在Retina设备上,一个点等于2*2个像素。iOS用点作为屏幕的坐标测算体系就是为了在Retina设备和普通设备上能有一致的视觉效果。
  • 像素——物理像素坐标并不会用来屏幕布局,但是仍然与图片有相对关系,UIImage是一个屏幕分辨率解决方案,所以指定点来度量大小。但是一些底层的图片表示如CGImage就会使用像素,所以你要清楚在Retina设备和普通设备上,它们表现出来了不同的大小。
  • 单位——对于与图片大小或是图层显示边界相关的显示,单位坐标是一个方便的度量方式,当大小改变的时候,也不需要再次调整。单位坐标在OpenGL这种纹理坐标系统中用得很多,Core Animation中也用到了单位坐标。

默认的contentsRect是{0, 0, 1, 1},这意味着整个寄宿图默认都是可见的,如果我们指定一个小一点的矩形,图片就会被裁剪。

contentsRect在app中最有趣的地方在于一个叫做image sprites(图片拼合)的用法。

典型地,图片拼合后可以打包整合到一张大图上一次性载入。相比多次载入不同的图片,这样做能够带来很多方面的好处:内存使用,载入时间,渲染性能等等。

拼合不仅给app提供了一个整洁的载入方式,还有效地提高了载入性能(单张大图比多张小图载入地更快),但是如果有手动安排的话,它们还是有一些不方便的,如果你需要要在一个已经创建好的的拼合图上做一些尺寸上的修改或者是其他变动,无疑是比较麻烦的。

contentsCenter

看名字你可能会以为它可能跟图片的位置有关,不过这名字着实误导了你。contentsCenter其实是一个CGRect,它定义了一个固定的边框和一个在图层上可拉伸的区域。改变contentsCenter的值并不会影响到寄宿图的显示,除非这个图层的大小改变了,你才看得到效果。

默认情况下,contentsCenter是{0, 0, 1, 1},这意味着如果大小(由contentsGravity决定)改变了,那么寄宿图将会均匀地拉伸开。但是如果我们增加原点的值并减小尺寸。我们会在图片的周围创造一个边框。当图像根据其contentsGravity属性调整大小时,其中心部分隐式定义了3x3网格,该网格控制图像如何按其绘制的大小缩放。中心部分在两个维度上拉伸;顶部和底部部分只是水平拉伸;左右部分只是垂直拉伸;四个角部分完全没有拉伸。(这通常被称为“9片缩放”。)

Custom Drawing

contents赋CGImage的值不是唯一的设置寄宿图的方法。我们也可以直接用Core Graphics直接绘制寄宿图。能够通过继承UIView并实现drawRect方法来自定义绘制。

drawRect方法没有默认的实现,因为对UIView来说,寄宿图并不是必须的,它不在意那到底是单调的颜色还是有一个图片的实例。如果UIView检测到drawRect方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以contentsScale的值。

如果你不需要寄宿图,那就不要创建这个方法了,这会造成CPU资源和内存的浪费,这也是为什么苹果建议:如果没有自定义绘制的任务就不要在子类中写一个空的drawRect方法。

当视图在屏幕上出现的时候drawRect方法就会自动调用。drawRect方法里面的代码利用Core Graphics去绘制一个寄宿图,然后内容就会被缓存起来直到它需要被更新(通常是因为开发者调用了setNeedsDisplay方法,尽管影响到表现效果的属性值被更改时,一些视图类型会被自动重绘,如bounds属性)。虽然drawRect方法是一个UIView方法,事实上都是底层的CALayer安排了重绘工作和保存了因此产生的图片。

CALayer有一个可选的delegate属性,实现了CALayerDelegate协议,当CALayer需要一个内容特定的信息时,就会从协议中请求。CALayerDelegate是一个非正式协议,其实就是说没有CALayerDelegate @proticol可以让你在类里面引用。你只需要调用你想调用的方法,CALayer会帮你做剩下的。(delegate属性被声明为id类型,所有的代理方法嗾使可选的)。

当需要被重绘时,CALayer会请求它的代理给它一个寄宿图来显示。它通过调用下面这个方法做到的:

(Void)displayLayer:(CALayerCaLayer *)layer;

趁着这个机会,如果代理想直接设置contents属性的话,它就可以这么做,不然没有别的方法可以调用了。如果代理不实现displayLayer方法,CALayer就会转而尝试调用下面这个方法:

- (void)drawLayer:(CALayer *)layer inContext: (CGContextRef)ctx;

在调用这个方法之前,CALayer创建了一个合适尺寸的空寄宿图(尺寸由boundscontentsScale决定)和一个Core Graphics的绘制上下文环境,为护孩子寄宿图做准备,他作为ctx参数传入。

现在你理解了CALayerDelegate,并知道怎么使用它。但是除非你创建了一个单独的图层,你几乎没有机会用到CALayerDelegate协议。因为当UIView创建了它的宿主图层时,他就会自动地把图层的delegate设置为它自己,并提供了一个displayLayer的实现,那所有的问题就都没了。

当使用寄宿了视图的图层的时候,你也不必实现displayLayerdrawLayer:inContext:方法来绘制你的寄宿图。通常做法是实现UIView的drawRect方法,UIView就会帮你做完剩下的工作,包括在需要重绘的时候调用display方法。

总结

本章介绍了寄宿图和一些相关的属性。我们学到了如何显示和放置图片,使用拼合技术来显示,以及用CALayerDelegate和Core Graphics来绘制图层内容。