核心动画系列(一): Core Animation 基础

Core Animation 位于 AppKitUIKit 之下,并紧密集成到 CocoaCocoa Touch 的视图工作流程中.

核心动画系列(一): Core Animation 基础_第1张图片

Core Animation 前身叫作 Layer Kit, 它是一个复合引擎, 通过组合屏幕上不同的可视内容来显示. 这些可视内容被分解成独立的图层,存储在图层树之中.

通过上面这两句话的描述, 有几个点需要注意.

  • iOS App 中, 用户直接接触到的是 UIKit 中的 UIView, 这个 ViewLayer 有什么关系?
  • 这个图层树是什么? layer 和 View 类似, 依靠层级关系进行管理, 父图层包含子图层.

View 和 Layer 的关系

首先来看一个问题, 一张图片是怎么在 App 界面上显示的呢?

let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
imageView.image = UIImage(named: "test")

过程如下:

  1. 根据 test 名字找到对应的图片.
  2. 通过 UIImage(named: "test") 将图片载入内存.
    • 通过此方式会对 test 这张图片进行内存缓存, 当下次再调用这张图片显示会直接从内存缓存中查找数据.
    • 将图片载入内存, 实际上是将压缩的图片数据解码成未压缩的位图形式, 即二进制数据转换成像素数据的过程.
  3. 当 App 更新视图层级 (view hierarchy) 的时候, UIKit 会结合 UIWindow 和 Subviews, 将像素数据进行渲染输出. 最终呈现在界面上.

App 对数据的处理必须载入内存, 才能借由CPU, GPU进行操作. 上面涉及到三种 Buffer (Buffer 是一段连续的内存区域).

  • Data Buffers 存储图片文件(test.png)的元数据. 它的大小和图片存储在磁盘中文件大小一致.
  • Image Buffers 代表了图片(Image)在内存中的表示, 每个元素代表一个像素点的颜色, 即我们上文提到的位图. 它的大小与图像大小成正比.
  • Frame Buffer 存储了 App 的每帧的实际渲染输出.
核心动画系列(一): Core Animation 基础_第2张图片

整了半天, 和 CALayer 有关系吗?

UIImageView 是一个 UIView 的子类, 为什么 UIView 无法直接显示图片, UIImageView 可以呢? 内部到底封装了什么?

关键在于 layer 的 contents 属性. 下面这部分代码能直接显示图片

let layer = CALayer()
layer.bounds = CGRect(x: 0, y: 0, width: 100, height: 100)
layer.backgroundColor = UIColor.red.cgColor
layer.contents = UIImage(named: "1")?.cgImage
view.layer.addSublayer(layer)
核心动画系列(一): Core Animation 基础_第3张图片

而且, 我们可以通过 layer 的 contentsGravity 属性来调整内容在图层中的位置. 与 UIView 中的属性 contentMode 对应.

kCAGravityCenter
kCAGravityTop
kCAGravityBottom
kCAGravityLeft
kCAGravityRight
kCAGravityTopLeft
kCAGravityTopRight
kCAGravityBottomLeft
kCAGravityBottomRight
kCAGravityResize
kCAGravityResizeAspect
kCAGravityResizeAspectFill

由此我们基本上可以得出结论:

  • View 是对 layer 的封装, View 通过 layer 来处理实际的内容. 比如像图片, 文本或者背景色. 子图层的位置, 利用属性做动画.
  • 除此之外, CALayer 不能处理用户的交互, 因为 Layer 不清楚具体的响应链. iOS 是通过 View 层级关系用来传送触摸事件的机制.

有一点需要指明
Core Animation 本身不是绘图系统. 它是用于在硬件中合成和操作 App 内容的基础结构.
此基础结构的核心是layer object, 可以使用它来管理和操作内容.
layer 将内容捕获到可以由图形硬件操作的位图中. 大部分 App 由 UIView 来管理 layer.

  • layer 使得 view 内容的绘制和动画更加有效,并且动画有较高的帧率,就是更流畅.
什么时候应该用 CALayer 而不是 UIView ?
  1. 需要通过 CALayer 以及其子类创建特殊的动画, 而且不想利用 UIView 进行封装.
  2. 追求极致性能, 比如重写 UIText 的 layer, 进行异步绘制内容.

基于 layer 的绘图模型

layer object 是在3D空间中组织的2D表面, 是使用 Core Animation 执行的所有操作的核心.
与视图一样, 图层管理有关其曲面的几何, 内容和视觉属性的信息.

与视图不同, 图层不会定义自己的外观. 图层仅管理位图周围的状态信息.
位图本身可以是视图绘制本身或您指定的固定图像的结果.
注: 位图, bitmap, 就是像素数据.

核心动画系列(一): Core Animation 基础_第4张图片
    1. 大多数图层不会在您的应用中进行任何实际绘图. 相反, 图层会捕获应用提供的内容, 并将其缓存在位图中, 有时也称为后备存储.
    1. 随后更改图层的属性时, 所做的只是更改与图层对象关联的状态信息.
    1. 当更改触发动画时(更改图层属性, 会触发隐私动画), Core Animation 会将图层的位图和状态信息传递给图形硬件, 图形硬件会使用新信息渲染位图.

  • 基于 View 的动画: 使用基于 View 的绘图时, 对视图本身的更改通常会导致调用视图的 drawRect: 方法以使用新参数重绘内容. 但是以这种方式绘制是很昂贵的,因为它是在主线程上使用CPU完成的.
  • 基于 Layer 的动画: Core Animation 通过在硬件中操纵缓存的位图来实现相同或类似的效果, 尽可能避免这种费用.

在硬件中操作位图会产生比在软件中更快的动画.

对于基于 layer 的动画, layer object 的数据和状态信息与屏幕上该图层内容的显示是分离的. 这意味着 Core Animation 能将从 旧状态值新状态值 的变化设置为动画.

在动画过程中, Core Animation 会在硬件中完成所有逐帧绘图.

核心动画系列(一): Core Animation 基础_第5张图片

layer object 定义自己的几何图形

与 View 一样, layer 具有frame, bound, 可以使用它们来定位图层及其内容.
layer 还具有 View 不具有的其他属性, 比如 anchor point 定位点, 用于定义操作发生的点.

需要特别注意的是, layer 使用两种类型的坐标系统: 基于点的坐标系单位坐标系.

基于点的坐标最常见的用途是指定图层的 sizeframe, 使用图层的 boundsposition 属性.

核心动画系列(一): Core Animation 基础_第6张图片

Core Animation 使用单位坐标来表示在图层大小更改时其值可能会更改的属性. 比如锚点.
可以将单位坐标视为指定总可能值的百分比. 单位坐标空间中的每个坐标的范围都为0.0到1.0.

核心动画系列(一): Core Animation 基础_第7张图片

下图演示了如何将锚点从其默认值更改为不同的值, 影响图层的 position 属性.

原始图


核心动画系列(一): Core Animation 基础_第8张图片

变换图


核心动画系列(一): Core Animation 基础_第9张图片

下面是代码实现

@IBAction func btnClick(_ sender: UIButton) {
        sender.isSelected = !sender.isSelected
        
        // 设置锚点        
        redView.layer.anchorPoint = CGPoint(x: 0, y: 0)

        let value: Double = sender.isSelected ? 1 : 0
        
        UIView.animate(withDuration: 0.35) {
            self.redView.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi * 0.15 * value))
        }
        
    }

展示界面


核心动画系列(一): Core Animation 基础_第10张图片

我们会发现, redView 在绕着锚点旋转, 锚点所在的位置就是原来的 position 的位置.

代码稍作修改.

@IBAction func btnClick(_ sender: UIButton) {
        sender.isSelected = !sender.isSelected
        
        let oldOrigin = redView.frame.origin
        redView.layer.anchorPoint = CGPoint(x: 0, y: 0)
        let newOrigin = redView.frame.origin
        
        let transition = CGPoint(x: newOrigin.x - oldOrigin.x,
                                 y: newOrigin.y - oldOrigin.y)
        redView.center = CGPoint(x: redView.center.x - transition.x, y: redView.center.y - transition.y)
        
        let value: Double = sender.isSelected ? 1 : 0
        
        UIView.animate(withDuration: 0.35) {
            self.redView.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi * 0.15 * value))
        }
        
    }

展示界面


核心动画系列(一): Core Animation 基础_第11张图片
2019-01-15 05.18.21.gif

通过修改 redView 的位置, 让其固定在原位置旋转.

layer 在三维中的操作

仿射变换

我们比较熟悉的是 UIView , 他有一个 transform 属性, 这是一个 CGAffineTransform 类型. 顾名思义, 仿射变换.

CGAffineTransform(rotationAngle: CGFloat)
CGAffineTransform(scaleX: CGFloat, y: CGFloat)
CGAffineTransform(translationX: CGFloat, y: CGFloat)

通过 UIView 的这个属性可以轻松实现视图的旋转, 缩放, 平移.
CGAffineTransform 本质是一个可以和二维空间向量(例如CGPoint)做乘法的3X2的矩阵

举例, 下面是一个平移变换, 输入的 50, 100, 会让 redView 水平移动 50, 纵向移动 100

redView.transform = CGAffineTransform(translationX: 50, y: 100)

内部实现就是


核心动画系列(一): Core Animation 基础_第12张图片

矩阵乘法的一个必要条件是两个矩阵的行列数[row1, col1][row2, col2], col1 必须等于 row2 才能进行乘积. 具体可查看维基百科-矩阵乘法.

图中的灰色元素是为了让矩阵既能做乘法, 又不影响最终运算结果添加的.

注意:

  • 当对图层应用变换矩阵, 图层矩形内的每一个点都被相应地做变换, 从而形成一个新的四边形的形状.
  • CGAffineTransform 中的 仿射 的意思是无论变换矩阵用什么值, 图层中平行的两条线在变换之后任然保持平行, CGAffineTransform 可以做出任意符合上述标注的变换.
    核心动画系列(一): Core Animation 基础_第13张图片
    仿射变换与非仿射变换

UIView 可以通过设置 transform 属性做变换, 但实际上它只是封装了内部图层的变换.
CALayer 对应于 UIViewtransform 属性叫做 affineTransform.

redView.layer.setAffineTransform(CGAffineTransform(translationX: 0, y: 100))

上面的这些仿射变换, CGAffineTransform 都是 CG 开头的, 说明它是属于 Core Graphics 框架的. 这个框架能做的仅仅是 2D 变换, 要想实现 3D 变换, 必须借助 layertransform 属性, 注意这个属性属于 CATransform3D 类型.

CATransform3DCA 开头, 说明它是属于 Core Animation 范畴的. 并且它也是一个矩阵, 但是和3x2的矩阵不同, CATransform3D 是一个可以在3维空间内做变换的4x4的矩阵.

Core Animation 提供了一系列函数用于处理 3D 变换.

CATransform3DMakeTranslation(tx CGFloat, ty CGFloat, tz CGFloat)
CATransform3DMakeScale(sx CGFloat, sy CGFloat, sz CGFloat)
CATransform3DMakeRotation(angle CGFloat, x CGFloat, y CGFloat, z CGFloat)

注意
3D 的平移和缩放多出了一个 z 参数, 并且旋转函数除了 angle 之外多出了 x, y, z 三个参数, 分别决定了每个坐标轴方向上的旋转.

核心动画系列(一): Core Animation 基础_第14张图片

x 轴, y 轴 我们比较熟悉, z 轴 与 x, y 轴垂直. 上图显示了 x,y, z 轴, 以及围绕它们旋转的方向.

对于上面说的 API, 他们也是通过矩阵数学来做计算的


核心动画系列(一): Core Animation 基础_第15张图片
image08.png

下面显示了一些更常见转换的矩阵配置.

核心动画系列(一): Core Animation 基础_第16张图片

注意

  • 对于平移变换, tx, ty, tz 分别代表着沿 x轴, y轴, z轴上的分量.
  • 对于缩放变换, sx, sy, sz 分别代表缩放后每个轴上的分量.
  • 对于旋转变换, 通过传入的角度, 来计算对应的 正弦值, 余弦值.

对于一些具体的应用, 比如做一个骰子, 类似下面这样. 这里不展开讨论, 感兴趣的可以看一下这个

核心动画系列(一): Core Animation 基础_第17张图片
image.png

layer 树反映了动画状态的不同方面

使用 Core Animation 的应用程序有三组图层对象. 每组图层对象在使应用内容显示在屏幕上时具有不同的作用

核心动画系列(一): Core Animation 基础_第18张图片
  • 模型层树( model layer tree ) 中的对象(或简称为“layer tree”)是 App 与之交互的对象. 此树中的对象是存储任何动画的目标值的模型对象. 每当更改图层的属性时, 都使用其中一个对象.
  • 显示树( presentation tree )中的对象包含任何正在运行的动画中的值. 模型层树对象包含动画的目标值, 而显示树中的对象反映屏幕上显示的当前值. 永远不应该修改此树中的对象. 相反, 可以使用这些对象来读取当前动画值, 可以从这些值开始创建新动画.
  • 渲染树( render tree )中的对象执行实际动画,并且是 Core Animation 的私有动画.

每个 View 都有一个对应的 layer 对象, 它构成图层层次结构的一部分.

核心动画系列(一): Core Animation 基础_第19张图片

对于 layer trees 中的每个对象, 在 presentation treesrender trees 中都有一个匹配的对象. App 主要使用 layer tree 中的对象, 但有时可能访问 presentation trees 中的对象.

具体来说,访问 layer tree 中对象的 presentationLayer 属性会返回 presentation trees 中的相应对象. 可以通过该对象以读取位于动画中间的属性的当前值.

核心动画系列(一): Core Animation 基础_第20张图片
image11.png

注意
只有在动画播放时才应访问 presentation tree 中的对象.
当动画正在进行时,presentation tree 将包含当时在屏幕上显示的图层值.
layer tree 始终反映代码设置的最后一个值, 并且等效于动画的最终状态.

参考

Core Animation Programming Guide
维基百科-矩阵乘法
iOS-Core-Animation-Advanced-Techniques

你可能感兴趣的:(核心动画系列(一): Core Animation 基础)