@evolxb
2016-07-16T17:28:39.000000Z
字数 22124
阅读 1601
ios
view
layer
每个UIView有一个伙伴称为layer
,一个CALayer
。UIView
实际上并没有把自己画到屏幕上;它绘制本身到它的layer
上,它的layer
被绘制到屏幕上。正如我已经提到的,视图不会被经常重绘;相反,它的绘制会被缓存,在可用的地方都会使用缓存版本(bitmap backing store
)。缓存的版本,实际上,就是layer
。视图的图形上下文话实际上是layer
的图形上下文。
这似乎仅仅是一个实现细节,但layer
是非常重要和有趣。理解layer
能更深刻地理解视图;layer
延伸了视图的能力。 尤其是:
UIView
绘制相关的属性。由于layer
是视图的绘制的接收者和展现者,你可以通过访问该图层的属性来修改视图在屏幕上的绘制。换句话说,通过视图的layer
,你可用实现UIView
的方法实现不了的东西。UIView
的layer
层可含有附加的layer
。因为层的目的是为了绘制,这允许UIView
把要显示的分散到不同的layer
中。这可以使绘图更容易。layer
就是天生能做动画的;在“CALayer"
的”CA“
就代表“Core Animation.”
。例如,假设我们要添加一个指南针到我们的应用程序的界面。下图展示了一个指南针的一个简单的版本。它利用了我们前面例子中绘制的箭头;箭头被绘制到自身的layer中。罗盘的其它部分也是layer:圆是一个layer,并且每个基点字母是一个layer。这样绘制代码易于复合;更耐人寻味,不同的部分可用独立放置,并分别动画,所以很容易不移动圆圈就旋转箭头。
一个UIView
实例有一个CALayer
的实例,通过视图layer
属性访问。这一layer
具有特殊的地位:它与视图合作来显示所有视图的绘制。该layer
没有相应的视图属性,但该视图是layer的委托。文档有时也会说layer
是视图的underlying layer
。
默认情况下,当一个UIView
被实例化,其layer
是CALayer
的实例。如果你的UIView
子类,想改变underlying layer
的类型,实现UIView
子类的layerClass
类方法并返回CALayer
的子类。
下面是创建指南针的代码。我们有一个UIView
子类,CompassView
和CALayer
的子类,CompassLayer
。这里是CompassView
的实现:
class CompassView: UIView {
override class func layerClass() -> AnyClass {
return CompassLayer.self
}
}
效果如下图:
因此,当CompassView
实例化时,其下层是一个CompassLayer
。在这个例子中,CompassView
中没有任何绘制。它的工作-在此情况下,其一的工作 - 是让CompassLayer
在界面上显示,因为一个层不能脱离视图单独出现在界面上。
因为每个视图有一个underlying layer
,这两者之间紧密结合。layer
描绘所有视图的绘制;如果视图绘制,它通过layer
来绘制。视图是层的委托。视图的属性往往只是用于访问层的属性。例如,当你设置视图的的backgroundColor
,你只是在设置layer
的backgroundColor
,如果你直接设置layer
的backgroundColor
,视图的backgroundColor
会自动匹配。同样,视图的frame
其实是该layer
的frame
,反之亦然。
一个
CALayer
的delegate
属性是可设置的,可以是任何基于NSObject
类的一个实例(CALayerDelegate
是一个非正式的协议,通过分类注入NSObject
类中)。但一个UIView
和它的layer
有一种特殊的关系。一个UIView
必须是layer
的delegate
;而且,它不能是任何其它layer
的delegate
。不要做任何事情,如果你搞砸了,绘图将不会正常工作。
视图绘制到它的layer
,然后layer
缓存这些绘制;然后layer
可以被操纵,从而改变视图的外观,而不必要求视图重绘自身。这是绘图系统高效率的原因。这也解释了前面例子中视图拉神的原因:当视图大小变化时,默认情况下,绘图系统只是简单的伸展或重新定位缓存的layer
图像,直到视图被告知刷新(drawRect:
),从而替换layer
的内容。
layer
可以有子layer
但是最多只能有一个superlayer
。因此存在一个layer
的树形结构。这和视图的树形结构是类似的。事实上,视图和它的layer
如此紧密,这些层次结构是相同的层次结构。给定一个视图及其layer
,该layer
的superlayer
是该view
的superview
的layer
,该layer
的子layer
是view
的subview
的layer
。事实上,由于layer
展示了view
如何被绘制,有人可能会说,视图层次只是一个layer
层次结构。如下图:
同时,layer
的层次结构可以超越视图的层次结构。一个视图只有一个layer
,但layer
可以有不属于任何视图的underlying layer
子层。因此视图的underlying layer
层次结构和视图的层次完全匹配,但总的layer
树型结构可以是这个结构的一个超集。在下图中有和上图一样的层级结构,但有两个layer
具有它们自己单独的layer
子层(即子layer
不属于任何视图的underlying layer
)。
如下图:
从视觉角度看,layer
的层次结构和视图的层次结构没有区别。例如,在前面例子中,我们三个重叠视图来绘制重叠的矩形。下面的代码通过操作layer
来实现相同的视觉效果:
let layer1 = CALayer()
layer1.backgroundColor = UIColor(red: 1, green: 0.4, blue: 1, alpha: 1).CGColor
layer1.frame = CGRectMake(113, 111, 132, 194)
mainview.layer.addSublayer(layer1)
let layer2 = CALayer()
layer2.backgroundColor = UIColor(red: 0.5, green: 1, blue: 0, alpha: 1).CGColor
layer2.frame = CGRectMake(41, 56, 132, 194)
layer1.addSublayer(layer2)
let layer3 = CALayer()
layer3.backgroundColor = UIColor(red: 1, green: 0, blue: 1, alpha: 1).CGColor
layer3.frame = CGRectMake(43, 197, 160, 230)
mainview.addSublayer(layer3)
效果如下图:
视图的子视图的layer
是这一视图的underlaying layer
的sublayer
,就像该视图的underlaying layer
的任何其他子layer
。因此,在绘制顺序中可以把它们放在任何地方。视图可以被分散到superview
的layer
的sublayer
中,这通常令初学者非常惊讶。例如,让我们重新构造上图,但是在layer2
和layer3
中间,我们将添加一个子视图:
// ...
layer1.addSublayer(layer2)
let iv = UIImageView(image: UIImage(named: "smiley"))
mainview.addSubview(iv)
iv.frame.origin = CGPointMake(180, 180)
let layer3 = CALayer()
效果如下图:
笑脸在红色矩形前面被添加到界面上所以看来在矩形的后面。通过颠倒其中红色矩形和笑脸加入到该界面的顺序,笑脸可以出现在该矩形的前面。笑脸是一个视图,而矩形只是一个layer
;所以他们没有像视图之间的兄弟关系,因为矩形不是一个视图。但笑脸是视图及其layer
;作为layer
,笑脸和矩形是兄弟关系,因为它们具有相同的superlayer
,所以一个可以出现在另一个的前面。
layer
是否超出自己的边界之外的子layer
的区域取决于它的masksToBounds
属性的值。这和视图的clipsToBounds
属性相似,而事实上,因为layer
是视图的underlying layer
,所以它们是同一个东西。在上面2张图中,layers
的clipsToBounds
都设置为false
(默认值);这就是为什么右侧layer
超出的中间layer
的原因。
和UIView
类似,一个CALayer
的具有一个hidden
属性可以把它和它的子layer
在界面中隐藏而不用从它的superlayer
中移除。
layer
使用了和视图相似的一整套方法来读取和操纵layer
的层次结构。layer
有一个superlayer
属性和sublayers
属性,以及下面的方法:
addSublayer:
insertSublayer:atIndex:
insertSublayer:below:, insertSublayer:above:
replaceSublayer:with:
removeFromSuperlayer
不同于视图的subviews
属性,layer
的sublayers
属性是可写的;因此,你可以通过sublayers
属性一次性给layer
设置多个sublayer
。通过设置sublayers
为nil
来移除layer
的所以子layer
。
虽然一个layer
的子layer
有顺序,可以通过上面提到的方法和sublayers
属性来操纵顺序,但这并不和绘制的顺序完全相同。默认情况下,layer
有一个CGFloat
类型的zPosition
属性值,这也决定了绘制顺序。绘制规则是相同的zPosition
的所有子layer
在sublayers
属性所列的顺序绘制,但较低的zPosition
属性比较高的zPosition
属性的layer
先绘制。 (默认的zPosition
是0.0
)。
有时,zPosition
属性比兄弟顺序更方便的决定绘制顺序。例如,如果layer
代表一个纸牌游戏的扑克牌,可能会更容易和方便通过设置zPosition
而不是子layer
自己的兄弟关系。此外,子视图的layer
本身只是一个layer
,这样你就可以通过设置它们的zPosition
来重新排列子视图的绘制顺序。在上图中,如果我们指定图像视图的layer
的zPosition
为1
,它会被绘制在红色矩形的前面:
mainview.addSubview(iv)
iv.layer.zPosition = 1
还有一些方法提供了用于在同一layer
层次结构内各layer
的坐标系统之间的转换方法:
convertPoint:fromLayer:, convertPoint:toLayer:
convertRect:fromLayer:, convertRect:toLayer:
layer
坐标系统和定位和视图的那些类似。layer
有自己的内部坐标系统是由它bounds
属性表示,就像视图一样。它的大小是它的bounds
大小,内部坐标系统的原点在它的左上角。
然而,sublayer
在它的superlayer
中的位置不是由它的center
属性决定的;因为layer
没有center
。相反,sublayer
在superlayer
中的位置由2个属性联合决定:
position
superlayer
坐标系统中的点anchorPoint
position
的位置,相对于该layer
自身的边界比率。它是描述layer
自身的边界的宽度和高度的比率的一个CGPoint
。因此,例如,(0.0,0.0)
是layer的边界的左上角,(1.0,1.0)
是layer
的边界的右下角。这里有一个比喻;并不是我创造的,但它是相当贴切。想象把sublayer
用图钉固定到superlayer
;那么你不得不说这个针在什么地方穿过sublayer
(anchorPoint
),并固定在superlayer
的哪个位置(position
)。
如果anchorPoint
为(0.5,0.5
)(默认值),position
属性和view
的center
属性一样。因此视图的center
是layer
的position
的一种特殊情况。这是比较典型的视图属性和图层特性之间的关系;视图属性往往是一个简单的 - 但不那么强大 - 的layer
属性的版本。
图层的position
和anchorPoint
是正交(独立的);改变一个不会改变另一个。因此,改变它们中的任一个,都可以改变layer
在superlayer
中的绘制的位置。
例如,在第一张图中,圆圈的最重要的一点是其中心;所有其他对象需要相对于它来定位。因此,它们都具有相同的position
:该圆的中心。但它们的anchorPoint
不同。例如,箭头的anchorPoint
是(0.5,0.8)
轴的中间靠近尾部。在另一方面,数字基点anchorPoint
为(0.5,3.0
),已经超过字母的边界,在圆形表盘的边缘附件。
layer
的frame
属性是一个纯粹的衍生属性。当你获取frame
时,它是从边界尺寸与position
和anchorPoint
计算出来的。当你设置frame
时,你设置边界的大小和position
。一般情况下,你应该把frame
作为一个便利的属性,这非常方便!例如,定位一个子层,使其恰好重叠superlayer
,你可以设置子层的frame
为superlayer
的bounds
。
在代码中创建(而不是一个视图的underlying layer)的layer的frame和bounds都是(0.0,0.0,0.0,0.0),当你把它添加到屏幕上的superlayer中它都是不可见的。如果你希望能够看到它给你的layer非零宽度和高度。创建layer并将它添加到一个superlayer然后发现它为什么没有在界面中出现是一种常见的初学者错误。
如果你将要移动一个layer
的边界原点作为重新定位其子层位置的方式,你可能想使用CAScrollLayer
,一个CALayer
的子类,提供了这样的方便的方法。(尽管是这样的名字,一个CAScrollLayer
不提供滚动界面,用户无法通过拖拽来滚动它。)默认情况下,CAScrollLayer
的masksToBounds
属性为true
;因此,CAScrollLayer
就像window
一样你只能看到它边界以内的东西。(你可以设置它的masksToBounds
为false
,但是这是一件奇怪的事,因为它有点和目的相背。)
要移动CAScrollLayer
的边界,你可以直接告诉它或者它的任何sublayer
:
Talking to the CAScrollLayer
scrollToPoint:
CAScrollLayer
的边界原点到那个点。scrollToRect:
CAScrollLayer
边界原点,使得边界矩形的给定部分是可见的。Talking to a sublayer
scrollPoint:
CAScrollLayer
边界原点,使得layer
的给定的点是在CAScrollLayer
的左上角。scrollRectToVisible:
CAScrollLayer
的边界原点,这样子层边界的给定的区域在CAScrollLayer
的边界区域内。你也可以访问子层的visibleRect
,sublayer
在CAScrollLayer
的可见区域的部分。视图层次结构实际上是一个layer
层次结构。视图在父视图的位置实际上是其layer
在superlaye
内的定位。一个视图可以被重新定位,通过其autoresizingMask
或通过根据它的约束自动调整大小。因此,如果layer
是视图的underlying layer
它会自动调整大小。否则,iOS
不会对layer
自动调整大小。因此不是视图的underlying layer
的sublayer
只能用代码手动调整大小。
layer
的边界更改或者调用setNeedsLayout
,此时layer
需要调整布局,你可以通过以下两种方式响应:
layer
的layoutSublayers
方法被调用;通过重写CALayer
的子类中layoutSublayers
方法来响应布局变化。layer
的delegate
中实现layoutSublayersOfLayer:
方法。 (请记住,如果layer
是一个视图的underlying layer
,那么视图是layer
的delegate
。)为了有效布局sublayer
,你可能需要一种方法来识别或引用子layer
。layer中没有viewWithTag:
这样的方法,因此怎样识别和引用layer
完全取决于你。键-值编码可能是有用的;layer
以一种特殊的方式实现了键值编码。
对于视图的underlying layer
来说,在视图的layoutSubviews
被调用后,layer
的layoutSublayers
或layoutSublayersOfLayer:
也会被调用。在自动布局中,你必须调用super
否则自动布局会崩溃。此外,这些方法在自动布局过程中可能被调用一次以上;如果你正在寻找手动布局layer
的时机,视图的布局事件可能是更好的选择。
在layer
中显示一些东西的最简单的方法是通过它的contents
属性。这和UIImageView
的image
属性很相似。它期望一个CGImage
(或nil
,表示没有图像)。因此,举例来说,下面是通过layer
而不是视图来生成笑脸的代码:
let layer4 = CALayer()
let im = UIImage(named: "smiley")!
layer4.frame = CGRect(origin: CGPointMake(180, 180), size: im.size)
layer4.contents = im.CGImage
mainview.layer.addSublayer(layer4)
设置layer的contents为一个UIImage,而不是一个CGImage,会默默地失败 -- 影像不会出现,但没有任何错误。这绝对会发疯,每一个操作我都已经做到了,然后浪费时间搞清楚为什么我的layer没有出现。
这有4种方法来为layer
提供需要的内容,类似于UIView
的drawRect:
方法.layer
会非常保守的调用这些方法(你不能直接调用其中的任何方法)。当layer
调用了这些方法,这就是说layer
重新显示自己。下面是引起layer
重新显示自己的方式:
layer
的needsDisplayOnBoundsChange
属性为false
(默认值),那么只有通过调用setNeedsDisplay
(或setNeedsDisplayInRect:
)才能让它重新显示自己。即使这样可能也不会导致layer
马上重新显示自己;如果重新显示自己非常重要,那么你可以调用displayIfNeeded
。layer
的needsDisplayOnBoundsChange
属性为true
,那么当layer
的边界变化是layer
也会重新显示自己(类似视图的.Redraw
模式)。下面的四种方法可以被调用用来使layer
重新显示自己;选择一个实现(不要重复实现它们,否则你会奔溃):
display
in a subclass CALayer
的子类可以重写display
方法。这个时候没有任何图形上下文,因此display
方法只能限制于设置contents
的图片。displayLayer:
in the delegate CALayer
的delegate
然后在delegate
种实现DisplayLayer:
方法。和display
方法一样,没有图形上下文,所以你只能给contents
设置图像。drawInContext:
in a subclass CALayer
子类可以重写drawInContext:
.此参数是一个图形上下文,因此你可以在其中直接绘制;它不会自动成为当前上下文。drawLayer:inContext:
in the delegate CALayer
的delegate
然后实现drawLayer:InContext:
.第二个参数是一个图形上下文,你可以在其中直接绘制;它不会自动成为当前上下文。给layer
的contents
分配一个图像和直接在layer
里绘制在效果上是相互排斥的。 所以:
layer
的contents
被分配一个图像,这个图像会立即显示出来,并替换掉已被显示在layer
上的绘制。layer
重新显示本身,drawInContext:
或者drawLayer:inContext:
会在layer
里绘制,那么绘制会替换掉layer
种显示的任何图片。layer
重新显示自己然而这四种方法没有那一个能提供任何内容,那么layer
会是空的。如果layer
是视图的underlying layer
,你通常不会使用四种方法种的任何一种来绘制到layer
:你会使用视图的drawRect:
.但是,你也可以使用这些方法,如果你真的想。在这种情况下,你可能会想实现一个空的drawRect:
方法。其原因是,这会导致layer
在适当的时刻重新显示本身。当一个视图被发送setNeedsDisplay
消息 - 包括当视图首次出现时 - 视图的underlying layer
也会被发送setNeedsDisplay
消息,除非视图没有实现drawRect:
(因为在这种情况下,假定该视图永远不需要重绘)。所以,如果直接使用视图的underlying layer
来绘制整个视图,而且当视图需要重绘自己时视图的underlying layer
在某个时刻要自动重新显示自己,那么你应该实现一个空的drawRect:
方法。 (该技术对underlying layer
的子layer
没有任何影响。)
下面这些都是能够绘制到视图(但不常用的)方法:
drawRect:
方法,然后实现displayLayer:
或者drawLayer:inContext:
.drawRect
方法和layerClass
方法返回自定义的layer
子类 - 在自定义的layer
子类种实现display
或者drawInContext:
方法.切记,你不能设置视图的underlying layer
的delegate
属性!视图是它的layer
的delegate
,并且必须保持其delegate
。通过delegate
绘制到layer
的一个有用的架构是将一个视图当作layer-hosting
:视图及其underlying layer
只用保持一个sublayer
,所以的绘制都方法在sublayer
中。如下图:
layer
有一个contentsScale
属性,这会把layer
中的图形上下文中的点距映射到设备上的像素距离,由Cocoa
管理的layer
,如果它由内容其contentsScale
属性会被自动调整;例如,一个实现drawRect:
的视图,在双分辨率的设备上其underlying layer
的contentsScale
属性会被设置为2
.你自己创建并管理的layer
是没有这种福利的,都得你自己手动设置;如果你想在layer
中绘制,那么正确的设置layer
的contentsScale
。contentsScale
为1
的的layer
的绘制内容在高分辨率的设置上看起来很模糊。如果layer
的contents
属性为一个UIImage
的CGImage
,而去UIImage
的scale
属性和layer
的scale
属性不匹配,那么图片会以一个错误的大小显示。
三个layer
的属性强烈地影响层layer
的显示,而去非常不好理解:
backgroundColor
backgroundColor
相似(如果该layer
是视图的underlying layer
,那么它就是视图的backgroundColor
)。改变backgroundColor
会立即生效。可以这样认为backgroundColor
和layer
自己的绘制是分开的, 而且在layer
自己的绘制的下面。opacity
layer
整体的透明度。它相当于一个视图的alpha
属性(并且如果该layer
是一个视图的underlying layer
,它就是视图的alpha
)。它也会影响layer
的子layer
的透明度。它分别影响layer
的背景颜色和内容的透明度(和视图的alpha属性类似)。改变opacity
属性立即生效。opaque
layer
的图形上下文是否是不透明的。不透明的图形上下文是黑色的;你可以在黑色的背景上绘制,但是黑色背景会被保留。非不透明的图形上下文是clear
的;没有绘制时,它是完全透明的。改变opaque
属性不会马上起作用,直到重新显示layer
自己。视图的underlying layer
的opaque
属性完全独立视图的opaque
属性;他们是不相关的,做完全不同的事情。layer
的内容会被做为图片缓存为位图, 然后根据layer
的各种属性绘制到layer
的bounds
内:
layer
的内容是通过给contents
属性设置一张图片,那么缓存的内容就是这张图片,大小就是CGImage
的大小。layer
的内容是直接绘制到layer
的图形上下文(drawInContext:
,drawLayer:inContext:
)中的,缓存的内容是layer
的整个图形上下文;它的大小是在执行绘制时layer
的大小。layer
的属性问题会导致layer
重新显示自己时,缓存的内容会被调整大小,重新定位,裁剪等等,这些属性是:
contentsGravity
UIView
的contentMode
属性,它描述了layer
的content
相对于bounds
如何被定位或者拉伸。例如,kCAGravityCenter
意味着内容在边界居中而且不改变大小;kCAGravityResize
(默认值)意味着内容调整大小以适合bounds
,即使拉伸;等等。 由于历史原因,
contentsGravity
值中Bottom
和Top
于它们的字面意思相反。
contentsRect
CGRect
表示内容被显示的比例。默认值是(0.0,0.0,1.0,1.0)
,意思是显示的全部内容。指定的内容部分根据contentsGravity
的设置会相对于bounds
重新调整大小和定位。因此通过设置contentsRect
,可以扩大部分内容来填充整个bounds
,或者不重新绘制或改变contents
的图片来把大图片整个缩小到视图中。 contentsRect
如(-0.5,-0.5,1.5,1.5)
来缩减内容;但是内容靠近contentRect
边缘的像素将被向外延伸到layer
的边缘(以防止这一点,确保内容的最外像素都是空的)。contentsCenter
CGRect
,结构类似contentsRect
,表示如果contentsGravity
设置为拉伸,被contentsRect
区分的九个拉伸区域的中间区域。中央区域(contentsCenter
的实际值)在两个方向上拉伸。其他八个区域中,四个角的区域不拉伸,四条边的区域向一个方向拉伸。 (这和resizible image
的拉伸方式类似)layer
layer
contentsGravity
layer
contentsGravity
contentsRect
(0.0,0.0,1.0,1.0)
contentsGravity
layer
arrow.needsDisplayOnBoundsChange = false
arrow.contentsCenter = CGRectMake(0.0, 0.4, 1.0, 0.6)
arrow.contentsGravity = kCAGravityResizeAspect
arrow.bounds.insetInPlace(dx: -20, dy: -20)
CATextLayer
CATextLayer
有一个字符串属性,它可以是一个NSString
或NSAttributedStrin
g,与其他文本格式属性一起,有点像一个简化的UILabel
;它绘制它的字符串。默认的文本颜色,就是ForegroundColor
属性,是白色的,这不太可能是你想要的效果。text
和contents
是不同的而且是互斥的:内容图片和文字只有一个会被绘制,所以一般你不应该给任何CATextLayer
设置内容图像。上面图片中,基点字母就是CATextLayer
实例。CAShapeLayer
CAShapeLayer
有一个path
属性,这是一个CGPath
。它填充或描边此路径,或两者,这取决于其fillColor
和则strokeColor
值,并显示描边或填充的结果;fillColor
默认是黑色,默认没有storkeColor
。它有线宽,虚线样式,端帽样式属性,和连接样式,类似于图形上下文;它也有绘制其路径(strokeStart
和strokeEnd
)的一部分的非凡能力,例如,绘制一个椭圆的一段弧线。一个CAShapeLayer
也可能有contents
;形状被显示在内容图像的顶部,但没有属性指定一个合成模式。在上面的图片中,背景圆是一个CAShapeLayer
实例,灰色描边,明亮和稍微透明的灰色填充。CAGradientLayer
CAGradientLayer
用简单的线性渐变覆盖它的背景;因此,在界面上用它绘制简单的渐变很容易。渐变和Core graphics
中的差不多,有一个颜色的数组,与一个起始和结束点沿。可以将mask
添加到CAGradientLayer
上裁剪形状。CAGradientLayer
不会显示contents
的内容。 通过变换(transform
)可以修改layer
在屏幕上的绘制。因为视图可以有一个transfrom
,而且视图是通过其layer
绘制到屏幕上的。但是的layer
的变换比视图的变换功能更强大;你可以使用它来完成你不能用一个视图变换独自完成的事。
在最简单的情况下,当变换是二维的时候,你可以访问layer
的AffineTransform
方法来访问layer
的变换。变换施加于anchorPoint
的。
你现在已经知道了生成指南针的所以代码含义。在这段代码中,self
是CompassLayer
;它没有绘制自己,而仅仅只是配置它的子layer
。这四个基本点的字母分别由CATextLayer
绘制;它们在相同的坐标系统中绘制,但它们具有不同的旋转变换,而且被固定使得它们的旋转是以圆的中心为中心。为了生成箭头,我们使自己成为箭头layer
的delegate
,并调用setNeedsDisplay;
这导致drawLayer:inContext
在CompassLayer
被调用。箭头layer
由anchorPoint
钉扎其尾部在圆的中心,并且通过变换围绕固定点旋转:
// gradient
let grad = CAGradientLayer()
grad.contentScale = UIScreen.mainScreen().scale
grad.frame = self.bounds
grad.colors = [
UIColor.blackColor().CGColor,
UIColor.redColor().CGColor
]
grad.locations = [0.0, 1.0]
self.addSublayer(grad)
// circle
let circle = CAShapeLayer()
circle.contentsScale = UIScreen.mainScreen().scale
circle.linewidth = 2.0
circle.fillColor = UIColor(red: 0.9, green: 0.95, blue: 0.93, alpha: 0.9).CGColor
circle.strokeColor = UIColor.grayColor().CGColor
let p = CGPathCreateMutable()
CGPathAddEllipseInRect(circle)
circle.path = p
self.addSublayer(circle)
circle.bounds = self.bounds
circle.position = self.bounds.center
// four cardinal points
let pts = "NESW"
for (ix, c) in pts.characters.enumerate() {
let t = CATextLayer()
t.contentsScale = UIScreen.mainScreen().scale
t.string = String(c)
t.bounds = CGRectMake(0, 0, 40, 40)
t.position = circle.bounds.center
let vert = circle.bounds.midY / t.bounds.height
t.anchorPoint = CGPointMake(0.5, vert)
t.alignmentMode = kCAAlignmentCenter
t.foregroundColor = UIColor.blackColor().CGColor
t.setAffineTransform = CGAffineTransform(
CGAffineTransformMakeRotation(CGFloat(ix) * CGFloat(M_PI) / 2.0))
circle.addSublayer(t)
}
// arrow
let arrow = CALayer()
arrow.contentsScale = UIScreen.mainScreen().scale
arrow.bounds = CGRectMake(0, 0, 40, 100)
arrow.position = self.bounds.center
arrow.anchorPoint = CGPointMake(0.5, 0.8)
arrow.delegate = self //will draw arrow in delegate method
arrow.setAffineTransform(CGAffineTransformMakeRotation(CGFloat(M_PI) / 5.0))
self.addSublayer(arrow)
arrow.setNeedDisplay()
一个完备的layer
变换会发生在三维空间;其包括一个z
轴,垂直于x
轴和y
轴。 (默认情况下,z轴正方向指向屏幕外面,指向用户的脸)。layer
不会奇迹般地给你逼真的三维渲染--你可以使用OpenGL
来实现三维渲染,这不在本文的讨论范围之内。layer
是二维对象,它们被设计的足够简单和快速。尽管如此,它们也可以在三维上操作,而且特别的快速真实,特别是执行动画时尤其如此。我们都看到过屏幕上的图像翻转像翻一张纸一样的效果而且可以显示背面的东西;这是在三维空间的旋转。
三维变换需要围绕anchorPoint
,其z
分量由anchorPointZ
属性提供。因此,在anchorPointZ
为0.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轴:
someLayer.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0, 1, 0)
默认情况下,该层被认为是双面的,所以当它被翻转,以显示背面的时候,显示的是它的layer
的内容的逆向版本(连同子layer
和它们的所以坐标系统)。但是,如果该layer
的doubleSided
属性为false
,那么当它被翻转显示其背面的时候;它的“背面”是透明的而且也是空的。
有两种方式来放置layer
在不同的深度。一种是通过它们的位置,就是zPosition
属性。另一种是在z
轴上施加一个平移变换来改变layer
的位置。layer
的position
的z
分量(zPosition
)和在z
轴的偏移量这两个量是相关的;在某种意义上说,zPosition
是在z
方向的平移变换的简写形式。 (如果你同时提供zPosition
和z
方向平移变换,那么你会非常迷惑。)
在现实世界中,改变一个对象的zPosition
会使其显示更大或更小,因为它和眼睛的距离更近或更远;但是layer
的绘制和真实世界不一样。这里没有视角的概念;layer
在平面上按照它们真实的大小绘制而且叠在一起没有间隙。(这就是所谓的正投影,并且蓝图经常以这样的方式从侧面显示一个物体)。
然而,有一个广泛使用的技巧对于layer
的绘制:使它的子layer
的sublayerTransform
属性映射所以的点到一个“远端”的平面。(这可能是关于sublayerTransform
属性唯一的作用。)与正投影相结合,效果是将点透视应用到绘制中,使得z轴负方向视角更小。
例如,让我们尝试采用一种“翻页”旋转到我们的指南针上:我们会在它的右侧固定,然后绕Y轴旋转。这里,我们旋转的子层(通过属性,rotationLayer
访问)是渐变层,并且圆和箭头是其子层,使得它们一起旋转:
self.rotationLayer.anchorPoint = CGPointMake(1, 0.5)
self.rotationLayer.position = CGPointMake(self.bounds.maxX, self.bounds.midY)
self.rotationLayer.transform = CATransform3DMakeRotation(CGFloat(M_PI) / 4.0, 0, 1, 0)
结果如上图;指南针看起来被压扁。然而,现在,我们将适用距离映射转换。这里的superlayer
是就是self
:
var transform = CATransform3DIdentity
transform.m34 = -1.0 / 1000.0
self.sublayerTransform = transform
结果如上图显示还可以,你可以用其他值来代替1000.0
试试各种效果;例如,500.0
给出了一个更夸张的效果。此外,rotationLayer
的zPosition
也会影响它得大小。
绘制layer
随深度改变大小的另一种方法是使用CATransformLayer
。这CALayer
的子类没有做任何关于自己的绘制;它的目的仅仅是作为其它layer
的宿主。它最显着的特征是你对它应用一个变换,它就会保持自己的子层之间的深度关系。 例如:
// layer1 is a layer, f is a CGRect
let layer2 = CALayer()
layer2.frame = f
layer2.backgroundColor = UIColor.blueColor().CGColor
layer1.addSublayer(layer2)
let layer3 = CALayer()
layer3.frame = f.offsetBy(dx: 20, dy: 30)
layer3.backgroundColor = UIColor.greenColor().CGColor
layer3.zPosition = 10
layer1.addSublayer(layer3)
layer1.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0, 1, 0)
在代码中,superlayer
layer1
有两个子层,layer2
和layer3。子层以上述顺序加入,所以
LAY3在
lay2的前面绘制。然后通过设置
layer1的
transform给
layer1执行了一个翻书页的翻转变换。如果
lay1是正常的
CALayer的子层则绘制顺序不会改变;
LAY3仍然绘制在
layer2的前面即使添加翻转变换后。但是,如果
lay1是
CATransformLayer,在翻转变换后
layer3会绘制在
layer2的后面;因为它们都是
lay1`的子层,因此它们的深度关系保持不变。
下图仍然通过给self
设置sublayerTransform
执行翻转变换,不过这一次self
唯一的子layer
是CATransformLayer
:
var transform = CATransform3DIdentity
transform.m34 = -1.0 / 1000.0
self.sublayerTransform = transform
let master = CATransformLayer()
master.frame = self.bounds
self.addSublayer(master)
self.rotationLayer = master
效果如下图:
执行翻转变换的CATransformLayer
,持有渐变layer
,circle layer
,箭头layer
。这三个层在不同深度(使用不同的zPosition
设置),给箭头添加阴影从圆形表盘中分离:
circle.zPosition = 10
arrow.shadowOpacity = 1.0
arrow.shadowRadius = 10
arrow.zPosition = 20
你可以明显的看到圆圈层漂浮在渐变层上面,在旋转变换执行的过程中使用动画可能效果更好。
为了更显著,我添加了一个白色的小挂钩,通过固定箭头然后扎入圆圈里面!这是一个CAShapeLayer
,旋转到垂直于CATransformLayer
:
let peg = CAShapeLayer()
peg.contentsScale = UIScreen.mainScreen().scale
peg.bounds = CGRectMake(0, 0, 3.5, 50)
let p2 = CGPathCreateMutable()
CGPathAddRect(p2, nil, peg.bounds)
peg.path = p2
peg.fillColor = UIColor(red: 1.0, green: 0.95, blue: 1.0, alpha: 0.95).CGColor
peg.anchorPoint = CGPointMake(0.5, 0.5)
peg.position = master.bounds.center
master.addSublayer(peg)
peg.setValue(M_PI / 2, forKeyPath: "transform.rotation.x")
peg.setValue(M_PI / 2, forKeyPath: "transform.ratation.z")
peg.zPosition = 15
上面的代码实际上给我们的layer
做了一个3d模型。
一个CALayer
具有很多影响绘制细节的属性。这也是UIView
高效绘制的原因,因为它们能作用于view
的underlying layer
。
一个CALayer
可以有阴影,由shadowColor
,shadowOpacity
,shadowRadius
和shadowOffset
属性定义。为使该层绘制阴影,shadowOpacity
应该设置为非零值。阴影通常是根据该层的不透明区域的形状绘制,但得到该形状是cpu
密集型的。您可以通过自己定义形状和把形状做为CGPath
赋值给shadowPath
属性,这会大大提高性能。
如果图层的masksToBounds是true,边界之外的阴影不会被绘制。。
CALayer
可以有一个边框(borderWidth
,borderColor
);borderWidth
在边框的里面绘制,这可能会遮盖一部分内容,除非你有其他的处理。
CALayer
可通过cornerRadius
来设置圆角矩形。如果该层有边框,也有圆角。如果该层具有backgroundColor
,那么背景颜色会被剪裁到圆角矩形的形状。如果该层的masksToBounds
是true
,图层的内容和它的子层会被圆角裁剪。
CALayer
可以有一个遮罩(mask
)。如果它本身就是一个层,其内容必须被以某种方式提供。mask
的内容在特定部分的透明度会成为layer
在相应部分的透明度。mask
的颜色没有任何用处,只有透明度有用,要放置一个mask
,把它做为一个子layer
。
下图显示了我们的箭头,一个灰色圆形层在它下面,并且施加了一个mask
:它是一个椭圆,用不透明颜色填充并且厚的半透明的颜色描边。代码如下:
let mask = CAShapeLayer()
mask.frame = arrow.bounds
let path = CGPathCreateMutable()
CGPathAddEllipseInRect(path, nil, CGRectMake(mask.bounds, 10, 10))
mask.strokeColor = UIColor(white: 0.0, alpha: 0.5).CGColor
mask.lineWidth = 20
mask.path = path
arrow.mask = mask
效果如下:
结合cornerRadius
,masksToBounds
和mask
,可以以更通用的方式来执行。例如,下面是产生远角mask
的方法:
func maskOfSize(size: CGSize, roundingCorners rad: CGFloat) -> CALayer {
let r = CGRect(origin: CGPointZero, size: size)
UIGraphicsBeginImageContextWithOptions(r.size, false, 0)
let context = UIGraphicsGetCurrentContext()
CGContextSetFillColorWithColor(context, UIColor(white: 0, alpha: 0).CGColor)
CGContextFillRect(context, r)
CGContextSetFillColorWithColor(context, UIColor(white: 0, alpha: 1).CGColor)
let p = UIBezierPath(roundedRect: r, cornerRadius: rad)
p.fill()
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let mask = CALayer()
mask.frame = r
mask.contents = im.CGImage
return mask
}
从上面方法返回的layer
可以做为任何layer
的mask
。其结果是,layer
的所以内容包括子layer
都被剪切到圆角矩形的形状;该形状之外的一切都没有绘制。这只是使用mask
实现的一个例子。mask
可以具有不透明和透明的之间的值,并且它可以是任何形状。透明区域不一定非得在mask
区域外面;可以使用一个外部不透明内部透明的mask
来给layer
打孔。
你可以给视图设置mask
通过maskView
属性。这可能很便利但是没有layer
层来的高效;这本质上还是给底层的layer
设置mask
。因此,这并不能解决mask
的大小调整问题。
现在,你可能对layer
实现各种mask
。这没有什么不妥,但是当iOS
设备把绘图从一个地方转移到另一个地方,设置可能不能迅速的响应这些请求。这类问题很可能出现尤其是当你执行的动画或当用户能够通过触摸动态绘制,滚动表视图时。您可以通过肉眼来发现这些问题,你可以通过使用Instruments
的Core Animation template
来显示动画期间所取得的帧速率来发现这些问题。模拟器的DEBUG菜单也能让你发现一些颜色叠加导致的绘图效率问题。
在一般情况下,不透明绘制是最有效的。(非不透明绘制在Instruments
上用红色标记为“blended layers
”。)如果一个图层将始终显示在单一颜色的背景上,你可以给它设置相同颜色的背景;
另一种方法来获取效率提升是通过“冻结”绘图的全部做为位图。实际上,你先绘制到一个二级缓存,然后把缓存绘制到屏幕。从缓存绘制比直接绘制到屏幕效率低,但是如果layer层次很深很复杂就不用每次都去渲染整棵树了。要做到这一点,设置图层的shouldRasterize
为true
,设置rasterizationScale
一些有意义的值(可能UIScreen.mainScreen().scale
)。您可以随时设置shouldRasterize
为false
,来关闭栅格化,所以很容易在一些很混乱的屏幕重排之前开启然后在关闭它。
此外,还有一个图层属性drawsAsynchronously
。默认为false
。如果设置为true
,该层的图形上下文积累绘图命令,然后在某个恰当的时刻在后台线程绘制。因此,您的绘图命令运行速度非常快,因为他们实际上不是在你发送绘制命令时绘制。我还没有机会使用这个,但是可能在你需要时间比较长的绘制的时候有效果。
所有图层属性都可以通过具有相同名称的属性键的键值编码来访问。因此,为layer
添加mask
,可以这样:
layer.mask = mask
也可以这样:
layer.setValue(mask, forKeyPath: "mask")
此外,CATransform3D
和CGAffineTransform
值可以通过键 - 值编码和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
:
rotation.x
,rotation.y
,rotation.z
rotation
(和rotation.z
一样)scale.x
,scale.y
,scale.z
translation.x
,translate.y
,translate.z
translation
甚至你可以把CALayer
作为一种字典,获取和设置任意键的值。这意味着你可以将任意信息附加到一个单独的层实例,并在以后检索。例如,手动布局layer需要先引用到此layer。那么可以这样做:
myLayer1.setValue("Foo", forKey: "name")
myLayer2.setVlaue("Foo2", forKey: "name")
图层没有一个name
属性;'name'
属性是我附加给layer
的。现在,我可以通过获取各自的“name”键的值后确定这些层。
另外,CALayer
有defaultValueForKey:
类方法;实现它,你需要继承和覆盖此方法。在需要提供特定键一个默认值的情况下,返回默认值,;否则,返回来自调用super
的返回值。因此,即使从来没有显式提供值给某个特定键,它也可以有一个非零值。