Layers

Layers

  • A UIView does not actually draw itself onto the screen; it draws itself into its layer, and it is the layer that is portrayed on the screen.
  • a view is not redrawn frequently;
  • instead, its drawing is cached, and the cached version of the drawing (the bitmap backing store) is used where possible.
  • The cached version is, in fact, the layer.
  • the view’s graphics context is actually the layer’s graphics context.
  • a layer is the recipient and presenter of a view’s drawing
  • Layers are made to be animated
  • View持有layer,是layer的代理(CALayerDeletgate
    • 但layer不能找到View
  • View的大部分属性都只是其underlying layer的便捷方法
  • layer能操控和改变view的表现,而无需ask the view to redraw itself

自定义underlaying layer的方法

class CompassView : UIView {
    override class var layerClass : AnyClass {
        return CompassLayer.self
    }
}

Layers and Sublayers

  • layer的继承树跟view的继承树几乎一致

  • layer的maskToBounds属性决定了能否显示sublayer超出了其bounds的部分,这也是view的clipsToBounds的平行属性

  • sublayers是可写的,而subviews不是

    • 所以设为nil可以移除所有子层,但subview却需要一个个removeFromSuperview
  • zPostion决定了层级(order),默认值都是0.0

  • a layer does not have a center靠positionanchorPoint定位

    • position: 在superLayer中的位置
    • anchorPoint: 用小数表示的bound(宽/高)位置,左上(0, 0), 右下(1, 1), default:(0.5, 0.5)
    • 所以(0.5, 0.5)的anchorPoint,对应的poosition就等同于center了,理解一下
      • 其实就是说你的“锚点”在superLayer的什么位置的意思
    • When you get the frame, it is calculated from the bounds size along with the position and anchorPoint.
      • When you set the frame, you set the bounds size and position
// demo, 把一个80x40的layer,左上角放到(130, 120的位置)
let layer = CALayer()
layer.bounds = CGRect.init(x: 0, y: 0, width: 80, height: 40)
layer.backgroundColor = UIColor.yellow.cgColor
layer.position = CGPoint.init(x: 130, y: 120)
layer.anchorPoint = CGPoint.init(x: 0, y: 0)

如果一个layer的position是(0, 0),锚点是(0,0),刚好显示在左上角 而(0.5,0.5)则只能显示右下角的1/4了 即(0.5, 0.5)到了原来(0,0)的位置。所以说其实就是把自身bounds度量下的哪个位置移到(0,0)

这么说来,对锚点的最正确理解其实是,

  • 我把自身坐标系里的哪个点定义为原点,
  • 并且,这个点移到原本“左上角”的位置(想象0.5,0.5)
  • 并且,所有的旋转之类的动画本来是对“左上角”的位置进行的,不管现在这个位置是layer上的哪个部分
    • 或者说,旋转永远是发生在position上的,你把哪个点放到position上它不管

理解frame的小练习

// 如果我设了layer的frame:
circle.frame = CGRect.init(x: 50, y: 50, width: 200, height: 200)

// 实际上是通过size, position, anchorPoint来实现的:
circle.bounds = CGRect(x: 0, y: 0, width: 200, height: 200)

// 以左上角为anchorPoint
circle.position = CGPoint(x:50, y:50)
circle.anchorPoint = CGPoint(x:0, y:0)
// 或者,以中心为anchorPoint
circle.position = CGPoint(x:150, y:150)
circle.anchorPoint = CGPoint(x:0.5, y:0.5)
// 或者其它任意anchorPoint,前提是自己换算
// 而且,虽然位置是一样的,但会影响transform

CAScrollLayer

  • 你想通过移动layer的bounds来重定位sublayers,可以使用CAScrollLayer
  • 但是它并不能通过拖拽来移动里面的内容(记得它没有响应链)
  • 而是理解为一个masksToBounds的窗口,你只能看到它bounds里面的内容
  • 能通过本身的scroll(to:)方法,和sublayers的scroll(_:)scrollRectToVisible(_:)方法来改变scroll layer的bounds,达到显示sublayer指定区域的目的

Layer and Delegate

  • 对一个不是UIView的undrelying layer的layer,让(任意)一个对象成为其delegate,可以由它来操控它的layout和drawing
  • 但千万不要让UIView成为不是其underlying的layer的代理,反之亦然

Layout of Lyaers

  • When a layer needs layout, either because its bounds have changed or because you called setNeedsLayout

Drawing in a Layer

  • set contents is the simplest way to draw in a layer -> CGImage
    • contents能接受任何类型,所以不正确的content只会fail silently
  • layer也有一个draw(_:)方法,它被(自动)调用的时候通常表示要redisplay itself`,什么时候需要redisplay itself?
    • 如果needsDisplayOnBoundsChange是false,那么就只有在sefNeedDisplay方法(及其inRect衍生方法)里会触发
      • 如果是非常重要的重绘,那么需要再显式调用一次displayIfNeeded
    • 是true的话就如其名,在bounds变化的时候也会重绘
  • 有四个方法能在redisplay的时候调用:
    1. subclass的display重载,它没有graphics context,所以只能提供图片
    2. delegate的display(in:)方法,同样,只能提供图片
    3. subclass的draw(in:)方法,有context,所以能直接在里面绘图,但不会make current context
    4. delegate的draw(_:in)方法,限制也同上
  • underlaying layer不应调用上面的方法,而交由view的draw(_:)方法
    • 一定要调也可以,但要显式实现view的draw(_:)方法,方法体为空就行了

Drawing-Related Layer Properties

  • contentsScale: 像素对高分屏的映射,Cocoa管理的layer会自动设置,自定义的类需要注意这个scale
  • opacity: 就是view的alpha
    • Changing the isOpaque property has no effect until the layer redisplays itself.

Content Resizing and Positioning

  • A layer’s content is stored (cached) as a bitmap which is then treated like an image:
    • 如果content来自一张图片,那么缓存的就是图片(CGImage),大小就是图片的point size
    • 如果来自绘图,那么存的是graphics context
  • ContentGravity,类似UIView’s contentMode property,即缩放拉伸
    • 因为坐标系不同的历史原因,top, bottom是相反的
    • 如果是自己绘制,则这个属性无意义,但结合下面的rect属性又有用了,因为截取了rect大小的绘制
  • contentsRect,结合上一个属性,做购物网站那种截取一小块,绘制到一个大图上去。这里是绘制到view上
    • 默认是全图(0,0,1,1)
  • contentsCenter ?? 好像是对上述rect属性划成9宫格,不同位置的格子缩放规则不一样,比如四个角落的格子,不会缩放
    • 所以给了一个center region(rect),把它的四条边延长,就有9个格子了

Layers that Draw Themselves

系统内置了一些能自我绘制的layer:

  • CATextLayer,轻量版的UILabel。通过string属性存取,与contenta会冲突,不要同时设。
  • CAShapeLayer, 有path属性,可以与contents共存,path绘制于content之上,并且不能设融合模式
  • CAGradientLayer,通过背景色做的渐变,去了解下clip和mask

Transforms

  • view的transform是根据其center来应用的,layer的是根据anchorPoint
    • 所以anchorPoint就两个作用,把它移动到position的位置,和以它为中心进行旋转
  1. 画刻度,核心是把文字先往上挪到圆圈的位置,所以anchorPoint只动y不动x (center, midY/textHeight)
let str = "ABCD"
for (i, s) in str.enumerated() {
    let t = CATextLayer()
    t.string = String(s)
    t.bounds = CGRect.init(x: 0, y: 0, width: 40, height: 40)
    t.position = circle.center // 这才是核心,一切定位和旋转的基准
    let vert = circle.bounds.midY/t.bounds.height
    t.anchorPoint = CGPoint.init(x: 0.5, y: vert) // 半圆是文字调蓄的多少倍,就上移多少,但隐形的脚(即高跷的支点)仍在position处
    t.foregroundColor = UIColor.red.cgColor
    
    t.setAffineTransform(CGAffineTransform(rotationAngle: .pi/2.0 * CGFloat(i)))
    circle.addSublayer(t)
}

结果如图:

  1. 画箭头,演示了复杂的绘制怎么把它代理出去,并且什么时机让它产生绘制:
// the arrow
let arrow = CALayer()
arrow.contentsScale = UIScreen.main.scale
arrow.bounds = CGRect(0, 0, 40, 100)
arrow.position = self.bounds.center
arrow.anchorPoint = CGPoint(0.5, 0.8) // 箭尾凹进去的位置(所以不可能是1.0)
arrow.delegate = self // we will draw the arrow in the delegate method
arrow.setAffineTransform(CGAffineTransform(rotationAngle:.pi/5.0))
self.addSublayer(arrow)
arrow.setNeedsDisplay() // draw, please

** 3D Transforms

  • A layer’s affineTransform is merely a façade for accessing its transform.
  • A layer’s transform is a three-dimensional transform, a CATransform3D

绕Y轴镜像的示例:

someLayer.transform = CATransform3DMakeRotation(.pi, 0, 1, 0)

一般而言,在Z轴没有分量的平面图,那就只剩旋转的效果了(没有翻转)

这是把anchorPoint设在了圆心,如果设在(0,0):

  • layer不是为了3D建模而诞生的(考虑Metal),它是2D对象,为speedsimplicity而设计

depth

现实世界z-component的加入会近大远小,layer绘制没有表现出这种距离,而是压平到一个面:orthographic projection,但是使用了一些技巧来制造这种视觉效果。