Core Animation
位于 AppKit
和 UIKit
之下,并紧密集成到 Cocoa
和 Cocoa Touch
的视图工作流程中.
Core Animation
前身叫作 Layer Kit
, 它是一个复合引擎, 通过组合屏幕上不同的可视内容来显示. 这些可视内容被分解成独立的图层,存储在图层树之中.
通过上面这两句话的描述, 有几个点需要注意.
- 在
iOS App
中, 用户直接接触到的是UIKit
中的UIView
, 这个View
和Layer
有什么关系? - 这个图层树是什么? layer 和 View 类似, 依靠层级关系进行管理, 父图层包含子图层.
View 和 Layer 的关系
首先来看一个问题, 一张图片是怎么在 App
界面上显示的呢?
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
imageView.image = UIImage(named: "test")
过程如下:
- 根据
test
名字找到对应的图片. - 通过
UIImage(named: "test")
将图片载入内存.- 通过此方式会对
test
这张图片进行内存缓存, 当下次再调用这张图片显示会直接从内存缓存中查找数据. - 将图片载入内存, 实际上是将压缩的图片数据解码成未压缩的位图形式, 即二进制数据转换成像素数据的过程.
- 通过此方式会对
- 当 App 更新视图层级 (view hierarchy) 的时候, UIKit 会结合 UIWindow 和 Subviews, 将像素数据进行渲染输出. 最终呈现在界面上.
App
对数据的处理必须载入内存, 才能借由CPU, GPU进行操作. 上面涉及到三种 Buffer
(Buffer 是一段连续的内存区域).
- Data Buffers 存储图片文件(test.png)的元数据. 它的大小和图片存储在磁盘中文件大小一致.
- Image Buffers 代表了图片(Image)在内存中的表示, 每个元素代表一个像素点的颜色, 即我们上文提到的位图. 它的大小与图像大小成正比.
- Frame Buffer 存储了 App 的每帧的实际渲染输出.
整了半天, 和 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
由此我们基本上可以得出结论:
- View 是对 layer 的封装, View 通过 layer 来处理实际的内容. 比如像图片, 文本或者背景色. 子图层的位置, 利用属性做动画.
- 除此之外, CALayer 不能处理用户的交互, 因为 Layer 不清楚具体的响应链. iOS 是通过 View 层级关系用来传送触摸事件的机制.
有一点需要指明
Core Animation
本身不是绘图系统. 它是用于在硬件中合成和操作 App 内容的基础结构.
此基础结构的核心是layer object, 可以使用它来管理和操作内容.
layer
将内容捕获到可以由图形硬件操作的位图中. 大部分 App 由 UIView 来管理 layer.
- layer 使得 view 内容的绘制和动画更加有效,并且动画有较高的帧率,就是更流畅.
什么时候应该用 CALayer
而不是 UIView
?
- 需要通过 CALayer 以及其子类创建特殊的动画, 而且不想利用 UIView 进行封装.
- 追求极致性能, 比如重写 UIText 的 layer, 进行异步绘制内容.
基于 layer 的绘图模型
layer object 是在3D空间中组织的2D表面, 是使用 Core Animation
执行的所有操作的核心.
与视图一样, 图层管理有关其曲面的几何, 内容和视觉属性的信息.
与视图不同, 图层不会定义自己的外观. 图层仅管理位图周围的状态信息.
位图本身可以是视图绘制本身或您指定的固定图像的结果.
注: 位图, bitmap, 就是像素数据.
-
- 大多数图层不会在您的应用中进行任何实际绘图. 相反, 图层会捕获应用提供的内容, 并将其缓存在位图中, 有时也称为后备存储.
-
- 随后更改图层的属性时, 所做的只是更改与图层对象关联的状态信息.
-
- 当更改触发动画时(更改图层属性, 会触发隐私动画), Core Animation 会将图层的位图和状态信息传递给图形硬件, 图形硬件会使用新信息渲染位图.
- 基于 View 的动画: 使用基于 View 的绘图时, 对视图本身的更改通常会导致调用视图的
drawRect:
方法以使用新参数重绘内容. 但是以这种方式绘制是很昂贵的,因为它是在主线程上使用CPU完成的. - 基于 Layer 的动画: Core Animation 通过在硬件中操纵缓存的位图来实现相同或类似的效果, 尽可能避免这种费用.
在硬件中操作位图会产生比在软件中更快的动画.
对于基于 layer 的动画, layer object 的数据和状态信息与屏幕上该图层内容的显示是分离的. 这意味着 Core Animation
能将从 旧状态值 到 新状态值 的变化设置为动画.
在动画过程中, Core Animation
会在硬件中完成所有逐帧绘图.
layer object 定义自己的几何图形
与 View 一样, layer 具有frame, bound, 可以使用它们来定位图层及其内容.
layer 还具有 View 不具有的其他属性, 比如 anchor point
定位点, 用于定义操作发生的点.
需要特别注意的是, layer 使用两种类型的坐标系统: 基于点的坐标系和单位坐标系.
基于点的坐标最常见的用途是指定图层的 size
和 frame
, 使用图层的 bounds
和 position
属性.
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))
}
}
展示界面
通过修改 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
才能进行乘积. 具体可查看维基百科-矩阵乘法.
图中的灰色元素是为了让矩阵既能做乘法, 又不影响最终运算结果添加的.
注意:
- 当对图层应用变换矩阵, 图层矩形内的每一个点都被相应地做变换, 从而形成一个新的四边形的形状.
-
CGAffineTransform
中的 仿射 的意思是无论变换矩阵用什么值, 图层中平行的两条线在变换之后任然保持平行,CGAffineTransform
可以做出任意符合上述标注的变换.
UIView
可以通过设置 transform
属性做变换, 但实际上它只是封装了内部图层的变换.
CALayer
对应于 UIView
的 transform
属性叫做 affineTransform
.
redView.layer.setAffineTransform(CGAffineTransform(translationX: 0, y: 100))
上面的这些仿射变换, CGAffineTransform
都是 CG
开头的, 说明它是属于 Core Graphics
框架的. 这个框架能做的仅仅是 2D 变换, 要想实现 3D 变换, 必须借助 layer
的 transform
属性, 注意这个属性属于 CATransform3D
类型.
CATransform3D
是 CA
开头, 说明它是属于 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, 他们也是通过矩阵数学来做计算的
下面显示了一些更常见转换的矩阵配置.
注意
- 对于平移变换,
tx
,ty
,tz
分别代表着沿 x轴, y轴, z轴上的分量. - 对于缩放变换,
sx
,sy
,sz
分别代表缩放后每个轴上的分量. - 对于旋转变换, 通过传入的角度, 来计算对应的 正弦值, 余弦值.
对于一些具体的应用, 比如做一个骰子, 类似下面这样. 这里不展开讨论, 感兴趣的可以看一下这个
layer 树反映了动画状态的不同方面
使用 Core Animation
的应用程序有三组图层对象. 每组图层对象在使应用内容显示在屏幕上时具有不同的作用
- 模型层树(
model layer tree
) 中的对象(或简称为“layer tree”)是 App 与之交互的对象. 此树中的对象是存储任何动画的目标值的模型对象. 每当更改图层的属性时, 都使用其中一个对象. - 显示树(
presentation tree
)中的对象包含任何正在运行的动画中的值. 模型层树对象包含动画的目标值, 而显示树中的对象反映屏幕上显示的当前值. 永远不应该修改此树中的对象. 相反, 可以使用这些对象来读取当前动画值, 可以从这些值开始创建新动画. - 渲染树(
render tree
)中的对象执行实际动画,并且是Core Animation
的私有动画.
每个 View
都有一个对应的 layer
对象, 它构成图层层次结构的一部分.
对于 layer trees
中的每个对象, 在 presentation trees
和 render trees
中都有一个匹配的对象. App 主要使用 layer tree 中的对象, 但有时可能访问 presentation trees 中的对象.
具体来说,访问 layer tree 中对象的 presentationLayer
属性会返回 presentation trees 中的相应对象. 可以通过该对象以读取位于动画中间的属性的当前值.
注意
只有在动画播放时才应访问 presentation tree
中的对象.
当动画正在进行时,presentation tree
将包含当时在屏幕上显示的图层值.
layer tree
始终反映代码设置的最后一个值, 并且等效于动画的最终状态.
参考
Core Animation Programming Guide
维基百科-矩阵乘法
iOS-Core-Animation-Advanced-Techniques