iOS核心動畫高級技巧--(五)變換

在這一章中,我們將要研究可以用來對圖層旋轉,擺放或者扭曲的CGAffineTransform,以及可以將扁平物體轉換成三維空間對象的 CATransform3D (而不是僅僅對圓角矩形添加下沉陰影)。

仿射變換

實際上UIViewtransform屬性是一個CGAffineTransform類型,用于在二維空間做旋轉,縮放和平
移。CGAffineTransform是一個可以和二維空間向量(例如 CGPoint )做乘法 的3X2的矩陣

用矩陣表示的 CGAffineTransform 和 CGPoint.png

CGPoint每一列和 矩陣的每一行對應元素相乘再求和,就形成了一個新的 類型的結果。要解釋一下圖中顯示的灰色元素,為了能讓矩陣做乘法,左邊矩陣的列數一定要和右邊矩陣的行數個數相同,所以要給矩陣填充一些標志值,使得既可以讓矩陣做乘法,又不改變運算結果,并且沒必要存儲這些添加的值,因為它們的值不會發生變化,但是要用來做運算。

當對圖層應用變換矩陣,圖層矩形內的每一個點都被相應地做變換,從而形成一個 新的四邊形的形狀。 CGAffineTransform中的“仿射”的意思是無論變換矩陣用什么值,圖層中平行的兩條線在變換之后任然保持平行, CGAffineTransform 可以做出任意符合上述標注的變換

一些仿射的和非仿射的變換.png
創建一個 CGAffineTransform

對矩陣數學做一個全面的闡述就超出本書的討論范圍了,不過如果你對矩陣完全不熟悉的話,矩陣變換可能會使你感到畏懼。幸運的是,Core Graphics提供了一系列函數,對完全沒有數學基礎的開發者也能夠簡單地做一些變換。如下幾個函數都 創建了一個 CGAffineTransform 實例:

CGAffineTransformMakeRotation(CGFloat angle)
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)

旋轉和縮放變換都可以很好解釋--分別旋轉或者縮放一個向量的值。平移變換是指 每個點都移動了向量指定的x或者y值--所以如果向量代表了一個點,那它就平移了 這個點的距離。

UIView可以通過設置transform屬性做變換,但實際上它只是封裝了內部圖層的變換。

CALayer同樣也有一個transform的屬性,但它的類型是 CATransform3D,而不是CGAffineTransformCALayer對應于UIViewtransform屬性叫做affineTransform。

- (void)viewDidLoad
{
    [super viewDidLoad];
    //rotate the layer 45 degrees
    CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
    self.layerView.layer.affineTransform = transform;
}

注意我們使用的旋轉常量是 M_PI_4 ,而不是你想象的45,因為iOS的變換函數使 用弧度而不是角度作為單位?;《扔脭祵W常量pi的倍數表示,一個pi代表180度,所以四分之一的pi就是45度。

C的數學函數庫(iOS會自動引入)提供了pi的一些簡便的換算, M_PI_4于是就 是pi的四分之一,如果對換算不太清楚的話,可以用如下的宏做換算:

 #define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)
混合變換

Core Graphics提供了一系列的函數可以在一個變換的基礎上做更深層次的變換, 如果做一個既要縮放又要旋轉的變換,這就會非常有用了。例如下面幾個函數:

CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)

當操縱一個變換的時候,初始生成一個什么都不做的變換很重要--也就是創建一 個 CGAffineTransform類型的空值,矩陣論中稱作單位矩陣,Core Graphics同 樣也提供了一個方便的常量:CGAffineTransformIdentity

最后,如果需要混合兩個已經存在的變換矩陣,就可以使用如下方法,在兩個變換
的基礎上創建一個新的變換:

 CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2)

Demo
我們來用這些函數組合一個更加復雜的變換,先縮小50%,再旋轉30度,最后向右移動200個像素


- (void)viewDidLoad
{
    [super viewDidLoad];
    //create a new transform
    CGAffineTransform transform = CGAffineTransformIdentity;
    //scale by 50%
    transform = CGAffineTransformScale(transform, 0.5, 0.5);
    //rotate by 30 degrees
    transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30);
    //translate by 200 points
    transform = CGAffineTransformTranslate(transform, 200, 0);
    //apply transform to layer
    self.layerView.layer.affineTransform = transform;
}
3D變換

CG的前綴告訴我們, CGAffineTransform類型屬于Core Graphics框架,Core Graphics實際上是一個嚴格意義上的2D繪圖API,并且 CGAffineTransform 僅僅 對2D變換有效。

我們提到了zPosition 屬性,可以用來讓圖層靠近或者遠離相機 (用戶視角),transform屬性( CATransfrom3D類型)可以真正做到這點,即讓圖層在3D空間內移動或者旋轉。

CGAffineTransform類似, CATransform3D 也是一個矩陣,但是和2x3的矩 陣不同,CATransform3D是一個可以在3維空間內做變換的4x4的矩陣。

對一個3D像素點做 CATransform3D 矩陣變換.png

CGAffineTransform矩陣類似,Core Animation提供了一系列的方法用來創建和組合CATransform3D類型的矩陣, 和Core Graphics的函數類似, 但是3D的平移和旋轉多出了一個z參數,并且旋轉函數除了angle之外多出了x,y,z三個參數,分別決定了每個坐標方向上的旋轉:

CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)

你應該對X軸和Y軸比較熟悉了,分別以右和下為正方向,Z軸和這兩個軸分別垂直,指向視角外為正方向。

Demo
使用代碼利用CATransform3DMakeRotation對視圖內的圖層 繞Y軸做了45度角的旋轉,我們可以把視圖向右傾斜,這樣會看得更清晰。

- (void)viewDidLoad
{
    [super viewDidLoad];
    //rotate the layer 45 degrees along the Y axis
    CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0,1,0);
    self.layerView.layer.transform = transform;
}
透視投影

在真實世界中,當物體遠離我們的時候,由于視角的原因看起來會變小,理論上說遠離我們的視圖的邊要比靠近視角的邊跟短,但實際上并沒有發生,而我們當前的 視角是等距離的,也就是在3D變換中任然保持平行,和之前提到的仿射變換類似。

在等距投影中,遠處的物體和近處的物體保持同樣的縮放比例,這種投影也有它自 己的用處(例如建筑繪圖,顛倒,和偽3D視頻),但當前我們并不需要。

為了做一些修正,我們需要引入投影變換(又稱作 z變換)來對除了旋轉之外的變 換矩陣做一些修改,Core Animation并沒有給我們提供設置透視變換的函數,因此 我們需要手動修改矩陣值,幸運的是,很簡單:

CATransform3D透視效果通過一個矩陣中一個很簡單的元素來控制:m34 。 用于按比例縮放XY的值來計算到底要離視角多遠。

CATransform3D 的 m34 元素,用來做透視.png

m34 的默認值是0,我們可以通過設置m34-1.0 / d來應用透視效果, d 代 表了想象中視角相機和屏幕之間的距離,以像素為單位,那應該如何計算這個距離 呢?實際上并不需要,大概估算一個就好了。

因為視角相機實際上并不存在,所以可以根據屏幕上的顯示效果自由決定它的防止 的位置。通常500-1000就已經很好了,但對于特定的圖層有時候更小或者更大的值會看起來更舒服,減少距離的值會增強透視效果,所以一個非常微小的值會讓它看起來更加失真,然而一個非常大的值會讓它基本失去透視效果。

- (void)viewDidLoad
{
    [super viewDidLoad];
    //create a new transform
    CATransform3D transform = CATransform3DIdentity;
    //apply perspective
    transform.m34 = - 1.0 / 500.0;
    //rotate by 45 degrees along the Y axis
    transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
    //apply to layer
    self.layerView.layer.transform = transform;
}
滅點

當在透視角度繪圖的時候,遠離相機視角的物體將會變小變遠,當遠離到一個極限
距離,它們可能就縮成了一個點,于是所有的物體最后都匯聚消失在同一個點。

Core Animation定義了這個點位于變換圖層的anchorPoint(通常位于圖層中 心,但也有例外)。這就是說,當圖層發生變換時,這個點永遠位于圖 層變換之前 anchorPoint 的位置。

當改變一個圖層的 position,你也改變了它的滅點,做3D變換的時候要時刻記住這一點,當你視圖通過調整 m34來讓它更加有3D效果,應該首先把它放置于屏 幕中央,然后通過平移來把它移動到指定位置(而不是直接改變它的position ),這樣所有的3D圖層都共享一個滅點。

sublayerTransform 屬性

如果有多個視圖或者圖層,每個都做3D變換,那就需要分別設置相同的m34值,并且確保在變換之前都在屏幕中央共享同一個 position ,如果用一個函數封裝這些 操作的確會更加方便,但仍然有限制(例如,你不能在Interface Builder中擺放視 圖),這里有一個更好的方法。

CALayer 有一個屬性叫做 sublayerTransform。它也是 CATransform3D類型,但和對一個圖層的變換不同,它影響到所有的子圖層。這意味著你可以一次性 對包含這些圖層的容器做變換,于是所有的子圖層都自動繼承了這個變換方法。

相較而言,通過在一個地方設置透視變換會很方便,同時它會帶來另一個顯著的優 勢:滅點被設置在容器圖層的中點,從而不需要再對子圖層分別設置了。這意味著 你可以隨意使用 positionframe 來放置子圖層,而不需要把它們放置在屏幕中點,然后為了保證統一的滅點用變換來做平移。

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //apply perspective transform to container
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = - 1.0 / 500.0;
    self.containerView.layer.sublayerTransform = perspective; // 重點
    //rotate layerView1 by 45 degrees along the Y axis
    CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0,1,0);
    self.layerView1.layer.transform = transform1;
    //rotate layerView2 by 45 degrees along the Y axis
    CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4,0,1,0);
    self.layerView2.layer.transform = transform2;
}
通過相同的透視效果分別對視圖做變換.png
背面

我們既然可以在3D場景下旋轉圖層,那么也可以從背面去觀察它。如果我們把角度修改為 M_PI (180度)而不是當前的 M_PI_4 (45度),那么將會把圖層完全旋轉一個半圈,于是完全背對了相機視角。

原始圖.png
旋轉后的圖.png

如你所見,圖層是雙面繪制的,反面顯示的是正面的一個鏡像圖片。

但這并不是一個很好的特性,因為如果圖層包含文本或者其他控件,那用戶看到這 些內容的鏡像圖片當然會感到困惑。另外也有可能造成資源的浪費:想象用這些圖 層形成一個不透明的固態立方體,既然永遠都看不見這些圖層的背面,那為什么浪 費GPU來繪制它們呢?

CALayer有一個叫做doubleSided的屬性來控制圖層的背面是否要被繪制。這是一個BOOL類型,默認為YES,如果設置為 NO ,那么當圖層正面從相機視角消失的時候,它將不會被繪制。

扁平化圖層

如果內部圖層相對外部圖層做了相反的變換(這里是繞Z軸的旋轉),那么按照邏 輯這兩個變換將被相互抵消。

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *outerView;
@property (nonatomic, weak) IBOutlet UIView *innerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //rotate the outer layer 45 degrees
    CATransform3D outer = CATransform3DMakeRotation(M_PI_4, 0, 0,1);
    self.outerView.layer.transform = outer;
    //rotate the inner layer -45 degrees
    CATransform3D inner = CATransform3DMakeRotation(-M_PI_4, 0, 0,1);
    self.innerView.layer.transform = inner;
}
旋轉后的視圖.png

運行結果和我們預期的一致?,F在在3D情況下再試一次。修改代碼,讓內外兩個視 圖繞Y軸旋轉而不是Z軸,再加上透視效果,以便我們觀察。注意不能用 sublayerTransform 屬性,因為內部的圖層并不直接是容器圖層的子圖層,所以這里分別對圖層設置透視變換。

- (void)viewDidLoad
{
    [super viewDidLoad];
    //rotate the outer layer 45 degrees
    CATransform3D outer = CATransform3DIdentity;
    outer.m34 = -1.0 / 500.0;
    outer = CATransform3DRotate(outer, M_PI_4, 0, 1, 0);
    self.outerView.layer.transform = outer;
    //rotate the inner layer -45 degrees
    CATransform3D inner = CATransform3DIdentity;
    inner.m34 = -1.0 / 500.0;
    inner = CATransform3DRotate(inner, -M_PI_4, 0, 1, 0);
    self.innerView.layer.transform = inner;
}
繞Y軸做相反旋轉的預期結果.png
繞Y軸做相反旋轉的真實結果.png

但其實這并不是我們所看到的,這是由于盡管Core Animation圖層存在于3D空間之內,但它們并不都存在同一個 3D空間。每個圖層的3D場景其實是扁平化的,當你從正面觀察一個圖層,看到的 實際上由子圖層創建的想象出來的3D場景,但當你傾斜這個圖層,你會發現實際上 這個3D場景僅僅是被繪制在圖層的表面。

這使得用Core Animation創建非常復雜的3D場景變得十分困難。你不能夠使用圖層樹去創建一個3D結構的層級關系--在相同場景下的任何3D表面必須和同樣的圖層保持一致,這是因為每個的父視圖都把它的子視圖扁平化了。

至少當你用正常的CALayer的時候是這樣, CALayer有一個叫做CATransformLayer的子類來解決這個問題。具體在“特殊的圖層”中將會 具體討論。

固體對象

現在你懂得了在3D空間的一些圖層布局的基礎,我們來試著創建一個固態的3D對 象(實際上是一個技術上所謂的空洞對象,但它以固態呈現)。我們用六個獨立的視圖來構建一個立方體的各個面。

光亮和陰影

如果想讓立方體看起來更加真實,需要自己做一個陰影效果。你可以通過改變每個面的背景顏色或者 直接用帶光亮效果的圖片來調整。

如果需要動態地創建光線效果,你可以根據每個視圖的方向應用不同的alpha值做出半透明的陰影圖層,但為了計算陰影圖層的不透明度,你需要得到每個面的正太向量(垂直于表面的向量),然后根據一個想象的光源計算出兩個向量叉乘結果。 叉乘代表了光源和圖層之間的角度,從而決定了它有多大程度上的光亮。

我們用GLKit框架來做向量的計算(你需要引入 GLKit庫來運行代碼),每個面的 CATransform3D 都被轉換成 GLKMatrix4,然后通過GLKMatrix4GetMatrix3函數得出一個3×3的旋轉矩陣。這個旋轉矩陣指定了圖層的方向,然后可以用它來得到正太向量的值。

點擊事件

點擊事件的處理由視圖在父視圖中的順序決定的,并不是3D空間中的Z軸順序。

你也許認為把 doubleSided 設置成 NO 可以解決這個問題,因為它不再渲染視圖 后面的內容,但實際上并不起作用。因為背對相機而隱藏的視圖仍然會響應點擊事 件(這和通過設置 hidden 屬性或者設置 alpha0而隱藏的視圖不同,那兩種 方式將不會響應事件)。所以即使禁止了雙面渲染仍然不能解決這個問題(雖然由于性能問題,還是需要把它設置成 NO )。

這里有幾種正確的方案:把其他視圖 userInteractionEnabled 屬性 都設置成 NO 來禁止事件傳遞?;蛘吆唵瓮ㄟ^代碼把修改視圖在父視圖的順序。

總結

這一章涉及了一些2D3D的變換。你學習了一些矩陣計算的基礎,以及如何用Core Animation創建3D場景。你看到了圖層背后到底是如何呈現的,并且知道了不能把扁平的圖片做成真實的立體效果,最后我們用demo說明了觸摸事件的處理,視圖中圖層添加的層級順序會比屏幕上顯示的順序更有意義。

iOS核心動畫高級技巧--目錄

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容