[关闭]
@evolxb 2016-07-16T17:28:39.000000Z 字数 22124 阅读 1578

programming ios - layer (one)

ios view layer


每个UIView有一个伙伴称为layer,一个CALayerUIView实际上并没有把自己画到屏幕上;它绘制本身到它的layer上,它的layer被绘制到屏幕上。正如我已经提到的,视图不会被经常重绘;相反,它的绘制会被缓存,在可用的地方都会使用缓存版本(bitmap backing store)。缓存的版本,实际上,就是layer。视图的图形上下文话实际上是layer的图形上下文。

这似乎仅仅是一个实现细节,但layer是非常重要和有趣。理解layer能更深刻地理解视图;layer延伸了视图的能力。 尤其是:

例如,假设我们要添加一个指南针到我们的应用程序的界面。下图展示了一个指南针的一个简单的版本。它利用了我们前面例子中绘制的箭头;箭头被绘制到自身的layer中。罗盘的其它部分也是layer:圆是一个layer,并且每个基点字母是一个layer。这样绘制代码易于复合;更耐人寻味,不同的部分可用独立放置,并分别动画,所以很容易不移动圆圈就旋转箭头。

View and Layer

一个UIView实例有一个CALayer的实例,通过视图layer属性访问。这一layer具有特殊的地位:它与视图合作来显示所有视图的绘制。该layer没有相应的视图属性,但该视图是layer的委托。文档有时也会说layer是视图的underlying layer

默认情况下,当一个UIView被实例化,其layerCALayer的实例。如果你的UIView子类,想改变underlying layer的类型,实现UIView子类的layerClass类方法并返回CALayer的子类。

下面是创建指南针的代码。我们有一个UIView子类,CompassViewCALayer的子类,CompassLayer。这里是CompassView的实现:

  1. class CompassView: UIView {
  2. override class func layerClass() -> AnyClass {
  3. return CompassLayer.self
  4. }
  5. }

效果如下图:

因此,当CompassView实例化时,其下层是一个CompassLayer。在这个例子中,CompassView中没有任何绘制。它的工作-在此情况下,其一的工作 - 是让CompassLayer在界面上显示,因为一个层不能脱离视图单独出现在界面上。

因为每个视图有一个underlying layer,这两者之间紧密结合。layer描绘所有视图的绘制;如果视图绘制,它通过layer来绘制。视图是层的委托。视图的属性往往只是用于访问层的属性。例如,当你设置视图的的backgroundColor,你只是在设置layerbackgroundColor,如果你直接设置layerbackgroundColor,视图的backgroundColor会自动匹配。同样,视图的frame其实是该layerframe,反之亦然。

一个CALayerdelegate属性是可设置的,可以是任何基于NSObject类的一个实例(CALayerDelegate是一个非正式的协议,通过分类注入NSObject类中)。但一个UIView和它的layer有一种特殊的关系。一个UIView必须是layerdelegate;而且,它不能是任何其它layerdelegate。不要做任何事情,如果你搞砸了,绘图将不会正常工作。

视图绘制到它的layer,然后layer缓存这些绘制;然后layer可以被操纵,从而改变视图的外观,而不必要求视图重绘自身。这是绘图系统高效率的原因。这也解释了前面例子中视图拉神的原因:当视图大小变化时,默认情况下,绘图系统只是简单的伸展或重新定位缓存的layer图像,直到视图被告知刷新(drawRect:),从而替换layer的内容。

Layers and Sublayers

layer可以有子layer但是最多只能有一个superlayer。因此存在一个layer的树形结构。这和视图的树形结构是类似的。事实上,视图和它的layer如此紧密,这些层次结构是相同的层次结构。给定一个视图及其layer,该layersuperlayer是该viewsuperviewlayer,该layer的子layerviewsubviewlayer。事实上,由于layer展示了view如何被绘制,有人可能会说,视图层次只是一个layer层次结构。如下图:

同时,layer的层次结构可以超越视图的层次结构。一个视图只有一个layer,但layer可以有不属于任何视图的underlying layer子层。因此视图的underlying layer层次结构和视图的层次完全匹配,但总的layer树型结构可以是这个结构的一个超集。在下图中有和上图一样的层级结构,但有两个layer具有它们自己单独的layer子层(即子layer不属于任何视图的underlying layer)。
如下图:

从视觉角度看,layer的层次结构和视图的层次结构没有区别。例如,在前面例子中,我们三个重叠视图来绘制重叠的矩形。下面的代码通过操作layer来实现相同的视觉效果:

  1. let layer1 = CALayer()
  2. layer1.backgroundColor = UIColor(red: 1, green: 0.4, blue: 1, alpha: 1).CGColor
  3. layer1.frame = CGRectMake(113, 111, 132, 194)
  4. mainview.layer.addSublayer(layer1)
  5. let layer2 = CALayer()
  6. layer2.backgroundColor = UIColor(red: 0.5, green: 1, blue: 0, alpha: 1).CGColor
  7. layer2.frame = CGRectMake(41, 56, 132, 194)
  8. layer1.addSublayer(layer2)
  9. let layer3 = CALayer()
  10. layer3.backgroundColor = UIColor(red: 1, green: 0, blue: 1, alpha: 1).CGColor
  11. layer3.frame = CGRectMake(43, 197, 160, 230)
  12. mainview.addSublayer(layer3)

效果如下图:

视图的子视图的layer是这一视图的underlaying layersublayer,就像该视图的underlaying layer的任何其他子layer。因此,在绘制顺序中可以把它们放在任何地方。视图可以被分散到superviewlayersublayer中,这通常令初学者非常惊讶。例如,让我们重新构造上图,但是在layer2layer3中间,我们将添加一个子视图:

  1. // ...
  2. layer1.addSublayer(layer2)
  3. let iv = UIImageView(image: UIImage(named: "smiley"))
  4. mainview.addSubview(iv)
  5. iv.frame.origin = CGPointMake(180, 180)
  6. let layer3 = CALayer()

效果如下图:

笑脸在红色矩形前面被添加到界面上所以看来在矩形的后面。通过颠倒其中红色矩形和笑脸加入到该界面的顺序,笑脸可以出现在该矩形的前面。笑脸是一个视图,而矩形只是一个layer;所以他们没有像视图之间的兄弟关系,因为矩形不是一个视图。但笑脸是视图及其layer;作为layer,笑脸和矩形是兄弟关系,因为它们具有相同的superlayer,所以一个可以出现在另一个的前面。

layer是否超出自己的边界之外的子layer的区域取决于它的masksToBounds属性的值。这和视图的clipsToBounds属性相似,而事实上,因为layer是视图的underlying layer,所以它们是同一个东西。在上面2张图中,layersclipsToBounds都设置为false(默认值);这就是为什么右侧layer超出的中间layer的原因。

UIView类似,一个CALayer的具有一个hidden属性可以把它和它的子layer在界面中隐藏而不用从它的superlayer中移除。

Manipulating the Layer Hierarchy

layer使用了和视图相似的一整套方法来读取和操纵layer的层次结构。layer有一个superlayer属性和sublayers属性,以及下面的方法:

不同于视图的subviews属性,layersublayers属性是可写的;因此,你可以通过sublayers属性一次性给layer设置多个sublayer。通过设置sublayersnil来移除layer的所以子layer

虽然一个layer的子layer有顺序,可以通过上面提到的方法和sublayers属性来操纵顺序,但这并不和绘制的顺序完全相同。默认情况下,layer有一个CGFloat类型的zPosition属性值,这也决定了绘制顺序。绘制规则是相同的zPosition的所有子layersublayers属性所列的顺序绘制,但较低的zPosition属性比较高的zPosition属性的layer先绘制。 (默认的zPosition0.0)。

有时,zPosition属性比兄弟顺序更方便的决定绘制顺序。例如,如果layer代表一个纸牌游戏的扑克牌,可能会更容易和方便通过设置zPosition而不是子layer自己的兄弟关系。此外,子视图的layer本身只是一个layer,这样你就可以通过设置它们的zPosition来重新排列子视图的绘制顺序。在上图中,如果我们指定图像视图的layerzPosition1,它会被绘制在红色矩形的前面:

  1. mainview.addSubview(iv)
  2. iv.layer.zPosition = 1

还有一些方法提供了用于在同一layer层次结构内各layer的坐标系统之间的转换方法:

Positioning a Sublayer

layer坐标系统和定位和视图的那些类似。layer有自己的内部坐标系统是由它bounds属性表示,就像视图一样。它的大小是它的bounds大小,内部坐标系统的原点在它的左上角。

然而,sublayer在它的superlayer中的位置不是由它的center属性决定的;因为layer没有center。相反,sublayersuperlayer中的位置由2个属性联合决定:

这里有一个比喻;并不是我创造的,但它是相当贴切。想象把sublayer用图钉固定到superlayer;那么你不得不说这个针在什么地方穿过sublayeranchorPoint),并固定在superlayer的哪个位置(position)。

如果anchorPoint为(0.5,0.5)(默认值),position属性和viewcenter属性一样。因此视图的centerlayerposition的一种特殊情况。这是比较典型的视图属性和图层特性之间的关系;视图属性往往是一个简单的 - 但不那么强大 - 的layer属性的版本。

图层的positionanchorPoint是正交(独立的);改变一个不会改变另一个。因此,改变它们中的任一个,都可以改变layersuperlayer中的绘制的位置。

例如,在第一张图中,圆圈的最重要的一点是其中心;所有其他对象需要相对于它来定位。因此,它们都具有相同的position:该圆的中心。但它们的anchorPoint不同。例如,箭头的anchorPoint(0.5,0.8)轴的中间靠近尾部。在另一方面,数字基点anchorPoint(0.5,3.0),已经超过字母的边界,在圆形表盘的边缘附件。

layerframe属性是一个纯粹的衍生属性。当你获取frame时,它是从边界尺寸与positionanchorPoint计算出来的。当你设置frame时,你设置边界的大小和position。一般情况下,你应该把frame作为一个便利的属性,这非常方便!例如,定位一个子层,使其恰好重叠superlayer,你可以设置子层的framesuperlayerbounds

在代码中创建(而不是一个视图的underlying layer)的layer的frame和bounds都是(0.0,0.0,0.0,0.0),当你把它添加到屏幕上的superlayer中它都是不可见的。如果你希望能够看到它给你的layer非零宽度和高度。创建layer并将它添加到一个superlayer然后发现它为什么没有在界面中出现是一种常见的初学者错误。

CAScrollLayer

如果你将要移动一个layer的边界原点作为重新定位其子层位置的方式,你可能想使用CAScrollLayer,一个CALayer的子类,提供了这样的方便的方法。(尽管是这样的名字,一个CAScrollLayer不提供滚动界面,用户无法通过拖拽来滚动它。)默认情况下,CAScrollLayermasksToBounds属性为true;因此,CAScrollLayer就像window一样你只能看到它边界以内的东西。(你可以设置它的masksToBoundsfalse,但是这是一件奇怪的事,因为它有点和目的相背。)

要移动CAScrollLayer的边界,你可以直接告诉它或者它的任何sublayer

Layout of Sublayers

视图层次结构实际上是一个layer层次结构。视图在父视图的位置实际上是其layersuperlaye内的定位。一个视图可以被重新定位,通过其autoresizingMask或通过根据它的约束自动调整大小。因此,如果layer是视图的underlying layer它会自动调整大小。否则,iOS不会对layer自动调整大小。因此不是视图的underlying layersublayer只能用代码手动调整大小。

layer的边界更改或者调用setNeedsLayout,此时layer需要调整布局,你可以通过以下两种方式响应:

为了有效布局sublayer,你可能需要一种方法来识别或引用子layer。layer中没有viewWithTag:这样的方法,因此怎样识别和引用layer完全取决于你。键-值编码可能是有用的;layer以一种特殊的方式实现了键值编码。

对于视图的underlying layer来说,在视图的layoutSubviews被调用后,layerlayoutSublayerslayoutSublayersOfLayer:也会被调用。在自动布局中,你必须调用super否则自动布局会崩溃。此外,这些方法在自动布局过程中可能被调用一次以上;如果你正在寻找手动布局layer的时机,视图的布局事件可能是更好的选择。

Drawing in a Layer

layer中显示一些东西的最简单的方法是通过它的contents属性。这和UIImageViewimage属性很相似。它期望一个CGImage(或nil,表示没有图像)。因此,举例来说,下面是通过layer而不是视图来生成笑脸的代码:

  1. let layer4 = CALayer()
  2. let im = UIImage(named: "smiley")!
  3. layer4.frame = CGRect(origin: CGPointMake(180, 180), size: im.size)
  4. layer4.contents = im.CGImage
  5. mainview.layer.addSublayer(layer4)

设置layer的contents为一个UIImage,而不是一个CGImage,会默默地失败 -- 影像不会出现,但没有任何错误。这绝对会发疯,每一个操作我都已经做到了,然后浪费时间搞清楚为什么我的layer没有出现。

这有4种方法来为layer提供需要的内容,类似于UIViewdrawRect:方法.layer会非常保守的调用这些方法(你不能直接调用其中的任何方法)。当layer调用了这些方法,这就是说layer重新显示自己。下面是引起layer重新显示自己的方式:

下面的四种方法可以被调用用来使layer重新显示自己;选择一个实现(不要重复实现它们,否则你会奔溃):

layercontents分配一个图像和直接在layer里绘制在效果上是相互排斥的。 所以:

如果layer是视图的underlying layer,你通常不会使用四种方法种的任何一种来绘制到layer:你会使用视图的drawRect:.但是,你也可以使用这些方法,如果你真的想。在这种情况下,你可能会想实现一个空的drawRect:方法。其原因是,这会导致layer在适当的时刻重新显示本身。当一个视图被发送setNeedsDisplay消息 - 包括当视图首次出现时 - 视图的underlying layer也会被发送setNeedsDisplay消息,除非视图没有实现drawRect:(因为在这种情况下,假定该视图永远不需要重绘)。所以,如果直接使用视图的underlying layer来绘制整个视图,而且当视图需要重绘自己时视图的underlying layer在某个时刻要自动重新显示自己,那么你应该实现一个空的drawRect:方法。 (该技术对underlying layer的子layer没有任何影响。)

下面这些都是能够绘制到视图(但不常用的)方法:

切记,你不能设置视图的underlying layerdelegate属性!视图是它的layerdelegate,并且必须保持其delegate。通过delegate绘制到layer的一个有用的架构是将一个视图当作layer-hosting:视图及其underlying layer只用保持一个sublayer,所以的绘制都方法在sublayer中。如下图:

layer有一个contentsScale属性,这会把layer中的图形上下文中的点距映射到设备上的像素距离,由Cocoa管理的layer,如果它由内容其contentsScale属性会被自动调整;例如,一个实现drawRect:的视图,在双分辨率的设备上其underlying layercontentsScale属性会被设置为2.你自己创建并管理的layer是没有这种福利的,都得你自己手动设置;如果你想在layer中绘制,那么正确的设置layercontentsScalecontentsScale1的的layer的绘制内容在高分辨率的设置上看起来很模糊。如果layercontents属性为一个UIImageCGImage,而去UIImagescale属性和layerscale属性不匹配,那么图片会以一个错误的大小显示。

三个layer的属性强烈地影响层layer的显示,而去非常不好理解:

Content Resizing and Positioning

layer的内容会被做为图片缓存为位图, 然后根据layer的各种属性绘制到layerbounds内:

layer的属性问题会导致layer重新显示自己时,缓存的内容会被调整大小,重新定位,裁剪等等,这些属性是:


layerlayercontentsGravitylayercontentsGravitycontentsRect(0.0,0.0,1.0,1.0)contentsGravitylayer

  1. arrow.needsDisplayOnBoundsChange = false
  2. arrow.contentsCenter = CGRectMake(0.0, 0.4, 1.0, 0.6)
  3. arrow.contentsGravity = kCAGravityResizeAspect
  4. arrow.bounds.insetInPlace(dx: -20, dy: -20)

Layers that Draw Themselves

Transforms

通过变换(transform)可以修改layer在屏幕上的绘制。因为视图可以有一个transfrom,而且视图是通过其layer绘制到屏幕上的。但是的layer的变换比视图的变换功能更强大;你可以使用它来完成你不能用一个视图变换独自完成的事。

在最简单的情况下,当变换是二维的时候,你可以访问layerAffineTransform方法来访问layer的变换。变换施加于anchorPoint的。

你现在已经知道了生成指南针的所以代码含义。在这段代码中,selfCompassLayer;它没有绘制自己,而仅仅只是配置它的子layer。这四个基本点的字母分别由CATextLayer绘制;它们在相同的坐标系统中绘制,但它们具有不同的旋转变换,而且被固定使得它们的旋转是以圆的中心为中心。为了生成箭头,我们使自己成为箭头layerdelegate,并调用setNeedsDisplay;这导致drawLayer:inContextCompassLayer被调用。箭头layeranchorPoint钉扎其尾部在圆的中心,并且通过变换围绕固定点旋转:

  1. // gradient
  2. let grad = CAGradientLayer()
  3. grad.contentScale = UIScreen.mainScreen().scale
  4. grad.frame = self.bounds
  5. grad.colors = [
  6. UIColor.blackColor().CGColor,
  7. UIColor.redColor().CGColor
  8. ]
  9. grad.locations = [0.0, 1.0]
  10. self.addSublayer(grad)
  11. // circle
  12. let circle = CAShapeLayer()
  13. circle.contentsScale = UIScreen.mainScreen().scale
  14. circle.linewidth = 2.0
  15. circle.fillColor = UIColor(red: 0.9, green: 0.95, blue: 0.93, alpha: 0.9).CGColor
  16. circle.strokeColor = UIColor.grayColor().CGColor
  17. let p = CGPathCreateMutable()
  18. CGPathAddEllipseInRect(circle)
  19. circle.path = p
  20. self.addSublayer(circle)
  21. circle.bounds = self.bounds
  22. circle.position = self.bounds.center
  23. // four cardinal points
  24. let pts = "NESW"
  25. for (ix, c) in pts.characters.enumerate() {
  26. let t = CATextLayer()
  27. t.contentsScale = UIScreen.mainScreen().scale
  28. t.string = String(c)
  29. t.bounds = CGRectMake(0, 0, 40, 40)
  30. t.position = circle.bounds.center
  31. let vert = circle.bounds.midY / t.bounds.height
  32. t.anchorPoint = CGPointMake(0.5, vert)
  33. t.alignmentMode = kCAAlignmentCenter
  34. t.foregroundColor = UIColor.blackColor().CGColor
  35. t.setAffineTransform = CGAffineTransform(
  36. CGAffineTransformMakeRotation(CGFloat(ix) * CGFloat(M_PI) / 2.0))
  37. circle.addSublayer(t)
  38. }
  39. // arrow
  40. let arrow = CALayer()
  41. arrow.contentsScale = UIScreen.mainScreen().scale
  42. arrow.bounds = CGRectMake(0, 0, 40, 100)
  43. arrow.position = self.bounds.center
  44. arrow.anchorPoint = CGPointMake(0.5, 0.8)
  45. arrow.delegate = self //will draw arrow in delegate method
  46. arrow.setAffineTransform(CGAffineTransformMakeRotation(CGFloat(M_PI) / 5.0))
  47. self.addSublayer(arrow)
  48. arrow.setNeedDisplay()

一个完备的layer变换会发生在三维空间;其包括一个z轴,垂直于x轴和y轴。 (默认情况下,z轴正方向指向屏幕外面,指向用户的脸)。layer不会奇迹般地给你逼真的三维渲染--你可以使用OpenGL来实现三维渲染,这不在本文的讨论范围之内。layer是二维对象,它们被设计的足够简单和快速。尽管如此,它们也可以在三维上操作,而且特别的快速真实,特别是执行动画时尤其如此。我们都看到过屏幕上的图像翻转像翻一张纸一样的效果而且可以显示背面的东西;这是在三维空间的旋转。

三维变换需要围绕anchorPoint,其z分量由anchorPointZ属性提供。因此,在anchorPointZ0.0的默认情况下,anchorPoint是足够的,正如我们使用CGAffineTransform时已经看到。

transform本身被一个称为CAtransform3D的数学结构描述。Core Animation Function Reference中列出了一些操作此结构的函数。他们很像CGAffineTransform,除了它们有第三个维度。例如,用于制造二维尺度变换的函数,CGAffineTransformMakeScale,有两个参数;用于制作3D尺寸变换的函数,CATransform3DMakeScale,有三个参数。

旋转3D转换是有点复杂。除了角度,你也必须提供三个坐标描述围绕其旋转发生的向量。也许你已经从你的高中数学的知识中忘了什么是向量,或者试图在你的脑袋中可视化三维向量。这真的很复杂。。。

假设该锚点为原点,(0.0,0.0,0.0)。现在想象一下从锚点发出一个箭头;它的另一端,它的结束点,由你你提供的三个坐标表示。现在,假设有一个相交于锚点并且垂直于箭头的平面。这就是旋转发生的平面;正角度值是顺时针旋转,就像下图中平面的侧面看到的效果。在效果上,你提供的三个坐标(相对于锚固点)你的眼睛都不得不将这张旋转看成以前的二维旋转。

矢量指定一个方向,而不是一个点。因此它对于你给的坐标标量没有区别:(1.0,1.0,1.0)(10.0,10.0,10.0)是相同的方向。如果三个值是(0.0,0.0,1.0),那么就是一个简单的CGAffineTransform,因为旋转平面就是屏幕。如果这三个值是(0.0,0.0,-1.0),这是一个反向的CGAffineTransform,使得正值角度看起来是逆时针旋转的(因为我们在旋转平面的背面看到的效果)

layer可以通过旋转显示它的背面。例如,下面的layer旋转翻转绕其y轴:

  1. someLayer.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0, 1, 0)

默认情况下,该层被认为是双面的,所以当它被翻转,以显示背面的时候,显示的是它的layer的内容的逆向版本(连同子layer和它们的所以坐标系统)。但是,如果该layerdoubleSided属性为false,那么当它被翻转显示其背面的时候;它的“背面”是透明的而且也是空的。

Depth

有两种方式来放置layer在不同的深度。一种是通过它们的位置,就是zPosition属性。另一种是在z轴上施加一个平移变换来改变layer的位置。layerpositionz分量(zPosition)和在z轴的偏移量这两个量是相关的;在某种意义上说,zPosition是在z方向的平移变换的简写形式。 (如果你同时提供zPositionz方向平移变换,那么你会非常迷惑。)

在现实世界中,改变一个对象的zPosition会使其显示更大或更小,因为它和眼睛的距离更近或更远;但是layer的绘制和真实世界不一样。这里没有视角的概念;layer在平面上按照它们真实的大小绘制而且叠在一起没有间隙。(这就是所谓的正投影,并且蓝图经常以这样的方式从侧面显示一个物体)。

然而,有一个广泛使用的技巧对于layer的绘制:使它的子layersublayerTransform属性映射所以的点到一个“远端”的平面。(这可能是关于sublayerTransform属性唯一的作用。)与正投影相结合,效果是将点透视应用到绘制中,使得z轴负方向视角更小。

例如,让我们尝试采用一种“翻页”旋转到我们的指南针上:我们会在它的右侧固定,然后绕Y轴旋转。这里,我们旋转的子层(通过属性,rotationLayer访问)是渐变层,并且圆和箭头是其子层,使得它们一起旋转:

  1. self.rotationLayer.anchorPoint = CGPointMake(1, 0.5)
  2. self.rotationLayer.position = CGPointMake(self.bounds.maxX, self.bounds.midY)
  3. self.rotationLayer.transform = CATransform3DMakeRotation(CGFloat(M_PI) / 4.0, 0, 1, 0)

结果如上图;指南针看起来被压扁。然而,现在,我们将适用距离映射转换。这里的superlayer是就是self

  1. var transform = CATransform3DIdentity
  2. transform.m34 = -1.0 / 1000.0
  3. self.sublayerTransform = transform


结果如上图显示还可以,你可以用其他值来代替1000.0试试各种效果;例如,500.0给出了一个更夸张的效果。此外,rotationLayerzPosition也会影响它得大小。

绘制layer随深度改变大小的另一种方法是使用CATransformLayer。这CALayer的子类没有做任何关于自己的绘制;它的目的仅仅是作为其它layer的宿主。它最显着的特征是你对它应用一个变换,它就会保持自己的子层之间的深度关系。 例如:

  1. // layer1 is a layer, f is a CGRect
  2. let layer2 = CALayer()
  3. layer2.frame = f
  4. layer2.backgroundColor = UIColor.blueColor().CGColor
  5. layer1.addSublayer(layer2)
  6. let layer3 = CALayer()
  7. layer3.frame = f.offsetBy(dx: 20, dy: 30)
  8. layer3.backgroundColor = UIColor.greenColor().CGColor
  9. layer3.zPosition = 10
  10. layer1.addSublayer(layer3)
  11. layer1.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0, 1, 0)

在代码中,superlayer layer1有两个子层,layer2layer3。子层以上述顺序加入,所以LAY3lay2的前面绘制。然后通过设置layer1transformlayer1执行了一个翻书页的翻转变换。如果lay1是正常的CALayer的子层则绘制顺序不会改变;LAY3仍然绘制在layer2的前面即使添加翻转变换后。但是,如果lay1CATransformLayer,在翻转变换后layer3会绘制在layer2的后面;因为它们都是lay1`的子层,因此它们的深度关系保持不变。

下图仍然通过给self设置sublayerTransform执行翻转变换,不过这一次self唯一的子layerCATransformLayer

  1. var transform = CATransform3DIdentity
  2. transform.m34 = -1.0 / 1000.0
  3. self.sublayerTransform = transform
  4. let master = CATransformLayer()
  5. master.frame = self.bounds
  6. self.addSublayer(master)
  7. self.rotationLayer = master

效果如下图:

执行翻转变换的CATransformLayer,持有渐变layercircle layer,箭头layer。这三个层在不同深度(使用不同的zPosition设置),给箭头添加阴影从圆形表盘中分离:

  1. circle.zPosition = 10
  2. arrow.shadowOpacity = 1.0
  3. arrow.shadowRadius = 10
  4. arrow.zPosition = 20

你可以明显的看到圆圈层漂浮在渐变层上面,在旋转变换执行的过程中使用动画可能效果更好。

为了更显著,我添加了一个白色的小挂钩,通过固定箭头然后扎入圆圈里面!这是一个CAShapeLayer,旋转到垂直于CATransformLayer

  1. let peg = CAShapeLayer()
  2. peg.contentsScale = UIScreen.mainScreen().scale
  3. peg.bounds = CGRectMake(0, 0, 3.5, 50)
  4. let p2 = CGPathCreateMutable()
  5. CGPathAddRect(p2, nil, peg.bounds)
  6. peg.path = p2
  7. peg.fillColor = UIColor(red: 1.0, green: 0.95, blue: 1.0, alpha: 0.95).CGColor
  8. peg.anchorPoint = CGPointMake(0.5, 0.5)
  9. peg.position = master.bounds.center
  10. master.addSublayer(peg)
  11. peg.setValue(M_PI / 2, forKeyPath: "transform.rotation.x")
  12. peg.setValue(M_PI / 2, forKeyPath: "transform.ratation.z")
  13. peg.zPosition = 15

上面的代码实际上给我们的layer做了一个3d模型。

Shadows, Borders, and Masks

一个CALayer具有很多影响绘制细节的属性。这也是UIView高效绘制的原因,因为它们能作用于viewunderlying layer

一个CALayer可以有阴影,由shadowColorshadowOpacityshadowRadiusshadowOffset属性定义。为使该层绘制阴影,shadowOpacity应该设置为非零值。阴影通常是根据该层的不透明区域的形状绘制,但得到该形状是cpu密集型的。您可以通过自己定义形状和把形状做为CGPath赋值给shadowPath属性,这会大大提高性能。

如果图层的masksToBounds是true,边界之外的阴影不会被绘制。。

CALayer可以有一个边框(borderWidthborderColor);borderWidth在边框的里面绘制,这可能会遮盖一部分内容,除非你有其他的处理。

CALayer可通过cornerRadius来设置圆角矩形。如果该层有边框,也有圆角。如果该层具有backgroundColor,那么背景颜色会被剪裁到圆角矩形的形状。如果该层的masksToBoundstrue,图层的内容和它的子层会被圆角裁剪。

CALayer可以有一个遮罩(mask)。如果它本身就是一个层,其内容必须被以某种方式提供。mask的内容在特定部分的透明度会成为layer在相应部分的透明度。mask的颜色没有任何用处,只有透明度有用,要放置一个mask,把它做为一个子layer

下图显示了我们的箭头,一个灰色圆形层在它下面,并且施加了一个mask:它是一个椭圆,用不透明颜色填充并且厚的半透明的颜色描边。代码如下:

  1. let mask = CAShapeLayer()
  2. mask.frame = arrow.bounds
  3. let path = CGPathCreateMutable()
  4. CGPathAddEllipseInRect(path, nil, CGRectMake(mask.bounds, 10, 10))
  5. mask.strokeColor = UIColor(white: 0.0, alpha: 0.5).CGColor
  6. mask.lineWidth = 20
  7. mask.path = path
  8. arrow.mask = mask

效果如下:

结合cornerRadius,masksToBoundsmask,可以以更通用的方式来执行。例如,下面是产生远角mask的方法:

  1. func maskOfSize(size: CGSize, roundingCorners rad: CGFloat) -> CALayer {
  2. let r = CGRect(origin: CGPointZero, size: size)
  3. UIGraphicsBeginImageContextWithOptions(r.size, false, 0)
  4. let context = UIGraphicsGetCurrentContext()
  5. CGContextSetFillColorWithColor(context, UIColor(white: 0, alpha: 0).CGColor)
  6. CGContextFillRect(context, r)
  7. CGContextSetFillColorWithColor(context, UIColor(white: 0, alpha: 1).CGColor)
  8. let p = UIBezierPath(roundedRect: r, cornerRadius: rad)
  9. p.fill()
  10. let im = UIGraphicsGetImageFromCurrentImageContext()
  11. UIGraphicsEndImageContext()
  12. let mask = CALayer()
  13. mask.frame = r
  14. mask.contents = im.CGImage
  15. return mask
  16. }

从上面方法返回的layer可以做为任何layermask。其结果是,layer的所以内容包括子layer都被剪切到圆角矩形的形状;该形状之外的一切都没有绘制。这只是使用mask实现的一个例子。mask可以具有不透明和透明的之间的值,并且它可以是任何形状。透明区域不一定非得在mask区域外面;可以使用一个外部不透明内部透明的mask来给layer打孔。

你可以给视图设置mask通过maskView属性。这可能很便利但是没有layer层来的高效;这本质上还是给底层的layer设置mask。因此,这并不能解决mask的大小调整问题。

Layer Efficiency

现在,你可能对layer实现各种mask。这没有什么不妥,但是当iOS设备把绘图从一个地方转移到另一个地方,设置可能不能迅速的响应这些请求。这类问题很可能出现尤其是当你执行的动画或当用户能够通过触摸动态绘制,滚动表视图时。您可以通过肉眼来发现这些问题,你可以通过使用InstrumentsCore Animation template来显示动画期间所取得的帧速率来发现这些问题。模拟器的DEBUG菜单也能让你发现一些颜色叠加导致的绘图效率问题。

在一般情况下,不透明绘制是最有效的。(非不透明绘制在Instruments上用红色标记为“blended layers”。)如果一个图层将始终显示在单一颜色的背景上,你可以给它设置相同颜色的背景;

另一种方法来获取效率提升是通过“冻结”绘图的全部做为位图。实际上,你先绘制到一个二级缓存,然后把缓存绘制到屏幕。从缓存绘制比直接绘制到屏幕效率低,但是如果layer层次很深很复杂就不用每次都去渲染整棵树了。要做到这一点,设置图层的shouldRasterizetrue,设置rasterizationScale一些有意义的值(可能UIScreen.mainScreen().scale)。您可以随时设置shouldRasterizefalse,来关闭栅格化,所以很容易在一些很混乱的屏幕重排之前开启然后在关闭它。

此外,还有一个图层属性drawsAsynchronously。默认为false。如果设置为true,该层的图形上下文积累绘图命令,然后在某个恰当的时刻在后台线程绘制。因此,您的绘图命令运行速度非常快,因为他们实际上不是在你发送绘制命令时绘制。我还没有机会使用这个,但是可能在你需要时间比较长的绘制的时候有效果。

Layers and Key–Value Coding

所有图层属性都可以通过具有相同名称的属性键的键值编码来访问。因此,为layer添加mask,可以这样:
layer.mask = mask
也可以这样:
layer.setValue(mask, forKeyPath: "mask")
此外,CATransform3DCGAffineTransform值可以通过键 - 值编码和key path表示。例如,:
self.ratationLayer.transform = CATransform3DMakeRotation(CGFloat(M_PI) / 4.0, 0, 1, 0)
也可以这样:
self.rotationLayer.setValue(M_PI / 4, forKeyPath: "transform.rotation.y")
这种表示法是行的通的,因为CATransform3D是键--值编码兼容。这些都不是属性,因为CATransform3D不具有属性。它没有任何属性,因为它都不是一个对象。你不能说:
self.ratationLayer.transform.rotation.y = ... //.. fail.
你经常会这样使用transform

甚至你可以把CALayer作为一种字典,获取和设置任意键的值。这意味着你可以将任意信息附加到一个单独的层实例,并在以后检索。例如,手动布局layer需要先引用到此layer。那么可以这样做:

  1. myLayer1.setValue("Foo", forKey: "name")
  2. myLayer2.setVlaue("Foo2", forKey: "name")

图层没有一个name属性;'name'属性是我附加给layer的。现在,我可以通过获取各自的“name”键的值后确定这些层。

另外,CALayerdefaultValueForKey:类方法;实现它,你需要继承和覆盖此方法。在需要提供特定键一个默认值的情况下,返回默认值,;否则,返回来自调用super的返回值。因此,即使从来没有显式提供值给某个特定键,它也可以有一个非零值。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注