iOS 中 UIView 和 CALayer 的关系

UIView 有一个名叫 layer ,类型为 CALayer 的对象属性,它们的行为很类似,主要区别在于:CALayer 继承自 NSObject ,不可以响应事件html

这是由于 UIView 除了负责响应事件 ( 继承自 UIReponder ) 外,它仍是一个对 CALayer 的底层封装。能够说,它们的类似行为都依赖于 CALayer 的实现,UIView 只不过是封装了它的高级接口而已。git

CALayer 是什么呢?github

CALayer(图层)

文档对它定义是:管理基于图像内容的对象,容许您对该内容执行动画编程

概念

图层一般用于为 view 提供后备存储,但也能够在没有 view 的状况下使用以显示内容。图层的主要工做是管理您提供的可视内容,但图层自己能够设置可视属性(例如背景颜色、边框和阴影)。除了管理可视内容外,该图层还维护有关内容几何的信息(例如位置、大小和变换),用于在屏幕上显示该内容swift

和 UIView 之间的关系

示例1 - layer 影响视图的变化:markdown

let view = UIView(frame: CGRect(x: 44, y: 44, width: UIScreen.width - 88, height: 300))
view.backgroundColor = .red
view.layer.backgroundColor = UIColor.orange.cgColor

print("view: \(view.backgroundColor!)")
print("layer: \(view.layer.backgroundColor!)")

// Prints "view: 1 0.5 0 1"
// Prints "layer: 1 0.5 0 1"

view.layer.frame.origin.y = 100

print("view: \(view.frame.origin.y)")
print("layer: \(view.layer.frame.origin.y)")

// Prints "view: 100"
// Prints "layer: 100"
复制代码

能够看到,不管是修改了 layer 的可视内容或是几何信息,view 都会跟着变化,反之也是如此。这就证实:UIView 依赖于 CALayer 得以显示。app

既然他们的行为如此类似,为何不直接用一个 UIViewCALayer 处理全部事件呢?主要是基于两点考虑:框架

  1. 职责不一样iview

    UIVIew 的主要职责是负责接收并响应事件;而 CALayer 的主要职责是负责显示 UI。async

  2. 须要复用

    在 macOS 和 App 系统上,NSViewUIView 虽然行为类似,在实现上却有着显著的区别,却又都依赖于 CALayer 。在这种状况下,只能封装一个 CALayer 出来。

CALayerDelegate

你可使用 delegate (CALayerDelegate) 对象来提供图层的内容,处理任何子图层的布局,并提供自定义操做以响应与图层相关的更改。若是图层是由 UIView 建立的,则该 UIView 对象一般会自动指定为图层的委托。

注意:

  1. 在 iOS 中,若是图层与 UIView 对象关联,则必须将此属性设置为拥有该图层的 UIView 对象。
  2. delegate 只是另外一种为图层提供处理内容的方式,并非惟一的。UIView 的显示跟它图层委托没有太大关系。
  1. func display(_ layer: CALayer)

    当图层标记其内容为须要更新 ( setNeedsDisplay() ) 时,调用此方法。例如,为图层设置 contents 属性:

    let delegate = LayerDelegate()
         
    lazy var sublayer: CALayer = {
        let layer = CALayer()
        layer.delegate = self.delegate
        return layer
    }()
         
    // 调用 `sublayer.setNeedsDisplay()` 时,会调用 `sublayer.display(_:)`。
    class LayerDelegate: NSObject, CALayerDelegate {
        func display(_ layer: CALayer) {
            layer.contents = UIImage(named: "rabbit.png")?.cgImage
        }
    }
    复制代码

    那什么是 contents 呢?contents 被定义为是一个 Any 类型,但实际上它只做用于 CGImage 。形成这种奇怪的缘由是,在 macOS 系统上,它能接受 CGImageNSImage 两种类型的对象。

    你能够把它想象中 UIImageView 中的 image 属性,其实是,UIImageView 在内部经过转换,将 image.cgImage 赋值给了 contents

    注意:

    若是是 view 的图层,应避免直接设置此属性的内容。视图和图层之间的相互做用一般会致使视图在后续更新期间替换此属性的内容。

  2. func draw(_ layer: CALayer, in ctx: CGContext)

    display(_:) 同样,可是可使用图层的 CGContext 来实现显示的过程(官方示例):

    // sublayer.setNeedsDisplay()
    class LayerDelegate: NSObject, CALayerDelegate {
        func draw(_ layer: CALayer, in ctx: CGContext) {
            ctx.addEllipse(in: ctx.boundingBoxOfClipPath)
            ctx.strokePath()
        }
    }
    复制代码
    • 和 view 中 draw(_ rect: CGRect) 的关系

      文档对其的解释大概是:

      此方法默认不执行任何操做。使用 Core Graphics 和 UIKit 等技术绘制视图内容的子类应重写此方法,并在其中实现其绘图代码。 若是视图以其余方式设置其内容,则无需覆盖此方法。 例如,若是视图仅显示背景颜色,或是使用基础图层对象直接设置其内容等。

      调用此方法时,在调用此方法的时候,UIKit 已经配置好了绘图环境。具体来讲,UIKit 建立并配置用于绘制的图形上下文,并调整该上下文的变换,使其原点与视图边界矩形的原点匹配。可使用 UIGraphicsGetCurrentContext() 函数获取对图形上下文的引用(非强引用)。

      那它是如何建立并配置绘图环境的?我在调查它们的关系时发现:

      /// 注:此方法默认不执行任何操做,调用 super.draw(_:) 与否并没有影响。
      override func draw(_ rect: CGRect) {
          print(#function)
      }
      
      override func draw(_ layer: CALayer, in ctx: CGContext) {
          print(#function)
      }
      
      // Prints "draw(_:in:)"
      复制代码

      这种状况下,只输出图层的委托方法,而屏幕上没有任何 view 的画面显示。而若是调用图层的 super.draw(_:in:) 方法:

      /// 注:此方法默认不执行任何操做,调用 super.draw(_:) 与否并没有影响。
      override func draw(_ rect: CGRect) {
          print(#function)
      }
      
      override func draw(_ layer: CALayer, in ctx: CGContext) {
          print(#function)
          super.draw(layer, in: ctx)
      }
      
      // Prints "draw(_:in:)"
      // Prints "draw"
      复制代码

      屏幕上有 view 的画面显示,为何呢?首先咱们要知道,在调用 view 的 draw(_:in:) 时,它须要一个载体/画板/图形上下文 ( UIGraphicsGetCurrentContext ) 来进行绘制操做。因此我猜想是,这个 UIGraphicsGetCurrentContext 是在图层的 super.draw(_:in:) 方法里面建立和配置的。

      具体的调用顺序是:

      1. 首先调用图层的 draw(_:in:) 方法;
      2. 随后在 super.draw(_:in:) 方法里面建立并配置好绘图环境;
      3. 经过图层的 super.draw(_:in:) 调用 view 的 draw(_:) 方法。

      此外,还有另外一种状况是:

      override func draw(_ layer: CALayer, in ctx: CGContext) {
          print(#function)
      }
      复制代码

      只实现一个图层的 draw(_:in:) 方法,而且没有继续调用它的 super.draw(_:in:) 来建立绘图环境。那在没有绘图环境的时候,view 能显示吗?答案是能够的!这是由于:view 的显示不依赖于 UIGraphicsGetCurrentContext ,只有在绘制的时候才须要。

    • contents 之间的关系

      通过测试发现,调用 view 中 draw(_ rect: CGRect) 方法的全部绘制操做,都被保存在其图层的 contents 属性中:

      // ------ LayerView.swift ------
      override func draw(_ rect: CGRect) {
          UIColor.brown.setFill()	// 填充
          UIRectFill(rect)
      
          UIColor.white.setStroke()	// 描边
          let frame = CGRect(x: 20, y: 20, width: 80, height: 80)
          UIRectFrame(frame)
      }
      
      // ------ ViewController.swift ------
      DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
          print("contents: \(self.layerView.layer.contents)")
      }
      
      // Prints "Optional(<CABackingStore 0x7faf91f06e20 (buffer [480 256] BGRX8888)>)"
      复制代码

      这也是为何要 CALayer 提供绘图环境、以及在上面介绍 contents 这个属性时须要注意的地方。

      重要:

      若是委托实现了 display(_ :) ,将不会调用此方法。

    • display(_ layer: CALayer) 之间的关系

      前面说过,view 的 draw(_:) 方法是由它图层的 draw(_:in:) 方法调用的。可是若是咱们实现的是 display(_:) 而不是 draw(_:in:) 呢?这意味着 draw(_:in:) 失去了它的做用,在没有上下文的支持下,屏幕上将不会有任何关于 view 的画面显示,而 display(_:) 也不会自动调用 view 的 draw(_:) ,view 的 draw(_:) 方法也失去了意义,那 display(_ layer: CALayer) 的做用是什么?例如:

      override func draw(_ rect: CGRect) {
          print(#function)
      }
      
      override func display(_ layer: CALayer) {
          print(#function)
      }
      
      // Prints "display"
      复制代码

      这里 draw(_:) 没有被调用,屏幕上也没有相关 view 的显示。也就是说,此时除了在 display(_:) 上进行操做外,已经没有任何相关的地方能够设置图层的可视内容了(参考 "1. func display(_ layer: CALayer)",这里再也不赘述,固然也能够设置背景颜色等)。固然,你可能永远都不会这么作,除非你建立了一个单独的图层。

      至于为何不在 display(_ layer: CALayer) 方法里面调用它的父类实现,这是由于若是调用了会崩溃:

      // unrecognized selector sent to instance 0x7fbcdad03ba0
      复制代码

      至于为何?根据个人参考资料,他们都没有在此继续调用 super ( UIView ) 的方法。我随意猜想一下是这样的:

      首先错误提示的意思翻译过来就是:没法识别的选择器(方法)发送到实例。那咱们来分析一下,是哪个实例中?是什么方法?

      1. super 实例;
      2. display(_ layer: CALayer)

      也就是说,在调用 super.display(_ layer: CALayer) 方法的时候,super 中找不到该方法。为何呢?请注意 UIView 默认已经遵循了 CALayerDelegate 协议(右键点击 UIView 查看头文件),可是应该没有实现它的 display(_:) 方法,而是选择交给了子类去实现。相似的实现应该是:

      // 示意 `CALayerDelegate`
      @objc protocol LayerDelegate: NSObjectProtocol {
          @objc optional func display()
          @objc optional func draw()
      }
      
      // 示意 `CALayer`
      class Layer: NSObject {
          var delegate: LayerDelegate?
      }
      
      // 示意 `UIView`
      class BaseView: NSObject, LayerDelegate {
          let layer = Layer()
          override init() {
              super.init()
              layer.delegate = self
          }
      }
      // 注意:并无实现委托的 `display()` 方法。
      extension BaseView: LayerDelegate {
          func draw() {}
      }
      
      // 示意 `UIView` 的子类
      class LayerView: BaseView {
          func display() {
              // 一样的代码在OC上实现没有问题。
              // 因为Swift是静态编译的关系,它会检测在 `BaseView` 类中有没有这个方法,
              // 若是没有就会提示编译错误。
              super.display()
          }
      }
      
      // ------ ViewController.swift ------
      let layerView = LayerView()
      // 若是在方法里面调用了 `super.display()` 将引起崩溃。
      layerView.display()
      // 正常执行
      layerView.darw()
      复制代码

    注意:

    只有当系统在检测到 view 的 draw(_:) 方法被实现时,才会自动调用图层的 display(_:)draw(_ rect: CGRect) 方法。不然就必须经过手动调用图层的 setNeedsDisplay() 方法来调用。

  3. func layerWillDraw(_ layer: CALayer)

    draw(_ layer: CALayer, in ctx: CGContext) 调用以前调用,可使用此方法配置影响内容的任何图层状态(例如 contentsFormatisOpaque )。

  4. func layoutSublayers(of layer: CALayer)

    UIViewlayoutSubviews() 相似。当发现边界发生变化而且其 sublayers 可能须要从新排列时(例如经过 frame 改变大小),将调用此方法。

  5. func action(for layer: CALayer, forKey event: String) -> CAAction?

    CALayer 之因此可以执行动画,是由于它被定义在 Core Animation 框架中,是 Core Animation 执行操做的核心。也就是说,CALayer 除了负责显示内容外,还能执行动画(实际上是 Core Animation 与硬件之间的操做在执行,CALayer 负责存储操做须要的数据,至关于 Model)。所以,使用 CALayer 的大部分属性都附带动画效果。可是在 UIView 中,默认将这个效果给关掉了,能够经过它图层的委托方法从新开启 ( 在 view animation block 中也会自动开启 ),返回决定它动画特效的对象,若是返回的是 nil ,将使用默认隐含的动画特效。

    示例 - 使用图层的委托方法返回一个从左到右移动对象的基本动画:

    final class CustomView: UIView {
        override func action(for layer: CALayer, forKey event: String) -> CAAction? {
            guard event == "moveRight" else {
                return super.action(for: layer, forKey: event)
            }
            let animation = CABasicAnimation()
            animation.valueFunction = CAValueFunction(name: .translateX)
            animation.fromValue = 1
            animation.toValue = 300
            animation.duration = 2
            return animation
        }
    }
    
    let view = CustomView(frame: CGRect(x: 44, y: 44, width: UIScreen.width - 88, height: 300))
    view.backgroundColor = .orange
    self.view.addSubview(view)
    
    let action = view.layer.action(forKey: "moveRight")
    action?.run(forKey: "transform", object: view.layer, arguments: nil)
    复制代码

    那怎么知道它的哪些属性是能够附带动画的呢?核心动画编程指南列出了你可能须要考虑设置动画的 CALayer 属性:

CALayer 坐标系

CALayer 具备除了 framebounds 以外区别于 UIView 的其余位置属性。UIView 使用的所谓 frameboundscenter 等属性,其实都是从 CALayer 中返回的,而 frame 只是 CALayer 中的一个计算型属性而已。

这里主要说一下 CALayer 中的 anchorPointposition 这两个属性,也是 CALayer 坐标系中的主要依赖:

  • var anchorPoint: CGPoint ( 锚点 )

    图层锚点示意图

    看 iOS 部分便可。能够看出,锚点是基于图层的内部坐标,它取值范围是 (0-1, 0-1) ,你能够把它想象成是 bounds 的缩放因子。中间的 (0.5, 0.5) 是每一个图层的 anchorPoint 默认值;而左上角的 (0.0, 0.0) 被视为是 anchorPoint 的起始点。

    任何基于图层的几何操做都发生在指定点附近。例如,将旋转变换应用于具备默认锚点的图层会致使围绕其中心旋转,锚点更改成其余位置将致使图层围绕该新点旋转。

    锚点影响图层变换示意图
  • var position: CGPoint ( 锚点所处的位置 )

    锚点影响图层的位置示意图

    看 iOS 部分便可。图1中的 position 被标记为了 (100, 100) ,怎么来的?

    对于锚点来讲,它在父图层中有着更详细的坐标。对 position 通俗来解释一下,就是锚点在父图层中的位置

    一个图层它的默认锚点是 (0.5, 0.5) ,既然如此,那就先看下锚点 x 在父图层中的位置,能够看到,从父图层 x 到锚点 x 的位置是 100,那么此时的 position.x 就是 100;而 y 也是相似的,从父图层 y 到锚点 y 的位置也是 100;则能够得出,此时锚点在父图层中的坐标是 (100, 100) ,也就是此时图层中 position 的值。

    对图2也是如此,此时的锚点处于起始点位置 (0.0, 0.0) ,从父图层 x 到锚点 x 的位置是 40;而从父图层 y 到锚点 y 的位置是 60 ,由此得出,此时图层中 position 的值是 (40, 60)

    这里其实计算 position 是有公式的,根据图1能够套用以下公式:

    1. position.x = frame.origin.x + 0.5 * bounds.size.width
    2. position.y = frame.origin.y + 0.5 * bounds.size.height

    由于里面的 0.5 是 anchorPoint 的默认值,更通用的公式应该是:

    1. position.x = frame.origin.x + anchorPoint.x * bounds.size.width
    2. position.y = frame.origin.y + anchorPoint.y * bounds.size.height

    注意:

    实际上,position 就是 UIView 中的 center 。若是咱们修改了图层的 position ,那么 view 的 center 会随之改变,反之也是如此。

anchorPoint 和 position 之间的关系

前面说过,position 处于锚点中的位置(相对于父图层)。这里就有一个问题,那就是,既然 position 相对于 anchorPoint ,那若是修改了 anchorPoint 会不会致使 position 的变化?结论是不会:

let redView = UIView(frame: CGRect(x: 40, y: 60, width: 120, height: 80))
print(self.redView.layer.position)	// Prints "(100.0, 100.0)"
redView.layer.anchorPoint = CGPoint(x: 0, y: 1)
print(self.redView.layer.position)	// Prints "(100.0, 100.0)"
复制代码

那修改了 position 会致使 anchorPoint 的变化吗?结论是也不会:

let redView = UIView(frame: CGRect(x: 40, y: 60, width: 120, height: 80))
print(redView.layer.anchorPoint)	// Prints "(0.5, 0.5)"
redView.layer.anchorPoint = CGPoint(x: 0, y: 1)
print(redView.layer.anchorPoint)	// Prints "(0.5, 0.5)"
复制代码

通过测试,不管修改了谁另外一方都不会受到影响,受到影响的只会是 frame.origin 。至于为何二者互不影响,我暂时还没想到。我随意猜想一下是这样的:

其实 anchorPoint 就是 anchorPointposition 就是 position 。他们自己实际上是没有关联的,由于它们默认处在的位置正好重叠了,因此就给咱们形成了一种误区,认为 position 就必定是 anchorPoint 所在的那个点。

和 frame 之间的关系

CALayerframe文档中被描述为是一个计算型属性,它是从 boundsanchorPointposition 的值中派生出来的。为此属性指定新值时,图层会更改其 positionbounds 属性以匹配您指定的矩形。

那它们是如何决定 frame 的?根据图片能够套用以下公式:

  1. frame.x = position.x - anchorPoint.x * bounds.size.width
  2. frame.y = position.y - anchorPoint.y * bounds.size.height

这就解释了为何修改 positionanchorPoint 会致使 frame 发生变化,咱们能够测试一下,假设把锚点改成处在左下角 (0.0, 1.0) :

let redView = UIView(frame: CGRect(x: 40, y: 60, width: 120, height: 80))
redView.layer.anchorPoint = CGPoint(x: 0, y: 1)
print(redView.frame.origin)	// Prints "(100.0, 20.0)"
复制代码

用公式来计算就是:frame.x (100) = 100 - 0 * 120frame.y (20) = 100 - 1 * 80 ;正好和打印的结果相符。反之,修改 position 属性也会致使 frame.origin 发生如公式般的变化,这里就再也不赘述了。

注意:

若是修改了 frame 的值是会致使 position 发生变化的,由于 position 是基于父图层定义的;frame 的改变意味着它自身的位置在父图层中有所改变,position 也会所以改变。

可是修改了 frame 并不会致使 anchorPoint 发生变化,由于 anchorPoint 是基于自身图层定义的,不管外部怎么变,anchorPoint 都不会跟着变化。

修改 anchorPoint 所带来的困惑

对于修改 position 来讲其实就是修改它的 "center" ,这里很容易理解。可是对于修改 anchorPoint ,相信不少人都有过一样的困惑,为何修改了 anchorPoint 所带来的变化每每和本身想象中的不太同样呢?来看一个修改锚点 x 的例子 ( 0.5 → 0.2 ):

仔细观察一下 "图2" 就会发现,无论是新锚点仍是旧锚点,它们在自身图层中的位置中都没有变化。既然锚点自己不会变化,那变化的就只能是 x 了。x 是如何变化的?从图片中能够很清楚地看到,是把新锚点移动到旧锚点的所在位置。这也是大部分人的误区,觉得修改 0.5 -> 0.2 就是把旧的锚点移动到新锚点的所在位置,结果偏偏相反,这就是形成修改 anchorPoint 每每和本身想象中不太同样的缘由。

还有一种比较好理解的方式就是,想象一下,假设 "图1" 中的底部红色图层是一张纸,而中间的白点至关于一枚大头钉固定在它中间,移动的时候,你就按住中间的大头钉让其保持不动。这时候假设你要开始移动到任意点了,那你会怎么作呢?惟一的一种方式就是,移动整个图层,让新的锚点顺着旧锚点中的位置靠拢,最终彻底重合,就算移动完成了

参考

完全理解position与anchorPoint

核心动画编程指南

iOS 核心动画:高级技巧

苹果 UIView 文档

苹果 CALayer 文档