读书笔记: iOS Layer 绘制

读 Programming iOS 10

除了使用 image context 和 UIView 提供的 context 进行绘制外, 还有另外的方式可以实现绘制, 就是 layer.

UIView 中有一个 layer 属性, 类型是 CALayer. 看前缀就知道是在 Core Animation 框架中定义的.

实际上 UIView 并非将自身绘制到屏幕上, 而是将自己绘制到 layer 上, 然后这个 layer 绘制到屏幕.

前面也提到过, UIView 没有经常重绘自己, 而是将绘制缓存起来, 然后在需要使用的时候直接读缓存(bitmap backing store)即可. 而实际就是通过 layer 来缓存绘制.

重点: UIView 在 draw 方法中获取到的 context 实际上就是 layer 的上下文.

而 Layer 相比 view 在绘图和动画方面都更加强大:

  • Layer 包含更多的和绘制相关的属性.

  • 在同一个 view 中, layer 可以被组合.

  • layer 是动画的基础.

不要把这里的 layer 和 Quartz 2D 中的 transparency layer 以及 CGLayer(CGLayer 需要有一个上下文来创建它) 搞混了. 这里的 Layer 指 CALayer.

总地来说, Layer 既是 View 绘制的基础(除去绘制 image, view 的绘制都是以 layer 为基础.), 又是动画的基础, 所以必须要对它有深入了解.

1 View 和 Layer

每个 UIView 都有一个和它配合的 CALayer, 可以通过 layer 属性获取到. 这个 layer 有一个特殊职责: 将 view 的所有呈现内容都嵌入到它里面, 故文档中有时将view 的 layer属性对应的 layer 称为 the view’s underlying layer.

View 和自身的 layer 关系如下:

读书笔记: iOS Layer 绘制_第1张图片
View 和 Layer 的关系

即 view 是 layer 的代理.

默认情况下 UIView 的 layer 是 CALayer 类型的, 不过可以通过继承 UIView 重写 layerClass 属性 来指定不同类型的 layer(但需要是 CALayer 的子类类型):

    override class var layerClass : AnyClass {
        return CAShapeLayer.self
    }

上面代码在当前版本 IDE(Xcode 9.2) 上可能不会进行自动提示, 所以需要自己输入.

以下将 view 的 layer 属性对应的 layer 称为view 的 根layer.

另外, CALayer 不能脱离 UIView 进行显示. 所以要显示 CALayer 的内容, 需要将其加入到 UIView 的layer 树中.

另外由于 UIView 默认是它的根 layer 的代理, 故可以直接在其中实现 layer 的一些代理方法.

另外实际 View 的许多属性都只是根 layer 的属性的便捷访问方式, 比如在设置 view 的 backgroundColor 的时候, 实际是在设置根 layer 的 backgroundColor. 如果你设置根 layer 的 backgroundColor, 则读取 view 的 backgroundColor 时就是设置的值. 类似的还有 view 的 frame, 实际就是 layer 的 frame.

虽然一个 layer 的 delegate 属性可以随意设置, 但由于 UIView 和它的根 layer 的特殊关系, 所以UIView 只能作为它的根 layer 的代理, 且根 layer 的代理也必须该 UIView. 需要强制执行这个规则, 否则绘制的时候就可能出错.

view 绘制时是绘制到 layer 中, layer 将绘制缓存起来. 且可以直接操作 layer 上的绘制, 而无需 view 进行重绘. 正是在这样的机制下, iOS 的绘制系统才能如此高效.

2 Layer 和 sublayer

视图树中每一个 view 的根 layer 根据视图关系, 形成相同关系的 layer 树, 同时每一个根 layer 还可以有自己的 sublayer, 如下图所示:

读书笔记: iOS Layer 绘制_第2张图片
视图树和图层树的对应关系

这里有一个问题, 在添加 layer 的同时添加子视图, 那最后绘制出来的结果中子视图和 layer 的层次关系是什么呢?

要回答这个问题, 首先要明确: 如果视图 A 是视图 B 的子视图, 则 A 的根 layer 也是 B 的根 layer 的 superlayer.

故仍然是根据添加的先后顺序来决定最终的渲染结果.

另外当 layer 中的绘制内容超过其 frame 的边界时, 可以通过 masksToBounds 属性来决定是否绘制边界以外的内容. 而 view 的 clipsToBounds 属性实际就是在设置 layer 的 maskToBounds 属性. 默认情况下设置为 false, 即要绘制超过边界的内容.

layer 也有 isHidden 属性, 通过它来设置是否显示 layer 的内容.

3 操作图层树

图层树和视图树一样可以操作, 具体使用如下方法:

  • addSublayer(_:)
  • insertSublayer(_:at:)
  • insertSublayer(:below:),insertSublayer(:above:)
  • replaceSublayer(_:with:)
  • removeFromSuperlayer

上述方法可以对单个的 sublayer 进行操作.

不同于 view, layer 的 sublayers 属性是可以直接操作的. 比如想移除所有的 sublayer, 则直接将该属性设置为 nil 即可.

作为兄弟的 sublayer 的绘制顺序是由添加顺序决定的. 这样的情况仅针对 zPosition 相同的情况下(默认是 0.0), 如果不同的时候, 则是按 zPosition 的大小进行绘制, 小的先绘制, 先绘制的被后绘制的遮挡.

另外系统还提供有在同一个图层树中不同分支下的 layer 坐标相互转换的方法:

  • convert(_:from:)
  • convert(_:to:)

参数可以是 Point 以及 Rect.

4 关于图层的定位

图层在其对应的 view 坐标系中, 或是在对应的父 layer 的坐标系中的大小是通过 bounds 定义的. 但位置是通过如下两个属性共同定义:

  • position: 描述的是在父layer坐标系下的一个点.

  • anchorPoint: 描述 position 点在 layer 上的位置, 这个位置是相对于 layer 自身的坐标系而言的, 且值为 0 到 1.

这样的定位方式实际就是: 先在 layer 上确定锚点, 然后将锚点挂接到 position 指定的位置上, 就定位了该
layer 在父 layer 中的位置.

anchorPoint 的默认值是 (0.5, 0.5), 即 layer 的中央位置. 这时 position 的作用就和 view 的 center 一样.

而 layer 的 frame 实际是通过当前的 bounds 和 position 以及 anchorPoint 共同计算出来的. 当设置 layer 的 frame 时, 相当于同时修改了 layer 的 bounds 和 position.

5 关于图层的布局

如果一个图层是视图的根图层, 则其布局就是通过视图的若干布局方式完成的. 但如果一个图层不是视图的根图层, 则它的布局在 iOS 中只有一种办法: 就是在代码中手动布局.

当图层需要进行布局时, 主要调用的是如下方法:

  • 图层自己的 layoutSublayers 方法被调用. (这个只有在继承图层后重写).

  • 图层代理的 layoutSublayers(of:) 方法. 根图层的代理方法必须在对应视图中实现. 其他图层需要指定代理对象.

图层的布局时机有:

视图的根图层: layoutSublayerslayoutSublayers(of:) 方法是在 view 的 layoutSubviews 方法之后被调用.

而作为子图层的话, 调用时机显然就是在父图层布局之后.

在使用约束布局时, 再次强调 重写
layoutSubviews 方法时必须先调用 super.

6 在图层中绘图

最简单的办法是设置图层的 contents 属性, 它接收一张 CGImage 图片(使用 UIImage 的话不会显示出来, 并且不会提示错误...).

下面就是一些绘制入口:

  • 重写子类的 display 方法

  • 重写子类的 draw(in:) 方法

  • 实现代理的 display(_:) 方法

  • 实现代理的 draw(_:in:) 方法

根据实际情况选用, display 方法主要用于设置 contents 属性, 因为在里面没有提供 context...

在根图层中绘制的时候, 需要注意如下两点:

  • layer 的代理或者子类中不能同时实现 drawdisplay, 如果同时实现了这两个方法, 则只有 display 方法会被执行. 在官方文档中有说明.
  • 在代理实现 drawdisplay 方法时, 必须要在 view 子类中提供 view 的 draw 方法的实现. 即使这个实现是空的也行, 如果没有这个的话, layer 的 drawdisplay 方法都不会被调用. 这个在官方文档中也有说明.

由于上面的两点原因, 故如果是在根图层中绘制, 更多时候是直接实现 view 的 draw 方法进行绘制.

在为图层提供 contents 时, 需要指定其 contentsScale 的大小, 否则绘制可能会失真.

另外有几个属性需要经常用到的:

  • backgroundColor: 背景色
  • opacity: 透明程度, 如果是根图层,则这个值就是 view 的 alpha值.
  • isOpaque: 决定图层的上下文是否透明. 不透明的上下文是黑色的.透明的上下文当然就是透明的了... 而这个属性和 view 的 isOpaque 属性是两个不同的东西, 且完成两个不同的功能.

7 绘制内容的尺寸和位置调整

图层的内容是在一个位图(bitmap)中保存的, 被看作 image.

下面是一些影响图层绘制的属性:

  • contentsGravity: 利用 contentsGravity 属性可以设置内容在图层中的渲染办法, 比如 kCAGravityCenter 表示图层中的内容会被居中绘制, 不会进行尺寸调整. 默认情况下这个属性的值是 kCAGravityResize.

  • contentsRect: 可以设置图层中的某个部分被显示出来, 默认情况下这个属性的值是 (0.0, 0.0, 1.0, 1.0). 它和 contentsGravity 属性共同决定内容的绘制.

  • contentsCenter: 用于定义拉伸时候的保持尺寸区域?

8 一些特殊的Layer

在框架中定义有一些特殊的 Layer 可供使用, 这些 Layer 往往具有特定的功能:

  • CATextLayer: 有 string 属性, 它可以自动将该属性表示的文字绘制出来.

  • CAShapeLayer: 有 path 属性, 为 CGPath 类型, 它可以根据是否拥有 fillColorstrokeColor 值来决定将路径进行填充或描边, 默认无描边色, 默认填充色为黑色. 另外它还有一些和图形上下文类似的属性, 这样可以设置 path 在其中如何绘制. 它也具有 content 属性, 当和 path 属性配合使用时, path 是在 content 图片之上的.

  • CAGradientLayer: 它提供了一种绘制渐变色的更方便的方式.

9 二维变换

通过 Transform 可以更加方便地在图层中进行绘制.

利用 affineTransform 属性可以查看当前的 transform. 利用 setAffineTransform(_:) 可以设置图层的二维变换.

不过需要注意, 二维变换是应用在 anchorPoint 上的, 而非类似 view 的 origin.

10 三维变换

三维变换是通过 transform 属性进行设置的.

11 深度(透视)操作

在 Layer 绘制中, 如果改变 z 值大小, 并不会和现实世界中那样呈现图形在近处变大.

如果想让图形如现实世界中绘图那样, 在近处显得更大, 则需要一些额外的技术.

方式是: 在图层应用 sublayerTransform, 让子图层的所有点都投影到一个平面上, 这样看起来图像就呈现了深度的效果.(这也许是 sublayerTransform 的唯一用途)

比如图层在 z 轴的负方向上时, 就会变得更小.

另外一种绘制透视效果图层的办法是使用 CATransformLayer 类. 它是主要作用是作为其他图层的容器, 而不是在其上绘制.

12 图层的其他属性

  • Shadows
  • Borders, Rounded Corners
  • Masks

13 图层性能相关问题

图层的不透明绘制是最高效的. 比如当图层后面的背景色一直都是一个颜色时, 可以让图层不透明, 然后让图层的背景色和后面的背景色一致即可.

另外还有许多, 这个慢慢去学, 去练习.

你可能感兴趣的:(读书笔记: iOS Layer 绘制)