前言
当我们使用核心动画时,Layer对象是一切的核心。Layers 管理我们APP的可视化content,Layer也提供了content样式及content可视化的外观的调整选项。尽管iOSAPP自动支持Layer,但是OS XAPP必须明确开启Layer的使用才能利用这些相关的性能特点。一旦开启Layer的使用,我们需要去理解如何配置和操作Layers才能得到想要的效果。
为APP开启核心动画的支持
在iOS APP里,核心动画是一直支持的并且每个view 都关联一个Layer(这种view被称为layer-backed view)。在OSX APP中,必须手动开启核心动画的支持,关联QuartzCore framework。(iOS APP必须关联这个framework仅当使用核心动画接口时候)。
开启Layer的支持后,创建layer-backed view是其功能之一,这种View,系统将会负责为其创建Layer对象并保持那个Layer的更新。
调整和View关联的Layer
Layer-backed view 默认创建一个CALayer类的实例,大多数情况下我们不需要其他样式的Layer对象。然而,核心动画提供了其他的Layer类,各个Layer类都具有特殊的功能。选择对应的Layer类有助于我们提高性能,或者有助于以一种更简单的方式显示特殊样式的content。例如CATiledLayer类为高效的展示大图而优化设计的。
改变Uiview的Layer类
我们能够改变iOS View 的Layer类别通过重写layerClass方式并一个不同的类。许多iOS Views创建一个CALayer对象并使用该对象作为content的存储。对于许多我们自己的Views,默认的layer类别就是一个不错的选择。但是在某些特定的场景别的Layer类是更合适的。例如,我们可能想要去改变Layer类在一下场景:
·我们的View绘制content通过使用Metal 或者OpenGL ES,此时我们需要使用CAMetalLayer 或者CAEAGLLayer 对象。
·当有特定的Layer类提供更好的性能。
·我们想要利用某些特定的核心动画Layer类别。例如粒子发射或者粒子复制
改变View的Layer类的方式是很简明的;就像2-1所示一样,我们需要做的只是重写LayerClass方法并且返回我们想要使用的类对象在展示之前View将会先调用layerClass方法,并使用它返回的类对象创建一个Layer对象,一旦创建后,该view的layer对象讲不可更换。
+ (Class) layerClass { return [CAMetalLayer class]; }
相应的Layer类别的列表,和如何使用它们,参见 Different Layer Classes Provide Specialized Behaviors。
各种Layer类提供对应的特性
核心动画定义了许多标准的Layer类,每一个都具有特定的用途。CALayer是根类,它定义的特性其他所有的Layer都必须支持,CALayer也是默认的类别。当然,我们也可以使用2-1表中的Layer类。
Class |
Usage |
---|---|
实现核心动画中粒子发射系统,发射器Layer控制粒子的生成和他们的原点。 |
|
用来画颜色的渐变,以便于用渐变的颜色填充layer上面的形状(一般指绘制的图形)。 |
|
用于设置和渲染可绘制的纹理(用于渲染layer 的content通过使用Metal) |
|
用于设置后端存储和渲染layer 的content的上下文(OpenGL ES或者OpenGL ) |
|
当想要自动拷贝一个或多个子layer时,复制器创建副本,并使用我们指定的属性改变外观或者副本的属性。 |
|
用于管理一个由许多子layer复合而成的可滚动的区域。 |
|
用于画一个三维或者二位的贝塞尔曲线。这种layer在绘制基于path形状方面是有利的,因为他们总会创建出一个完善的path;而我们将path绘制到某个layer的后台存储的,看起来将会有瑕疵当缩放的时候。然而,这个完善的性能牵涉到在主线程渲染该形状并缓存这个结果。(个人觉得他应该是保存的矢量图) |
|
用于渲染文本中普通的或者属性字符串。 |
|
用于管理能够被分割成许多小块并单独渲染的大图,并支持缩小和放大content. |
|
用于渲染真实的3D layer 图层(而不是其他layer类别所展现的平面图层) |
|
用于渲染一个Quartz Composer 合成品 (OS X only)。 |
为Layer提供contents
Layers是管理(由APP提供)content的数据对象。一个Layer的content由包含要被展现的可视化数据的bitmap所组成。我们可以通过以下三种方式提供bitmap:
·直接将一个image对象关联到Layer对象的content属性上。(对于很少或者不会改变的Layer content而言,这是最好的方式。)
·为Layer关联一个代理并让这个代理绘制Layer的content。(对于可能定期或者偶尔改变的Layer content,或者要通过某个对象提供contnent,例如view,这是最好的方式。)
·定义一个Layer的子类并且重写他的绘制方法以便提供Layer的contents。(如果我们不得不创建一个Layer的子类,或者如果我们想要去改变Layer基础的绘制行为,这将是最合适的方法。)
当我们单独创建Layer对象时,也是唯一我们需要考虑怎么为Layer提供content的时候,如果我们的APP只包含layer-backed view,我们不必考虑前面提到的如何提供Layer content的方法。Layer-backed views将会尽可能的以最有效的方式为他们关联的Layer提供contents。
将一个image作为Layer的content
因为一个Layer仅仅是一个管理bitmap image的容器,所以我们直接为Layer的contnent属性关联一个image。将一个图片关联到Layer是很便捷的,并且能让我们轻松的指定一个图片显示到屏幕上,Layer使用我们直接提供大的Image,并且将不会创建那个图片的副本。当我们的APP在多个地方使用同一个图片,这个方式可以节约内存。
我们关联到Layer的图片必须是一个CGImageRef类型。(在OSX v10.6和之后,我们也可以关联NSImage对象。)当关联某个图片的时候,记住要提供和设备的分辨率相匹配的图片。对于Retina屏幕的设备而言,我们需要调整图片的contentsScale属性,更多关于为Layer提供高分辨率contnet可以参见 Working with High-Resolution Images。
通过代理为Layer提供content
如果我们的Layer需要动态改变,当需要改变的时候,我们可以使用代理对象来提供或者更新content,在显示的时候,Layer通过调用代理的方法来提供所需要的content。
·如果代理实现了displayLayer:方法,那么该方法的实现体需要为创建一个bitmap并将该bitmap关联到Layer的content属性中。
·如果代理实现了 drawLayer:inContext:方法,核心动画将会创建bitmap,以及创建用于绘制bitmap的图形context,并调用这个代理方法填充bitmap,此代理方法需要做的就是将所需内容绘制到图形context中。
代理对象必须实现displayLayer: 或者drawLayer:incontext:方法,如果代理同时实现了这两个方法,那么代理仅仅只会执行displayLayer:方法。
当我们的APP需要加载或者创建要显示的bitmap的时候,重写displayLayer:方法是最合适的方法。代码清单2-3展示了displayLayer:代理方法的简单实现过程,在这个例子中,代理对象使用一个协助对象来加载所需要的image。代理方法选择显示哪个image取决于它自身的状态变量,这个变量在例子中就是这得自定义的属性displayYesImage。
- (void)displayLayer:(CALayer *)theLayer {
// Check the value of some state property
if (self.displayYesImage) {
// Display the Yes image
theLayer.contents = [someHelperObject loadStateYesImage];
}
else {
// Display the No image
theLayer.contents = [someHelperObject loadStateNoImage];
}
}
如果我们没有预先渲染bitmap,或者也没有供创建bitmaps的协助对象,那我们需要通过drawLayer:incontext:方法实现动态绘制content。代码清单2-4展示了drawLayer:incontext:方法的实现。在这个例子中,代理使用设定的宽度和渲染颜色绘制了一个简单的曲线path。
- (void)drawLayer:(CALayer *)theLayer inContext:(CGContextRef)theContext { CGMutablePathRef thePath = CGPathCreateMutable(); CGPathMoveToPoint(thePath,NULL,15.0f,15.f); CGPathAddCurveToPoint(thePath, NULL, 15.f,250.0f, 295.0f,250.0f, 295.0f,15.0f); CGContextBeginPath(theContext); CGContextAddPath(theContext, thePath); CGContextSetLineWidth(theContext, 5); CGContextStrokePath(theContext); // Release the path CFRelease(thePath); }
对于需要自定义content的layer-backed views而言,我们仍然需要充写view的方法来实现绘制。一个Layer-backed view将他的Layer的代理关联给他本身,并实现了所需的代理方法,而且我们也不需要手动改变这个配置,不过,我们需要实现这个layer-backed view的drawRect:方法来绘制我们的content。
通过子类提供Layer 的content
如果我们需要实现一个自定义的Layer,我们可以重写Layer的绘制方法做各种各样的绘制。不同于Layer对象自身生成自定义的content,layer需要管理这个用于显示的content(这句话应该是指的是相对于指定image提供content的方式)。例如,CATiledLayer类管理一个大的image的原理是:通过讲image分隔成许多小的可以单独管理和绘制的片段,由于这个Layer有关于在何时需要渲染那一个片段的信息,他直接管理了整个绘制行为。
当自定义子类的时候,我们需要实现下面两种绘制content的方式之一:
·重写子类的display方法,并直接在该方法中设置contents属性。
·重写子类的drawInContext:方法并且使用他在该图形context中进行绘制。
选择重写那种方法取决于我们需要控制绘制过程的程度。display方法是对于更新Layer的content的完全控制,因此重写该方法将使我们完全控制绘制的过程。重写display方法也意味着我们需要创建CGImageRef并将其关联到content属性。如果我们仅仅想要绘制content(或者让我们的layer管理绘制操作),我们可以重写drawInContext:方法,并让Layer创建后备存储。
对我们提供的content进行调整
当将一个image关联到Layer的content属性时候,Layer的contentGravity属性决定了那个图片将被如何调整为了适应当前的bounds。默认的如果一个图片是比当前的bounds更大或者更小的,Layer对象就会缩放这个图片以便适应可用的空间,如果Layerbounds的宽高比是不同于image的宽高比,这将会引起图片显示效果不完全,我们可以使用contentsGravity属性来确保content以最好的方式展现。
可供使用的contentsGravity类型被分为两大种:
·基于position的重力常量,使我们可以在无缩放的情况下,从bounds的某个边或者角铺展image。
·基于缩放的重力常量,使我们可以以某几种方式之一伸缩image,有些选项可以维持宽高比,有些将不维持image的原有宽高比。
图2-1展示了基于点的重力设置对image的影响。除了kCAGravityCenter常量的使用,其他的常量将会以image bounds的某个边或者角来铺展image。kCAGravityCenter常量将image从中心开始铺展。这类常量不会引起图片的伸缩,因此image会按照原来的尺寸进行渲染。如果image是大于Layer的bounds,这将会导致image一部分被裁减掉,如果image的尺寸是小于Layer的bounds,如果设置了layer的背景色,空缺的部分将会显示Layer的背景色。
图2-2展示了基于缩放的重力常量如何影响image显示的。如果image的尺寸和Layer的bounds不一致,所有的这些常量将会缩放image。这些常量的不同之处在于如何调整这些图片原来的宽高比,其中有些模式是保持原来的宽高比,还有一些将会改变原来的宽高比。默认情况下Layer的contentsGravity属性是被设置为kCAGravityResize 常量,这是该类型中唯一不保持图片原有宽高比的图片。
如何使用高分辨率的Images
Layers本身并不知道所在设备屏幕分辨率。Layer仅仅存储指向bitmap的指针并使用所给定的可利用的像素按照尽可能好的方式展现。如果我们将一个Image关联到一个Layer的content属性,我们必须通过设置Layer的contentScale属性告诉核心动画Image的分辨率。ContentScale属性的默认值是1.0,这只适用于那些将要显示到标准分辨率屏幕的Image,如果我们的Image想要显示到Retina屏幕上,那么我们就必须设置这个属性为2.0。
如果我们直接关联一个bitmap到Layer,那么我们就需要设置contentScale的属性。为了适配屏幕的分辨率和被View管理的content,UIKit 和AppKit框架中 layer-backed view会自动设置Layer的缩放因子。例如,在OSX中,如果我们关联一个NSImage对象到Layer的contnet属性,AppKit将会查找标准和更高分辨率的Image,如果都有,那么AppKit将会使用正确的分辨率的image来设置contentScal的属性。
调整Layer的可视化风格和展示样式
Layer对象可以创建可视化的配件来增添Layer的主要contents,例如边框和背景色。这些可视化的配件不需要我们自己做任何的渲染工作,因此在某些情况下就可以使用Layer作为单独的整体。所有需要我们做的仅仅是设置Layer的属性,Layer将会处理所需的绘制工作,当然也包含任何动画。对于可视化配件是如何影响Layer显示的说明可以参见 Layer Style Property Animations。
Layer有他们自己的背景和边框
除了他的基于image的content之外,一个Layer可以显示填充的背景和填充的边框。背景色是在Layer的content后面渲染的,边框是在image的content的上面渲染的,就像图2-3所示。如果Layer包含子Layer,他们也出现在边框的下面。由于背景色是放置在image的后面的,image的透明的部分将会显示背景色。
代码清单2-5显示设置Layer的背景色和边框所需要的代码,所有这些属性都是可动画的。
myLayer.backgroundColor = [NSColor greenColor].CGColor; myLayer.borderColor = [NSColor blackColor].CGColor; myLayer.borderWidth = 3.0;
注意:我们可以使用任何颜色作为Layer的背景色,包括带有透明分量的颜色或者使用样品image。当使用样品image的时候,要注意Core Graphics 处理图片的渲染,以及在此处理图片渲染的过程中将使用标准坐标系统,标准坐标系统和iOS中默认的坐标系统是不同的。在iOS上默认情况下,图片渲染是上下颠倒的,除非我们调整坐标系统。
如果我们设置Layer的背景色为不透明的颜色,那么我们应该讲Layer的opaque属性设置为YES,这么做将会提升性能当合成该Layer到屏幕上显示时候,并消除了layer 作为辅助存储时候管理的透明通道。如果Layer有非0的圆角,我们就不必将该Layer标记为不透明。
Layer支持圆角
通过添加corner radius,我们可以为Layer创建圆角效应。corner radius是一个可视化配件,它可以遮盖Layer的四角让下面的content显示出来。就像图2-4显示的。它涉及到透明遮罩的应用,corner radius不影响image的Layer中的content属性,除非设置masksToBounds属性为YES。然而,corner radiu 一直影响Layer的背景色和边框的绘制。
为了将corner radius应用到Layer,我们需要为Layer的cornerRadius属性指定一个值。圆角的指定单位是points,并且显示的时候,它将会预先应用到Layer的四个角。
Layer支持内建阴影
CALayer 类包含几个为配置阴影效应的属性。阴影会通过让它看起来像是漂浮在content下面的方式来为Layer添加深度。这是另外一种可视化的配件,当APP需要的时候可以使用。对于Layer而言我们可以控制阴影的颜色,相对于content的位置,不透明度,和形状。
Layer阴影的不透明度默认被设置为0,这将会有效的隐藏阴影。改变一个透明度为非零的值,将会引起核心动画绘制阴影。阴影默认是被直接放置在Layer的下面的,为了能够看到阴影,我们也需要调整阴影的偏移量。有个很重要的事情需要记住,我们为阴影指定的偏移量是基于Layer的本地坐标系统的,也就是说在iOS和OSX 上面是不同的。如图2-5展示了一个偏向右下的阴影,在iOS中,需要指定一个正数在Y轴分量上,但是在OSX 上就需要指定为负值。
当为Layer添加阴影的时候,阴影就是Layer的content的一部分,但是实际上阴影有可能超出Layer的边界,如果开启Layer的maskToBounds属性那么阴影的效应将会在Layer的边界处被裁剪,这将会产生一个奇怪的现象,那就是在Layer的内部的阴影是显示的,但是在Layer的边界外部的阴影缺看不到。如果我们想要显示完整的阴影并且还想使用bounds masking,我们就应该使用两个Layer而不是一个,将mask应用到包含content的Layer上,然后将这个Layer嵌入到第二个Layer上,第二个Layer的尺寸和第一个的尺寸一样,第二个Layer上设定阴影效果。
更多关于如何Layer上如使用阴影可参见Shadow Properties。
为Layer添加自定义属性
CAAnimation 和CALayer 类延伸了KVC以便支持自定义属性。我们可以其使用这个特性为Layer添加数据,然后通过自定义的属性获取该数据。我们甚至可以关联actions到自定义的属性中,进而达到当修改这个属性的时候,对应的动画将会被执行。
为更多的信息关于如何使用和设置自定义属性,可以参见 Key-Value Coding Compliant Container Classes。为更多的信息关于添加actions到Layer对象,可以参见 Changing a Layer’s Default Behavior。
打印Layer-backed View的内容
在打印期间,Layer 会重绘他们的contents为了适配打印环境。核心动画正常情况下依靠缓存的bitmap当渲染到屏幕上的时候,然而他将会重绘制content当打印的时候。尤其,layer-backed view使用drawRect:方法提供Layer 的content,当打印的时候,核心动画将再次调用drawRect:来生成需要打印的Layer的content。