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

2019-01-15  本文已影响13人  Lin__Chuan

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

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

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

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 是一段连续的内存区域).


整了半天, 和 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)

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

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

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

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

什么时候应该用 CALayer 而不是 UIView ?
  1. 需要通过 CALayer 以及其子类创建特殊的动画, 而且不想利用 UIView 进行封装.
  2. 追求极致性能, 比如重写 UIText 的 layer, 进行异步绘制内容.

基于 layer 的绘图模型

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

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


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

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

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

layer object 定义自己的几何图形

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

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

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

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

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

原始图


变换图


下面是代码实现

@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))
        }
        
    }

展示界面


我们会发现, 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))
        }
        
    }

展示界面


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)

内部实现就是


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

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

注意:

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 三个参数, 分别决定了每个坐标轴方向上的旋转.

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

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


image08.png

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


注意

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

image.png

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

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

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

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

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

image11.png

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

参考

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

上一篇 下一篇

猜你喜欢

热点阅读