理解contentsScale

最近在看《iOS CoreAnimation: Advanced Techiques》时,不太理解CALayer的contentsScale属性,在后面的CATextLayer中再次遇到,于是花功夫Google了一下各类文档,下面说说自己对contentsScale的理解,可能涉及的方面有些多:

Point与Pixel

iOS中的绘图系统使用的尺寸单位为Point,而屏幕显示的单位为Pixel,为什么要这样做呢?其实就是为了隔离变化:对于绘图而言,并不关心如何在屏幕上显示,这些属于硬件细节,也不应该关心,因此框架使用了万金油方法——抽象,绘图使用与硬件无关的Point,系统根据当前屏幕的情况自动将Point转成Pixel,所以不论以后硬件屏幕如何变化,使用Point的绘图系统以不变应万变,这就是抽象的好处!

对于非Retina与Retina的屏幕,Point与Pixel的转化关系如下:

  • 非Retina:1 Point = 1 x 1 Pixel
  • Retina:1 Point = 2 x 2 Pixel

系统通过一个变量来表示这种映射关系,这就是UIScreen中的scale属性的作用。需要注意的是,Retina屏幕1个Point对应4个Pixel,但scale为2,为什么不是4呢?因为在屏幕的二维空间中,一切可显示的物体都有X轴跟Y轴性质,Retina屏幕的映射是X跟Y两个方向同时放大2倍实现的,所以scale为2(这也是为什么上面写成2 x 2而不是直接为4的原因)。

CALayer与Render

在iOS中,如果想要显示什么,可能第一时间想到的就是UIView及其子类,但是UIView本身其实并不负责显示,从UIView从UIResponder继承可以看出,UIView的主要任务是响应触摸事件(在责任链中),那UIView是如何实现显示的呢?通过组合,将责任委托给成员变量:每个UIView都有一个Backing Layer,UIView将显示的任务就交给CALayer这个小弟啦,自己作为一个Wrapper,将CALayer一些比较复杂的操作封装成简单已用的接口,供外部使用。下图就是二者的关系图(直接用笔记中的手绘,见谅):

因此对纹理封装的CALayer才是显示的核心,CALayer的contents才指定了真正的要显示的内容,理解了这一点,下面就开始介绍正主contentsScale(现在才开始真是醉了),按照惯例,先看下官方文档,我截取了一段:

This value defines the mapping between the logical coordinate space of the layer (measured in points) and the physical coordinate space (measured in pixels). Higher scale factors indicate that each point in the layer is represented by more than one pixel at render time.
Core Animation uses the value you specify as a cue to determine how to render your content.

官方文档有两个关键点:

  1. contentsScale决定了CALayer的内容如何从Point映射到Pixel。
  2. contentsScale决定了CALayer的内容如何被渲染。

为了理解这两个关键点,就要介绍下CALayer内容的来源了。CALayer内容的来源有两种:

1. 通过Core Graphics自定义绘图到CALayer的Backing Store中

CALayer会根据contentsScale创建Backing Store,并且根据contentScale设置Context的CTM(Concurrent Transform Matrix)。例如,Layer的尺寸为10 Point x 10 Point,当contentsScale为2.0时,最后生成的Backing Store大小为20 Pixel x 20 Pixel,并且在将创建的Graphics Context传入drawLayer:InContext:前,会调用CGContextScaleCTM(context, 2, 2)进行放大,这样会使生成的内容自动满足屏幕的要求。

2. 将CGImage类型的图片设置为CALayer的contents

图片的尺寸是像素相关的,iOS在加载图片时,通过图片的名称进行处理。例如对于@2x的图片,在加载为UIImage时,会将scale设置为2,size为像素大小除以scale,这样就保证了UIImage在Retina屏幕上能够正常显示。但需要注意的是:将UIImage转换成CGImage时会丢失scale属性,使用CGImageGetWidth/Height时得到的是像素尺寸。

与Core Graphics生成内容不同的是,图片作为纹理已经被上传至GPU,CALayer不需要分配Backing Store,contentsScale会在渲染时起作用:对于Retina的屏幕,如果contentsScale为2.0,与屏幕的scale匹配,则渲染系统不对内容进行处理,如果contentsScale为1.0,说明Layer的内容并不匹配屏幕scale,渲染系统会对Layer的内容进行2倍的Scale操作。由于图片本身内容没那么多,于是渲染系统会填充像素,导致模糊。

总之,contentsScale描述了CALayer内容的Scale特性,CALayer在生成内容时会根据contentsScale做处理,并且渲染系统根据contentsScale进行渲染。另,《编写可读代码的艺术》中写到的“名字才是最好的注释”在这里得到很好的诠释,理解了contentsScale之后,再回过头看看命名,清晰准确,值得学习。

需要注意scale的地方

  1. CALayer的contentsScale默认为1.0,只有在使用Core Graphics在drawRect:中自定义绘图时系统才会根据当前屏幕的情况设置,因此以下情况需要设置合适的contentsScale
    • 直接设置CALayer的contents时。
    • 创建新的CALayer时。例如,CATextLayer在Retina屏幕时如果不设置contentsScale,所显示的文字就会模糊。
  2. 在使用Image Context时需要注意:UIGraphicsBeginImageContext以1.0的比例系数创建Bitmap,所以当屏幕为Retina时,在渲染时可能会显得模糊。要创建比例系数为其它值的图片,需要使用 UIGraphicsBeginImageContextWithOptions。

参考

关于使用CALayer中mask的一些技巧

CALayer拥有mask属性,Apple的官方解释如下:

An optional layer whose alpha channel is used to mask the layer’s content. The layer’s alpha channel determines how much of the layer’s content and background shows through. Fully or partially opaque pixels allow the underlying content to show through but fully transparent pixels block that content.

mask同样也是一个CALayer。假设将CALayer本身称为ContentLayer,将mask称为MaskLayer,蒙版(Masking)的工作原理是通过MaskLayer的alpha值定义ContentLayer的显示区域:对于ContentLayer上每一个Point,计算公式为ResultLayer = ContentLayer * MaskLayer_Alpha。所以当alpha为1时Content显示,alpha为0时Content不显示,其他处于0与1之间的值导致Content半透明。

需要注意的是:

  1. MaskLayer的color不重要,主要使用opacity(CALayer中的alpha),但是注意[UIColor clearColor]其实就是alpha为0的color。
  2. ContentLayer超出MaskLayer以外的部分不会被显示出来。
  3. MaskLayer必须是个“单身汉”,不能有sublayers,否则蒙版(Masking)的结果就是未知(Undefined)。

由于mask是一个CALayer,可以通过组合产生很多非常棒的效果。例如可以将MaskLayer指定为CAGradientLayer类型实现Gradient效果,可以给MaskLayer添加动画,下面两个例子就是这种用法的经典实例:

理解anchorPoint,position,frame的关系

对于UIView的frameboundscenter都比较熟悉:

  • bounds指定UIView自身坐标系。
  • center指定UIView在superview中的位置坐标。
  • frame由互不影响(正交)的boundscenter生成。

但是我一直有这样的疑问:为什么要使用UIView中心位置的center来指定坐标,为什么不用其他位置?

后来学习了CALayer,发现UIView的主要任务其实是响应事件(这就是为什么从UIResponer继承的原因),而将显示委托给CALayer处理。一个成功的UIView背后总有一个默默贡献的CALayer,CALayer作为GPU/OpenGL纹理的一种高层封装,处理显示的方方面面,UIView则将CALayer一些功能再次封装,提供简洁好用的接口。像UIView中的framebounds就是直接使用CALayer中的framebounds

但是对于center,CALayer对应项是position,为什么不同名了呢?因为position更灵活,谁规定“指定UIView在superview中的位置坐标”一定要在UIView的中心位置呢?!而position默认在中心位置的目的我觉得是为了方便Rotation,因为一般Rotation都是绕着中心旋转,UIView为了简化使用,满足大部分情况即可,所以就将默认在中心的position封装成了center

由于CALayer的position并没有限制一定要在bounds的中心位置,所以就需要一个属性来描述positionbounds中的位置,这样才能推算出frame的origin点位置。于是anchorPoint出现了,CALayer用anchorPoint指定positionbounds中的位置,有如下特点:

  • 为了可以任意指定位置,因此anchorPoint就不使用绝对值,而是比例值。
  • 由于Rotation总是围绕position旋转,因此指定position在Layer中位置的anchorPoint就被命名为锚点(像船的锚一样固定位置)。

总之,frame是由positionboundsanchorPoint共同生成(其实还有transform,这就需要了解Current Transform Matrix的概念,为了减少复杂性,这里就不涉及了),公式如下:

  • frame.origin.x = position.x - bounds.size.width * anchorPoint.x
  • frame.origin.y = position.y - bounds.size.height * anchorPoint.y

这就解释了:

  1. 为什么anchorPoint改变会导致frame改变,而position却没变?
  2. 为什么anchorPoint要使用比例而不是具体值?

豁然开朗。

PS. 多思考,保持好奇心,多从设计者角度想想为什么这样设计,即使是简单的概念,也会有收获。Stay Hungry,Stay Foolish。

UIView中hidden、alpha、clear color与opaque的区别

关于UIView的透视度,有四个属性(下图中红框中的项)都与之相关,作为一个喜欢刨根问底的程(处)序(女)员(座),一定要搞清楚它们各自的用处跟区别是什么呢?

hidden

此属性为BOOL值,用来表示UIView是否隐藏。关于隐藏大家都知道就是让UIView不显示而已,但是需要注意的是:

  • 当前UIView的所有subview也会被隐藏,忽略subview的hidden属性。UIView中的subview就相当于UIView的死忠小弟,老大干什么我们就跟着老大,同进同退,生死与共!
  • 当前UIView也会从响应链中移除。你想你都不显示了,就不用在响应链中接受事件了。

alpha

此属性为浮点类型的值,取值范围从0到1.0,表示从完全透明到完全不透明,其特性有:

  • 当前UIView的alpha值会被其所有subview继承。因此,alpha值会影响到UIView跟其所有subview。
  • alpha具有动画效果。当alpha为0时,跟hidden为YES时效果一样,但是alpha主要用于实现隐藏的动画效果,在动画块中将hidden设置为YES没有动画效果。

backgroundColor的alpha(Clear Color)

此属性为UIColor值,而UIColor可以设置alpha的值,其特性有:

  • 设置backgroundColor的alpha值只影响当前UIView的背景,并不会影响其所有subview。这点是同alpha的区别,Clear Color就是backgroundColor的alpha为1.0。
  • alpha值会影响backgroundColor最终的alpha。假设UIView的alpha为0.5,backgroundColor的alpha为0.5,那么backgroundColor最终的alpha为0.25(0.5乘以0.5)。

opaque

此属性为BOOL值。要搞清楚这个属性的作用,就要先了解绘图系统的一些原理:屏幕上的每个像素点都是通过RGBA值(Red、Green、Blue三原色再配上Alpha透明度)表示的,当纹理(UIView在绘图系统中对应的表示项)出现重叠时,GPU会按照下面的公式计算重叠部分的像素(这就是所谓的“合成”):

Result = Source + Destination * (1 - SourceAlpha)

Result是结果RGB值,Source为处在重叠顶部纹理的RGB值,Destination为处在重叠底部纹理的RGB值。通过公式发现:当SourceAlpha为1时,绘图系统认为下面的纹理全部被遮盖住了,Result等于Source,直接省去了计算!尤其在重叠的层数比较多的时候,完全不同考虑底下有多少层,直接用当前层的数据显示即可,这样大大节省了GPU的工作量,提高了效率。(多像现在一些“美化墙”,不管后面的环境多破烂,“美化墙”直接遮盖住了,什么都看不到,不用整治改进,省心省力)。更详细的可以读下objc.io中<绘制像素到屏幕上>这篇文章。

那什么时候SourceAlpha为1呢?这时候就是opaque上场的时候啦!当opaque为YES时,SourceAlpha为1。opaque就是绘图系统向UIView开放的一个性能开关,开发者根据当前UIView的情况(这些是绘图系统不知道的,所以绘图系统也无法优化),将opaque设置为YES,绘图系统会根据此值进行优化。所以,如果在开发时某UIView是不透明的,就将opaque设置为YES,能优化显示效率。

需要注意的是:

  1. 当UIView的opaque为YES时,其alpha必须为1.0,这样才符合opaque为YES的场景。如果alpha不为1.0,最终的结果将是不可预料的(unpredictable)。
  2. opaque只对UIView及其subclass生效,对系统提供的类(像UIButton,UILabel)是没有效果的。

参考

UIImage图片拉伸

为什么进行图片拉伸

  1. 灵活:对于一些带边框的背景图而言,如果不进行图片拉伸,每当改变frame时就需要一份新size的背景图
  2. 节约资源:对于一些比较规则的图片,没必须使用frame大小,而是使用一个满足拉伸条件的较小的图片进行拉伸,这样加载图片变快,而且图片大小变小

使用方法

使用- resizeableImageWithCapInsets:(UIEdgeInsets)capInsets,iOS官方解释如下:

You use this method to add cap insets to an image or to change the existing cap insets of an image. In both cases, you get back a new image and the original image remains untouched.
During scaling or resizing of the image, areas covered by a cap are not scaled or resized. Instead, the pixel area not covered by the cap in each direction is tiled, left-to-right and top-to-bottom, to resize the image. This technique is often used to create variable-width buttons, which retain the same rounded corners but whose center region grows or shrinks as needed. For best performance, use a tiled area that is a 1x1 pixel area in size.

原理就是对UIImage指定了Insets后,在Insets中的区域不进行拉伸,只对区域外的部分进行拉伸。需要注意的有两点:

  1. UIEdgeInsetsMake的四个参数从前到后依次是top、left、bottom、right,不是成对的,别搞错了
  2. 拉伸的方式不是全方向一起拉伸!而是先左右拉伸,再上下拉伸,最后的结果是两次拉伸的叠加!不然会出现这样的问题

参考