圖層幾何學與幾何變換
CALayer基礎介紹完成后,我們已經能過實現很多的基本的視覺效果了,但是這些效果都還是靜態的遠遠沒有動畫交互帶來的那種體驗。動畫效果的實現的基本原理就是:對平移、縮放、旋轉等幾何變化進行組合然后設定一個動畫持續時間,然后系統就會幫我們實現這些動畫幀。本文將會介紹哪些iOS中動畫涉及到的幾何學概念和原理。
iOS圖形幾何學
幾何學的基礎應用就是要在對應的坐標系統里面對事物進行布局操作,而這些布局位置也是所有動畫實現的基石。iOS中涉及布局的屬性有UIView中的frame、bounds、center以及對應的CALayer中的frame、bounds、
position。
對于UIView:
-
frame:子視圖最小外接矩形相對于父視圖最小外接矩形的位置和大小
-
bounds:代表自身的坐標系統,常用于判斷系統
-
center:與CALayer中的 position 屬性等值。
對于CALayer:
-
frame:子圖層最小外接矩形相對于父圖層最小外接矩形的位置和大小
-
bounds:代表自身的坐標系統,常用于判斷系統
-
position:子圖層錨點 anchorPoint 相對于父圖層的位置
對于視圖和圖層來說 frame 是一個虛擬屬性,這個最小外接矩形其實是根據 center 、 position 、 bounds 、 transform 等屬性計算得到的。也就是說改變其中任何一個屬性值都會相應地導致 frame 屬性的變化,只不過平時使用的時候視圖和圖層都是沒有做旋轉操作無法察覺 frame 與 bounds 的區別。
錨點 anchorPoint
從一個例子開始入手吧,想象一下,把一張A4白紙用圖釘訂在書桌上,如果訂得不是很緊的話,白紙就可以沿順時針或逆時針方向圍繞圖釘旋轉,這時候圖釘就起著支點的作用。我們要解釋的 anchorPoint 就相當于白紙上的圖釘,它主要的作用就是用來作為變換的支點。很明顯旋轉支點位置不同得到的旋轉效果差別是很大的。 anchorPoint 的取值是相對與bounds的比列來計算的,左上角為(0,0)又下角為 (1,1),默認 anchorPoint 為(0.5,0.5)。
下面是官方iOS左手系和macOS右手系中的概念和旋轉情形的圖解:
anchorPoint 的數值發生改變的時候,實際上移動的不是 anchorPoint 而是 bounds 。 bounds 會根據 anchorPoint 計算偏移量,然后進行反向偏移。上面說過 frame 是 bounds 最小外接矩形,那么這意味著 frame 會相應地移動。
在上圖你可以發現移動前后 position 的數值沒有發生改變,而且與 anchorPoint 相對與父視圖的位置是一致的。同時你還可以發現如下的等式關系:
position.x = frame.origin.x + anchorPoint.x * bounds.size.width;
position.y = frame.origin.y + anchorPoint.y * bounds.size.height;
但是該公式并不是正確的,它適用于與上面這種 frame 與 bounds 重合的特殊情況。現實情況遠比這個復雜尤其當圖形發生過旋轉后。上面等式中的: anchorPoint.x bounds.size.width 和 anchorPoint.y bounds.size.height 在旋轉圖形中并不代表的?x和?y,還需要與變換矩陣 transform 進行計算。這超過了本文的內容,感興趣的可以自己回憶一下線性代數和計算機圖形學。我們唯一需要知道的就是 position 屬性其實是根據計算得到的,它代表了 anchorPoint 在suplayer中的相對位置。
幾何變換
無論是電影、游戲以及其他給你帶來強烈視覺沖擊的那些動畫效果也包括軟件應用中的那些交互動畫,其實都是一系列變化過程的靜態圖片在添加Timeline后以你肉眼無法察覺的頻率更換圖片來達到的。而這些時間線上圖片的狀態變化無非就是平移、旋轉、縮放以及它們組合起來的幾何變換。下面我們開始來聊聊這些幾何變換。
仿射變換
仿射變換是指在二維空間坐標系統中對圖像進行平移、旋轉、縮放等幾何變換。在iOS系統中UIView的 transform 和CALayer的 affineTransform 屬性就是用來實現這些變換的,這兩個屬性都對應同一個類型: CGAffineTransform。該類型其實就是我們在線性代數中常用的矩陣,它的結構如下:
下面我們再來看下線性代數中二維空間的方式變化公式以及最終得到的計算結果:左側為變化后的坐標,右側為原始坐標以及變換矩陣:
上面的等式中我們能夠發現CGAffineTransform中的 a 、 d 兩個屬性對應的是縮放、 tx 、 ty 對應的是平移、 b 、 c 對應的是旋轉。所以我們可以知道CGAffineTransform中:
-
rotated(by: CGFloat)函數設置的是 c 、 d 屬性的值,這個值對應的是弧度切逆時針為正。
-
scaledBy(x: CGFloat, y: CGFloat)函數設置的值分別為 a 、 d 屬性的值。
-
translatedBy(x: CGFloat, y: CGFloat)函數設置的值分別為 tx 、 ty 屬性的值。
你可以對上面三個基本變換進行組合來實現自定義的變換,也就是說復雜的仿射變換可以通過拆封然后進行組合通過矩陣計算得到最終的變換 CGAffineTransform 各個屬性的值。
3D變換
除了上面常用的二維仿射變換,CALayer還可以實現更復雜的3D動畫。在變換的過程中屏幕到人眼將作為三維空間中的Z軸,對應的屬性變量為 zPosition 。與仿射變換一致3D變換的實現也是基于線性代數的計算,只不過矩陣的維數比之前更多而已,對應的屬性是CATransform3D類型的tramsform,下圖是官方的矩陣變換計算公式以及常用的變換矩陣:
注意:矩陣變換計算公式在數學表達上其實是錯的,應該是1x4矩陣乘以4x4矩陣,但是這不影響你對文章本身的理解。
具體每一個屬性值對應的作用你可以參照上一部分的講解,同時對照常用變換矩陣一切就很明了。
透視投影
我們先看一下將突破繞Y軸旋轉45o的代碼以及效果圖:
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.orange
viewForLayer = UIView.init(frame: CGRect.init(x: 50, y: 50, width: self.view.bounds.width - 100, height: self.view.bounds.height/2))
viewForLayer.backgroundColor = UIColor.white
viewForLayer.layer.contents = UIImage.init(named: "YiXiu")?.cgImage
viewForLayer.layer.contentsGravity = kCAGravityResizeAspect
let transform :CATransform3D = CATransform3DMakeRotation(CGFloat(M_PI_4), 0, 1, 0)
viewForLayer.layer.transform = transform
self.view.addSubview(viewForLayer)
}
是不是很奇怪?明明設置了旋轉效果,但是圖片看起來僅僅是水平方向上進行了一些壓縮而已。其實代碼和效果都沒有錯,原因就處在了視角上面。我們使用的是一個等距的視角,而不是現實世界中我們眼球所處的透視視角。
在現實世界中因為視角的原因會讓我們產生一種視覺誤差,那就是遠處的物體看起來會比近處的物體小。而實際上遠處的物體可能比眼前的更大,上面的效果就是因為我們是一種等距視角所以顯示的縮放比例是一致的也就不會產生眼球那種透視所帶來的“假象”視覺效果。當然習慣的力量是強大的,雖然iOS沒有提供實現透視效果的變換函數,我們還是可以通過設置屬性值來實現對眼球的欺騙。這個屬性就是CATransform3D矩陣中的 m34 ,它主要就是用來設置:
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.orange
viewForLayer = UIView.init(frame: CGRect.init(x: 50, y: 50, width: self.view.bounds.width - 100, height: self.view.bounds.height/2))
viewForLayer.backgroundColor = UIColor.white
viewForLayer.layer.contents = UIImage.init(named: "YiXiu")?.cgImage
viewForLayer.layer.contentsGravity = kCAGravityResizeAspect
var transform: CATransform3D = CATransform3DIdentity
transform.m34 = -1.0 / 500
transform = CATransform3DRotate(transform,CGFloat(M_PI_4), 0, 1, 0)
viewForLayer.layer.transform = transform
self.view.addSubview(viewForLayer)
}
滅點與sublayerTransform
當在透視角度繪圖的時候,遠離相機視角的物體將會變小變遠,當遠離到一個極限距離,它們可能就縮成了一個點,于是所有的物體最后都匯聚消失在同一個點。這個點就是圖形學中的滅點,通常情況下位于視圖的中間。在CALayer中這個滅點與 anchorPoint 是重合的,這意味著我們在設置多個sublayer的時候可能因為位置的不同導致滅點的位置也不同,這直接就回導致3D顯示效果會非常的差。所以對于這種多sublayer的情況,我們可以先將這些sublayer統一放在父圖層的中間,然后通過變換矩陣進行平移。這樣我們就能保證滅點位置的一致從而實現完美的顯示效果。
在多sublayer情況下還有一個棘手的問題就是:如果我們要對圖層作變換那么是不是意味著我們都要去對每個sublayer的 m34 進行設置來實現透視效果呢?這種情況下,我們可以通過設置父圖層的 sublayerTransform 來讓所有的sublayer進行自動集成來實現全部sublayer的同步變換。
總結
前后分篇文章概要的講解了Core Animation架構、CALayer的基礎、以及圖層幾何學,雖然不是很詳盡但是看完后應該對Core Animation有了一些基本的認識。在這些基礎上,后面我可能還會詳細的帶來一些特殊圖層的分析和應用當然還有常見動畫的實現。
來自:https://segmentfault.com/a/1190000007708351