programming iOS - layer

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

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

  • Layers have properties that affect drawing.
    layer具有超出一个UIView绘制相关的属性。由于layer是视图的绘制的接收者和展现者,你可以通过访问该图层的属性来修改视图在屏幕上的绘制。换句话说,通过视图的layer,你可用实现UIView的方法实现不了的东西。
  • Layers can be combined within a single view.
    一个UIViewlayer层可含有附加的layer。因为层的目的是为了绘制,这允许UIView把要显示的分散到不同的layer中。这可以使绘图更容易。
  • Layers are the basis of animation.
    动画可以让你的界面更加简洁更加酷炫。而layer就是天生能做动画的;在“CALayer"”CA“就代表“Core Animation.”

例如,假设我们要添加一个指南针到我们的应用程序的界面。下图展示了一个指南针的一个简单的版本。它利用了我们前面例子中绘制的箭头;箭头被绘制到自身的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的实现:

class CompassView: UIView {
    override class func layerClass() -> AnyClass {
        return CompassLayer.self
    }
}

效果如下图:


因此,当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来实现相同的视觉效果:

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

// ...
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张图中,layersclipsToBounds都设置为false(默认值);这就是为什么右侧layer超出的中间layer的原因。

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

Manipulating the Layer Hierarchy

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

  • addSublayer:
  • insertSublayer:atIndex:
  • insertSublayer:below:, insertSublayer:above:
  • replaceSublayer:with:
  • removeFromSuperlayer

不同于视图的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,它会被绘制在红色矩形的前面:

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

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

  • convertPoint:fromLayer:, convertPoint:toLayer:
  • convertRect:fromLayer:, convertRect:toLayer:

Positioning a Sublayer

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

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

  • position
    一个super layer坐标系统中的点
  • anchorPoint
    其中position的位置,相对于该layer自身的边界比率。它是描述layer自身的边界的宽度和高度的比率的一个CGPoint。因此,例如,(0.0,0.0)layer的边界的左上角,(1.0,1.0)layer的边界的右下角。

这里有一个比喻;并不是我创造的,但它是相当贴切。想象把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,你可以设置子层的``frame为superlayerbounds

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

CAScrollLayer

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

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

  • Talking to the CAScrollLayer
    • scrollToPoint:
      改变CAScrollLayer的边界原点到那个点。
    • scrollToRect:
      最低限度地改变CAScrollLayer边界原点,使得边界矩形的给定部分是可见的。
  • Talking to a sublayer
    • scrollPoint:
      改变CAScrollLayer边界原点,使得layer的给定的点是在CAScrollLayer的左上角。
    • scrollRectToVisible:
      改变CAScrollLayer的边界原点,这样子层边界的给定的区域在CAScrollLayer的边界区域内。你也可以访问子层的visibleRectsublayerCAScrollLayer的可见区域的部分。

Layout of Sublayers

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

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

  • 该``layerlayoutSublayers方法被调用;通过重写CALayer的子类中layoutSublayers`方法来响应布局变化。
  • 或者在layerdelegate中实现layoutSublayersOfLayer:方法。 (请记住,如果layer是一个视图的underlying layer,那么视图是layerdelegate。)

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

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

Drawing in a Layer

layer中显示一些东西的最简单的方法是通过它的contents属性。这和UIImageViewimage属性很相似。它期望一个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提供需要的内容,类似于UIViewdrawRect:方法.layer会非常保守的调用这些方法(你不能直接调用其中的任何方法)。当layer调用了这些方法,这就是说layer重新显示自己。下面是引起layer重新显示自己的方式:

  • 如果layerneedsDisplayOnBoundsChange属性为false(默认值),那么只有通过调用setNeedsDisplay(或setNeedsDisplayInRect:)才能让它重新显示自己。即使这样可能也不会导致layer马上重新显示自己;如果重新显示自己非常重要,那么你可以调用displayIfNeeded
  • 如果layerneedsDisplayOnBoundsChange属性为true,那么当layer的边界变化是layer也会重新显示自己(类似视图的.Redraw模式)。

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

  • display in a subclass
    你的CALayer的子类可以重写display方法。这个时候没有任何图形上下文,因此display方法只能限制于设置contents的图片。
  • displayLayer: in the delegate
    您可以设置CALayerdelegate然后在delegate种实现DisplayLayer:方法。和display方法一样,没有图形上下文,所以你只能给contents设置图像。
  • drawInContext: in a subclass
    你的CALayer子类可以重写drawInContext:.此参数是一个图形上下文,因此你可以在其中直接绘制;它不会自动成为当前上下文。
  • drawLayer:inContext: in the delegate
    你可以设置CALayerdelegate然后实现drawLayer:InContext:.第二个参数是一个图形上下文,你可以在其中直接绘制;它不会自动成为当前上下文。

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

  • 如果layercontents被分配一个图像,这个图像会立即显示出来,并替换掉已被显示在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 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的显示,而去非常不好理解:

  • backgroundColor
    和视图的backgroundColor相似(如果该layer是视图的underlying layer,那么它就是视图的backgroundColor)。改变backgroundColor会立即生效。可以这样认为backgroundColorlayer自己的绘制是分开的, 而且在layer自己的绘制的下面。
  • opacity
    这会影响layer整体的透明度。它相当于一个视图的alpha属性(并且如果该layer是一个视图的underlying layer,它就是视图的alpha)。它也会影响layer的子layer的透明度。它分别影响layer的背景颜色和内容的透明度(和视图的alpha属性类似)。改变opacity属性立即生效。
  • opaque
    确定layer的图形上下文是否是不透明的。不透明的图形上下文是黑色的;你可以在黑色的背景上绘制,但是黑色背景会被保留。非不透明的图形上下文是clear的;没有绘制时,它是完全透明的。改变opaque属性不会马上起作用,直到重新显示layer自己。视图的underlying layeropaque属性完全独立视图的opaque属性;他们是不相关的,做完全不同的事情。

Content Resizing and Positioning

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

  • 如果layer的内容是通过给contents属性设置一张图片,那么缓存的内容就是这张图片,大小就是CGImage的大小。
  • 如果layer的内容是直接绘制到layer的图形上下文(drawInContext:,drawLayer:inContext:)中的,缓存的内容是layer的整个图形上下文;它的大小是在执行绘制时layer的大小。

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

  • contentsGravity
    这个属性是一个字符串,类似于UIViewcontentMode属性,它描述了layercontent相对于bounds如何被定位或者拉伸。例如,kCAGravityCenter意味着内容在边界居中而且不改变大小;kCAGravityResize(默认值)意味着内容调整大小以适合bounds,即使拉伸;等等。

由于历史原因,contentsGravity值中BottomTop于它们的字面意思相反。

  • 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相适应。

而且,如果一个layer的内容来自直接绘制到其图形上下文中的绘制,那么当该layer被调整大小时,如果该layer被要求再次显示本身,绘制会被再次执行使layer的内容和layer的大小相匹配。但是,如果当needsDisplayOnBoundsChangefalse的时layer的大小被调整的时候,则该layer不重新显示本身,所以其缓存的内容不再适合layer的大小,那么contentsGravity就会起作用。

通过一些聪明的设置,你就可以执行一些平时很难执行的绘制任务。例如,代码如下:

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

效果如下图:


由于needsDisplayOnBoundsChangefalse,当箭头的边界增加时不会重新显示内容;相反,会使用缓存的内容。contentsGravity属性显示要按比例调整大小;因此,箭头会更长更宽,而不是一种扭曲的比例。然而,请注意,虽然三角形箭头更宽,但它没有变更长;因为contentsCenter包括箭头的轴,所以轴就整个拉伸了。

layermasksToBounds属性在其自己的内容和子layer上有相同的效果。如果值是false,则显示全部内容,即使该内容超过layer的大小。如果值是true,将只显示该layer的边界内的内容。

Layers that Draw Themselves

一些内置的CALayer子类提供一些基本的但非常有用的自我绘制能力:

  • CATextLayer
    一个CATextLayer有一个字符串属性,它可以是一个NSStringNSAttributedString,与其他文本格式属性一起,有点像一个简化的UILabel;它绘制它的字符串。默认的文本颜色,就是ForegroundColor属性,是白色的,这不太可能是你想要的效果。textcontents是不同的而且是互斥的:内容图片和文字只有一个会被绘制,所以一般你不应该给任何CATextLayer设置内容图像。上面图片中,基点字母就是CATextLayer实例。
  • CAShapeLayer
    CAShapeLayer有一个path属性,这是一个CGPath。它填充或描边此路径,或两者,这取决于其fillColor和则strokeColor值,并显示描边或填充的结果;fillColor默认是黑色,默认没有storkeColor。它有线宽,虚线样式,端帽样式属性,和连接样式,类似于图形上下文;它也有绘制其路径(strokeStartstrokeEnd)的一部分的非凡能力,例如,绘制一个椭圆的一段弧线。一个CAShapeLayer也可能有contents;形状被显示在内容图像的顶部,但没有属性指定一个合成模式。在上面的图片中,背景圆是一个CAShapeLayer实例,灰色描边,明亮和稍微透明的灰色填充。
  • CAGradientLayer
    CAGradientLayer用简单的线性渐变覆盖它的背景;因此,在界面上用它绘制简单的渐变很容易。渐变和Core graphics中的差不多,有一个颜色的数组,与一个起始和结束点沿。可以将mask添加到CAGradientLayer上裁剪形状。CAGradientLayer不会显示contents的内容。
    下图显示一个渐变效果的指南针:

Transforms

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

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

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

// 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属性提供。因此,在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轴:

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访问)是渐变层,并且圆和箭头是其子层,使得它们一起旋转:

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给出了一个更夸张的效果。此外,rotationLayerzPosition也会影响它得大小。

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

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

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,持有渐变layercircle 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模型。

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:它是一个椭圆,用不透明颜色填充并且厚的半透明的颜色描边。代码如下:

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,masksToBoundsmask,可以以更通用的方式来执行。例如,下面是产生远角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可以做为任何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

  • 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”键的值后确定这些层。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容

  • 转载:http://www.jianshu.com/p/32fcadd12108 每个UIView有一个伙伴称为l...
    F麦子阅读 5,914评论 0 13
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,311评论 6 30
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 4,982评论 5 13
  • Core Animation其实是一个令人误解的命名。你可能认为它只是用来做动画的,但实际上它是从一个叫做Laye...
    小猫仔阅读 3,527评论 1 4
  • 城市下着雨,回家的路总是崎岖。 小时候,很惧怕下雨,不是不喜欢雨,而是不喜欢泥泞。无论多大的风雨,总要一个人去上学...
    孙冬雪silense阅读 219评论 0 0