核心動(dòng)畫系列(一): Core Animation 基礎(chǔ)

Core Animation 位于 AppKitUIKit 之下,并緊密集成到 CocoaCocoa Touch 的視圖工作流程中.

Core Animation 前身叫作 Layer Kit, 它是一個(gè)復(fù)合引擎, 通過組合屏幕上不同的可視內(nèi)容來顯示. 這些可視內(nèi)容被分解成獨(dú)立的圖層薄嫡,存儲在圖層樹之中.

通過上面這兩句話的描述, 有幾個(gè)點(diǎn)需要注意.

  • iOS App 中, 用戶直接接觸到的是 UIKit 中的 UIView, 這個(gè) ViewLayer 有什么關(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")

過程如下:

  1. 根據(jù) test 名字找到對應(yīng)的圖片.
  2. 通過 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ù)的過程.
  3. 當(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 ?
  1. 需要通過 CALayer 以及其子類創(chuàng)建特殊的動(dòng)畫, 而且不想利用 UIView 進(jìn)行封裝.
  2. 追求極致性能, 比如重寫 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ù).

    1. 大多數(shù)圖層不會在您的應(yīng)用中進(jìn)行任何實(shí)際繪圖. 相反, 圖層會捕獲應(yīng)用提供的內(nèi)容, 并將其緩存在位圖中, 有時(shí)也稱為后備存儲.
    1. 隨后更改圖層的屬性時(shí), 所做的只是更改與圖層對象關(guān)聯(lián)的狀態(tài)信息.
    1. 當(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)最常見的用途是指定圖層的 sizeframe, 使用圖層的 boundsposition 屬性.

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

展示界面


2019-01-15 05.18.21.gif

通過修改 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)于 UIViewtransform 屬性叫做 affineTransform.

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

上面的這些仿射變換, CGAffineTransform 都是 CG 開頭的, 說明它是屬于 Core Graphics 框架的. 這個(gè)框架能做的僅僅是 2D 變換, 要想實(shí)現(xiàn) 3D 變換, 必須借助 layertransform 屬性, 注意這個(gè)屬性屬于 CATransform3D 類型.

CATransform3DCA 開頭, 說明它是屬于 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ì)算的


image08.png

下面顯示了一些更常見轉(zhuǎn)換的矩陣配置.


注意

  • 對于平移變換, tx, ty, tz 分別代表著沿 x軸, y軸, z軸上的分量.
  • 對于縮放變換, sx, sy, sz 分別代表縮放后每個(gè)軸上的分量.
  • 對于旋轉(zhuǎn)變換, 通過傳入的角度, 來計(jì)算對應(yīng)的 正弦值, 余弦值.

對于一些具體的應(yīng)用, 比如做一個(gè)骰子, 類似下面這樣. 這里不展開討論, 感興趣的可以看一下這個(gè)

image.png

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 treesrender trees 中都有一個(gè)匹配的對象. App 主要使用 layer tree 中的對象, 但有時(shí)可能訪問 presentation trees 中的對象.

具體來說,訪問 layer tree 中對象的 presentationLayer 屬性會返回 presentation trees 中的相應(yīng)對象. 可以通過該對象以讀取位于動(dòng)畫中間的屬性的當(dāng)前值.

image11.png

注意
只有在動(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贫贝,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蛉谜,更是在濱河造成了極大的恐慌稚晚,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件型诚,死亡現(xiàn)場離奇詭異客燕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)狰贯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進(jìn)店門也搓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人暮现,你說我怎么就攤上這事还绘。” “怎么了栖袋?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長抚太。 經(jīng)常有香客問我塘幅,道長昔案,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任电媳,我火速辦了婚禮踏揣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘匾乓。我一直安慰自己捞稿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布拼缝。 她就那樣靜靜地躺著娱局,像睡著了一般。 火紅的嫁衣襯著肌膚如雪咧七。 梳的紋絲不亂的頭發(fā)上衰齐,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天,我揣著相機(jī)與錄音继阻,去河邊找鬼耻涛。 笑死,一個(gè)胖子當(dāng)著我的面吹牛瘟檩,可吹牛的內(nèi)容都是我干的抹缕。 我是一名探鬼主播,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼墨辛,長吁一口氣:“原來是場噩夢啊……” “哼歉嗓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起背蟆,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤鉴分,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后带膀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體志珍,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年垛叨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了伦糯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,643評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嗽元,死狀恐怖敛纲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情剂癌,我是刑警寧澤淤翔,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站佩谷,受9級特大地震影響旁壮,放射性物質(zhì)發(fā)生泄漏监嗜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一抡谐、第九天 我趴在偏房一處隱蔽的房頂上張望裁奇。 院中可真熱鬧,春花似錦麦撵、人聲如沸刽肠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽音五。三九已至,卻和暖如春杜秸,著一層夾襖步出監(jiān)牢的瞬間放仗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工撬碟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留诞挨,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓呢蛤,卻偏偏與公主長得像惶傻,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子其障,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內(nèi)容