Core Animation
位于 AppKit
和 UIKit
之下,并緊密集成到 Cocoa
和 Cocoa Touch
的視圖工作流程中.
Core Animation
前身叫作 Layer Kit
, 它是一個(gè)復(fù)合引擎, 通過組合屏幕上不同的可視內(nèi)容來顯示. 這些可視內(nèi)容被分解成獨(dú)立的圖層薄嫡,存儲在圖層樹之中.
通過上面這兩句話的描述, 有幾個(gè)點(diǎn)需要注意.
- 在
iOS App
中, 用戶直接接觸到的是UIKit
中的UIView
, 這個(gè)View
和Layer
有什么關(guān)系? - 這個(gè)圖層樹是什么? layer 和 View 類似, 依靠層級關(guān)系進(jìn)行管理, 父圖層包含子圖層.
View 和 Layer 的關(guān)系
首先來看一個(gè)問題, 一張圖片是怎么在 App
界面上顯示的呢?
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
imageView.image = UIImage(named: "test")
過程如下:
- 根據(jù)
test
名字找到對應(yīng)的圖片. - 通過
UIImage(named: "test")
將圖片載入內(nèi)存.- 通過此方式會對
test
這張圖片進(jìn)行內(nèi)存緩存, 當(dāng)下次再調(diào)用這張圖片顯示會直接從內(nèi)存緩存中查找數(shù)據(jù). - 將圖片載入內(nèi)存, 實(shí)際上是將壓縮的圖片數(shù)據(jù)解碼成未壓縮的位圖形式, 即二進(jìn)制數(shù)據(jù)轉(zhuǎn)換成像素?cái)?shù)據(jù)的過程.
- 通過此方式會對
- 當(dāng) App 更新視圖層級 (view hierarchy) 的時(shí)候, UIKit 會結(jié)合 UIWindow 和 Subviews, 將像素?cái)?shù)據(jù)進(jìn)行渲染輸出. 最終呈現(xiàn)在界面上.
App
對數(shù)據(jù)的處理必須載入內(nèi)存, 才能借由CPU, GPU進(jìn)行操作. 上面涉及到三種 Buffer
(Buffer 是一段連續(xù)的內(nèi)存區(qū)域).
- Data Buffers 存儲圖片文件(test.png)的元數(shù)據(jù). 它的大小和圖片存儲在磁盤中文件大小一致.
- Image Buffers 代表了圖片(Image)在內(nèi)存中的表示, 每個(gè)元素代表一個(gè)像素點(diǎn)的顏色, 即我們上文提到的位圖. 它的大小與圖像大小成正比.
- Frame Buffer 存儲了 App 的每幀的實(shí)際渲染輸出.
整了半天, 和 CALayer 有關(guān)系嗎?
UIImageView 是一個(gè) UIView 的子類, 為什么 UIView 無法直接顯示圖片, UIImageView 可以呢? 內(nèi)部到底封裝了什么?
關(guān)鍵在于 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
屬性來調(diào)整內(nèi)容在圖層中的位置. 與 UIView 中的屬性 contentMode
對應(yīng).
kCAGravityCenter
kCAGravityTop
kCAGravityBottom
kCAGravityLeft
kCAGravityRight
kCAGravityTopLeft
kCAGravityTopRight
kCAGravityBottomLeft
kCAGravityBottomRight
kCAGravityResize
kCAGravityResizeAspect
kCAGravityResizeAspectFill
由此我們基本上可以得出結(jié)論:
- View 是對 layer 的封裝, View 通過 layer 來處理實(shí)際的內(nèi)容. 比如像圖片, 文本或者背景色. 子圖層的位置, 利用屬性做動(dòng)畫.
- 除此之外, CALayer 不能處理用戶的交互, 因?yàn)?Layer 不清楚具體的響應(yīng)鏈. iOS 是通過 View 層級關(guān)系用來傳送觸摸事件的機(jī)制.
有一點(diǎn)需要指明
Core Animation
本身不是繪圖系統(tǒng). 它是用于在硬件中合成和操作 App 內(nèi)容的基礎(chǔ)結(jié)構(gòu).
此基礎(chǔ)結(jié)構(gòu)的核心是layer object, 可以使用它來管理和操作內(nèi)容.
layer
將內(nèi)容捕獲到可以由圖形硬件操作的位圖中. 大部分 App 由 UIView 來管理 layer.
- layer 使得 view 內(nèi)容的繪制和動(dòng)畫更加有效草穆,并且動(dòng)畫有較高的幀率咙咽,就是更流暢.
什么時(shí)候應(yīng)該用 CALayer
而不是 UIView
?
- 需要通過 CALayer 以及其子類創(chuàng)建特殊的動(dòng)畫, 而且不想利用 UIView 進(jìn)行封裝.
- 追求極致性能, 比如重寫 UIText 的 layer, 進(jìn)行異步繪制內(nèi)容.
基于 layer 的繪圖模型
layer object 是在3D空間中組織的2D表面, 是使用 Core Animation
執(zhí)行的所有操作的核心.
與視圖一樣, 圖層管理有關(guān)其曲面的幾何, 內(nèi)容和視覺屬性的信息.
與視圖不同, 圖層不會定義自己的外觀. 圖層僅管理位圖周圍的狀態(tài)信息.
位圖本身可以是視圖繪制本身或您指定的固定圖像的結(jié)果.
注: 位圖, bitmap, 就是像素?cái)?shù)據(jù).
- 大多數(shù)圖層不會在您的應(yīng)用中進(jìn)行任何實(shí)際繪圖. 相反, 圖層會捕獲應(yīng)用提供的內(nèi)容, 并將其緩存在位圖中, 有時(shí)也稱為后備存儲.
- 隨后更改圖層的屬性時(shí), 所做的只是更改與圖層對象關(guān)聯(lián)的狀態(tài)信息.
- 當(dāng)更改觸發(fā)動(dòng)畫時(shí)(更改圖層屬性, 會觸發(fā)隱私動(dòng)畫), Core Animation 會將圖層的位圖和狀態(tài)信息傳遞給圖形硬件, 圖形硬件會使用新信息渲染位圖.
-
基于 View 的動(dòng)畫: 使用基于 View 的繪圖時(shí), 對視圖本身的更改通常會導(dǎo)致調(diào)用視圖的
drawRect:
方法以使用新參數(shù)重繪內(nèi)容. 但是以這種方式繪制是很昂貴的俏拱,因?yàn)樗窃谥骶€程上使用CPU完成的. - 基于 Layer 的動(dòng)畫: Core Animation 通過在硬件中操縱緩存的位圖來實(shí)現(xiàn)相同或類似的效果, 盡可能避免這種費(fèi)用.
在硬件中操作位圖會產(chǎn)生比在軟件中更快的動(dòng)畫.
對于基于 layer 的動(dòng)畫, layer object 的數(shù)據(jù)和狀態(tài)信息與屏幕上該圖層內(nèi)容的顯示是分離的. 這意味著 Core Animation
能將從 舊狀態(tài)值 到 新狀態(tài)值 的變化設(shè)置為動(dòng)畫.
在動(dòng)畫過程中, Core Animation
會在硬件中完成所有逐幀繪圖.
layer object 定義自己的幾何圖形
與 View 一樣, layer 具有frame, bound, 可以使用它們來定位圖層及其內(nèi)容.
layer 還具有 View 不具有的其他屬性, 比如 anchor point
定位點(diǎn), 用于定義操作發(fā)生的點(diǎn).
需要特別注意的是, layer 使用兩種類型的坐標(biāo)系統(tǒng): 基于點(diǎn)的坐標(biāo)系和單位坐標(biāo)系.
基于點(diǎn)的坐標(biāo)最常見的用途是指定圖層的 size
和 frame
, 使用圖層的 bounds
和 position
屬性.
Core Animation
使用單位坐標(biāo)來表示在圖層大小更改時(shí)其值可能會更改的屬性. 比如錨點(diǎn).
可以將單位坐標(biāo)視為指定總可能值的百分比. 單位坐標(biāo)空間中的每個(gè)坐標(biāo)的范圍都為0.0到1.0.
下圖演示了如何將錨點(diǎn)從其默認(rèn)值更改為不同的值, 影響圖層的 position 屬性.
原始圖
變換圖
下面是代碼實(shí)現(xiàn)
@IBAction func btnClick(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
// 設(shè)置錨點(diǎn)
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))
}
}
展示界面
我們會發(fā)現(xiàn), redView 在繞著錨點(diǎn)旋轉(zhuǎn), 錨點(diǎn)所在的位置就是原來的 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 的位置, 讓其固定在原位置旋轉(zhuǎn).
layer 在三維中的操作
仿射變換
我們比較熟悉的是 UIView
, 他有一個(gè) transform
屬性, 這是一個(gè) CGAffineTransform
類型. 顧名思義, 仿射變換.
CGAffineTransform(rotationAngle: CGFloat)
CGAffineTransform(scaleX: CGFloat, y: CGFloat)
CGAffineTransform(translationX: CGFloat, y: CGFloat)
通過 UIView 的這個(gè)屬性可以輕松實(shí)現(xiàn)視圖的旋轉(zhuǎn), 縮放, 平移.
CGAffineTransform 本質(zhì)是一個(gè)可以和二維空間向量(例如CGPoint)做乘法的3X2的矩陣
舉例, 下面是一個(gè)平移變換, 輸入的 50, 100, 會讓 redView 水平移動(dòng) 50, 縱向移動(dòng) 100
redView.transform = CGAffineTransform(translationX: 50, y: 100)
內(nèi)部實(shí)現(xiàn)就是
矩陣乘法的一個(gè)必要條件是兩個(gè)矩陣的行列數(shù)[row1, col1]
與 [row2, col2]
, col1
必須等于 row2
才能進(jìn)行乘積. 具體可查看維基百科-矩陣乘法.
圖中的灰色元素是為了讓矩陣既能做乘法, 又不影響最終運(yùn)算結(jié)果添加的.
注意:
- 當(dāng)對圖層應(yīng)用變換矩陣, 圖層矩形內(nèi)的每一個(gè)點(diǎn)都被相應(yīng)地做變換, 從而形成一個(gè)新的四邊形的形狀.
-
CGAffineTransform
中的 仿射 的意思是無論變換矩陣用什么值, 圖層中平行的兩條線在變換之后任然保持平行,CGAffineTransform
可以做出任意符合上述標(biāo)注的變換.
UIView
可以通過設(shè)置 transform
屬性做變換, 但實(shí)際上它只是封裝了內(nèi)部圖層的變換.
CALayer
對應(yīng)于 UIView
的 transform
屬性叫做 affineTransform
.
redView.layer.setAffineTransform(CGAffineTransform(translationX: 0, y: 100))
上面的這些仿射變換, CGAffineTransform
都是 CG
開頭的, 說明它是屬于 Core Graphics
框架的. 這個(gè)框架能做的僅僅是 2D 變換, 要想實(shí)現(xiàn) 3D 變換, 必須借助 layer
的 transform
屬性, 注意這個(gè)屬性屬于 CATransform3D
類型.
CATransform3D
是 CA
開頭, 說明它是屬于 Core Animation
范疇的. 并且它也是一個(gè)矩陣, 但是和3x2的矩陣不同, CATransform3D
是一個(gè)可以在3維空間內(nèi)做變換的4x4的矩陣.
Core Animation
提供了一系列函數(shù)用于處理 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 的平移和縮放多出了一個(gè) z 參數(shù), 并且旋轉(zhuǎn)函數(shù)除了 angle 之外多出了 x, y, z 三個(gè)參數(shù), 分別決定了每個(gè)坐標(biāo)軸方向上的旋轉(zhuǎn).
x 軸, y 軸 我們比較熟悉, z 軸 與 x, y 軸垂直. 上圖顯示了 x勤家,y, z 軸, 以及圍繞它們旋轉(zhuǎn)的方向.
對于上面說的 API, 他們也是通過矩陣數(shù)學(xué)來做計(jì)算的
下面顯示了一些更常見轉(zhuǎn)換的矩陣配置.
注意
- 對于平移變換,
tx
,ty
,tz
分別代表著沿 x軸, y軸, z軸上的分量. - 對于縮放變換,
sx
,sy
,sz
分別代表縮放后每個(gè)軸上的分量. - 對于旋轉(zhuǎn)變換, 通過傳入的角度, 來計(jì)算對應(yīng)的 正弦值, 余弦值.
對于一些具體的應(yīng)用, 比如做一個(gè)骰子, 類似下面這樣. 這里不展開討論, 感興趣的可以看一下這個(gè)
layer 樹反映了動(dòng)畫狀態(tài)的不同方面
使用 Core Animation
的應(yīng)用程序有三組圖層對象. 每組圖層對象在使應(yīng)用內(nèi)容顯示在屏幕上時(shí)具有不同的作用
- 模型層樹(
model layer tree
) 中的對象(或簡稱為“l(fā)ayer tree”)是 App 與之交互的對象. 此樹中的對象是存儲任何動(dòng)畫的目標(biāo)值的模型對象. 每當(dāng)更改圖層的屬性時(shí), 都使用其中一個(gè)對象. - 顯示樹(
presentation tree
)中的對象包含任何正在運(yùn)行的動(dòng)畫中的值. 模型層樹對象包含動(dòng)畫的目標(biāo)值, 而顯示樹中的對象反映屏幕上顯示的當(dāng)前值. 永遠(yuǎn)不應(yīng)該修改此樹中的對象. 相反, 可以使用這些對象來讀取當(dāng)前動(dòng)畫值, 可以從這些值開始創(chuàng)建新動(dòng)畫. - 渲染樹(
render tree
)中的對象執(zhí)行實(shí)際動(dòng)畫抵卫,并且是Core Animation
的私有動(dòng)畫.
每個(gè) View
都有一個(gè)對應(yīng)的 layer
對象, 它構(gòu)成圖層層次結(jié)構(gòu)的一部分.
對于 layer trees
中的每個(gè)對象, 在 presentation trees
和 render trees
中都有一個(gè)匹配的對象. App 主要使用 layer tree 中的對象, 但有時(shí)可能訪問 presentation trees 中的對象.
具體來說,訪問 layer tree 中對象的 presentationLayer
屬性會返回 presentation trees 中的相應(yīng)對象. 可以通過該對象以讀取位于動(dòng)畫中間的屬性的當(dāng)前值.
注意
只有在動(dòng)畫播放時(shí)才應(yīng)訪問 presentation tree
中的對象.
當(dāng)動(dòng)畫正在進(jìn)行時(shí)抒巢,presentation tree
將包含當(dāng)時(shí)在屏幕上顯示的圖層值.
layer tree
始終反映代碼設(shè)置的最后一個(gè)值, 并且等效于動(dòng)畫的最終狀態(tài).
參考
Core Animation Programming Guide
維基百科-矩陣乘法
iOS-Core-Animation-Advanced-Techniques