IOS核心動畫高級五:變換

在第四章“視覺效果”中,我們研究了一些增強圖層和它的內容顯示效果的一些技術,在這一章中,我們將要研究可以用來對圖層旋轉、擺放或者扭曲的CGAffineTransform。以及可以將扁平物體轉換成三維空間對象的CATransform3D。

仿射變換

在第三章【圖層幾何學】中我們使用了UIView的transform屬性旋轉了鐘錶的指針,但是並沒有解釋背後運作的原理,實際上UIView的transform屬性是一個CGAffineTransfom類型,用於在二維空間做旋轉、縮放和平移。CGAffineTransform其實是一個可以和二維空間向量(例如CGPoint)做乘法的 3x2的矩陣

用矩陣表示CGAffineTransform和CGPoint

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

因此,通常會用3 x 3(而不是2 X 3 )的矩陣來做二維變換,你可能會見到 3 行 2 列格式的矩陣,這是所謂的以列爲主的格式,上圖所示的是以行爲主的矩陣,只要能保持一致,用哪種格式都無所謂。

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

5.2.jpeg

創建一個CGAffineTransform

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

CGAffineTransformMakeRotation(CGFloat angle); //旋轉
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy); //縮放
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty); //平移

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

我們用一個簡單的項目來做一個demo,把一個原始視圖旋轉45度。

使用仿射變換旋轉了45度的視圖

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

CALayer同樣也有一個transform屬性,但是它的類型是CATransform3D。而不是CGAffineTransform,本章後續將會詳細解釋。

CALayer對應於UIView的CGAffineTransform的屬性叫做affineTransform。

視圖代碼如下

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];


UIImage *image = [UIImage imageNamed:@"tesla.jpg"];

UIView *imgView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
imgView.backgroundColor = [UIColor grayColor];
imgView.layer.contents = (__bridge id)image.CGImage;
imgView.layer.contentsGravity = kCAGravityResizeAspect;
CGAffineTransform t = CGAffineTransformMakeRotation(M_PI_4);
//    imgView.transform = CGAffineTransformRotate(t, M_PI_4);
imgView.transform = t;


[self.view addSubview:imgView];
}

圖層代碼如下:

UIView *imgView2 = [[UIView alloc] initWithFrame:CGRectMake(100, 400, 200, 200)];
imgView2.backgroundColor = [UIColor yellowColor];
imgView2.layer.contents = (__bridge id) image.CGImage;
imgView2.layer.contentsGravity = kCAGravityResize;
imgView2.layer.affineTransform = t;

[self.view addSubview:imgView2];

效果如下:

圖層和視圖的仿射變換

注意我們使用的旋轉常量是M_PI_4,而不是你想象的45,因爲IOS的變換函數使用弧度而不是角度作爲單位弧度用數學常量中的PI的倍數來表示。一個PI代表180度。所以4分之一的PI 就是45度。

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

#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)

混合變換

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

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

當操縱一個變換的時候,初始生成一個什麼都不做的變換很重要,也就是創建一個CGAffineTransform類型的空值,矩陣論中稱爲單位矩陣,CoreGraphics同樣也提供了一個方便的變量。

CGAffineTransformIdentity

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

CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2); //合併

我們來用這些函數組合一個更加複雜的變換。先縮小50 %, 再旋轉30度,最後向右移動200個像素,
代碼如下:

 - (void)transform{

CGAffineTransform transform = CGAffineTransformIdentity;

transform = CGAffineTransformScale(transform, 0.5, 0.5); //縮放50%
transform = CGAffineTransformRotate(transform, M_PI_4); //旋轉45度
transform = CGAffineTransformTranslate(transform, 200, 0); //平移

_layerView.layer.affineTransform = transform;

 }

順序應用多個仿射變換之後的結果.png

在上圖中有些需要注意的地方:圖片向右邊發生了平移,但並沒有指定距離那麼遠(200像素),另外它還有點向下發生了平移。原因在於當你按順序做了變換,上一個變換的結果將會影響之後的變換,所以200像素的向右平移同樣也被旋轉了45度,縮小了50%,所以它實際上是斜向移動了100像素。

這意味着變換的順序會影響最終的結果,也就是說旋轉之後的平移和平移之後的旋轉結果可能不同。

3D變換

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

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

和CGAffineTransform類似,CATransform3D也是一個矩陣,但是和2 x 3的矩陣不同,CATransform3D是一個可以在三維空間內做變換的4 X 4的矩陣。
下圖對一個3D像素點做CATransform3D矩陣變換。
5.6.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(CGFloat tx, CGFloat ty, CGFloat tz);

你應該對X軸和Y軸比較熟悉了,分別以右和下爲正方向(回憶第三章,這是ios上的標準結構,在MacOS上,以Y軸的朝上爲正方向,)* Z軸和這兩個軸分別垂直,指向視角外爲正方向*

X, Y, Z軸以及圍繞它們旋轉的方向

由圖可見,繞Z軸的旋轉等同於之前二維空間中的仿射變換——旋轉,但是X軸和Y軸的旋轉就突破了屏幕的二維空間,並且在用戶的視角看來發生了傾斜

舉個例子,代碼使用CATransform3DMakeRotation對視圖內的圖層繞Y軸做45度角的旋轉,我們可以是視圖向右傾斜,這樣會看的更清晰。

self.view.backgroundColor = [UIColor whiteColor];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"tesla.jpg"];
imageView.layer.transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
[self.view addSubview:imageView];

效果並不像我們期待的那樣

繞Y軸進行旋轉.png

看起來圖層並沒有旋轉,而是僅僅在水平方向上的一個壓縮。是哪裏出問題了呢?
其實完全沒錯,視圖看起來更窄實際上是因爲我們在一個傾斜的角度上看它,而不是透視。

透視投影

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

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

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

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

CATransform3D變換矩陣中的m34元素,用來做透視

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

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

對變換應用透視效果代碼:

 CATransform3D transform3D = CATransform3DIdentity;

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
transform3D.m34 = -1.0/500;
transform3D = CATransform3DRotate(transform3D, M_PI_4, 0, 1, 0);
imageView.layer.transform = transform3D;
imageView.image = [UIImage imageNamed:@"tesla.jpg"];
[self.view addSubview:imageView];

透視效果:

應用透視效果之後再對圖層做旋轉

滅點

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

在現實中,這個點通常是視圖的中心,於是爲了在應用中創建擬真效果的透視,這個點應該聚在屏幕中點,或者至少是包含所有3D對象視圖中點。

滅點

CoreAnimation定義了這個點位於變換圖層的anchorPoint(錨點,通常是位於圖層中心,但也有例外,見第三章),這就是說,當圖層發生變換時,這個點永遠位於圖層發生變換之前anchorPoint的位置

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

sublayerTransform屬性

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

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

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

我們來用一個demo來舉例說明,我們並排放置兩個視圖,然後通過設置它們容器視圖的透視變換,我們可以保證它們有相同的透視和滅點。

應用sublayerTransform代碼如下:

 - (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
UIImageView *imgview1 = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
imgview1.image = [UIImage imageNamed:@"tesla.jpg"];



UIImageView *imgview2 = [[UIImageView alloc] initWithFrame:CGRectMake(200, 20, 200, 200)];
imgview2.image = imgview1.image;

UIView *containtsview = [[UIView alloc] initWithFrame:CGRectMake(0,200,  [UIScreen mainScreen].bounds.size.width,  [UIScreen mainScreen].bounds.size.width)];
containtsview.backgroundColor = [UIColor grayColor];

[containtsview addSubview:imgview1];
[containtsview addSubview:imgview2];


//應用sublayerTransform

CATransform3D transformv = CATransform3DIdentity;
transformv.m34 = -1.0/500.0;
containtsview.layer.sublayerTransform = transformv;

CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
imgview2.layer.transform = transform2;
imgview1.layer.transform = transform1;


[self.view addSubview:containtsview];

}

應用sublayerTransform的變換效果:

通過相同的透視效果分別對視圖做變換

背面

我們既然可以在3D場景下旋轉圖層,那麼也可以從背面去觀察它,如果我們把它的旋轉(圍繞Y軸進行旋轉)角度改成M_PI (180度),而不是當前的M_PI_4(45度)。那麼將會把圖層完全旋轉一個半圈,於是完全背對了相機視角
那麼從背部看圖層是什麼樣子的呢?

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

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

背面

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

代碼如下:

 self.view.backgroundColor = [UIColor whiteColor];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
imageView.backgroundColor = [UIColor grayColor];
imageView.image = [UIImage imageNamed:@"tesla.jpg"];

imageView.layer.transform = CATransform3DMakeRotation(M_PI, 0, 1, 0);
imageView.layer.doubleSided = NO;


[self.view addSubview:imageView];

效果如下:

背面不進行繪製.png

扁平化圖層

如果對包含已經做過變換的圖層的圖層做反方向的變換將會發生什麼呢?是不是有點困惑:
如下圖:反方向變換的嵌套圖層

5.15.jpeg

注意做了-45度旋轉的內部圖層是怎樣抵消旋轉45度的圖層,從而恢復正常狀態的。

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

驗證一下:

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];

UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 400, 400)];
view1.backgroundColor = [UIColor grayColor];

[self.view addSubview:view1];

UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
view2.backgroundColor = [UIColor blueColor];
[view1 addSubview:view2];

CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);
view1.layer.transform = transform1;

CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1);
view2.layer.transform = transform2;

}

效果:

旋轉後的效果

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

圍繞Y進行旋轉 + 透視

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];

UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 400, 400)];
view1.backgroundColor = [UIColor grayColor];

[self.view addSubview:view1];

UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
view2.backgroundColor = [UIColor blueColor];
[view1 addSubview:view2];

CATransform3D transform1 = CATransform3DIdentity;
transform1.m34 = -1.0/500.0;
transform1 = CATransform3DRotate(transform1, M_PI_4, 0, 1, 0);
view1.layer.transform = transform1;

CATransform3D transform2 = CATransform3DIdentity;
transform2.m34 = -1.0/500.0;
transform2 = CATransform3DRotate(transform2, -M_PI_4, 0, 1, 0);

view2.layer.transform = transform2;

}

預期的效果如下:
5.17.jpeg

但其實這並不是我們所看到的,相反,我們所看到的效果如下圖所示,
效果:
圍繞Y軸做旋轉的真是結果

發生了什麼呢?內部的圖層仍然向左旋轉,並且發生了扭曲,但按道理說它應該保持正面朝上,並且顯示正常的方塊。

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

類似的,當你在玩一個3D遊戲,實際上僅僅是把屏幕做了一次傾斜,或許在遊戲中可以看見一面牆在你面前,但是傾斜屏幕並不能夠看見牆裏面的東西所有場景裏面繪製的東西並不會隨着你觀察它的角度改變而發生變化 ,圖層也是同樣的道理。

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

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

固體對象

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

在這個例子中,立方體的六個面,我們當然可以用代碼來寫,但是用interfaceBuilder創建的好處是可以方便的在每一個面上添加子視圖。記住這些面僅僅是包含視圖和控件的普通的用戶界面元素,它們完全是我們界面交互的部分,並且當把它們折成一個正方體之後也不會改變這個性質。

5.19.jpeg

這些面視圖並沒有放置到主視圖中,而是鬆散的排列在根nib文件裏面。我們並不關心在這個容器中如何擺放它們的位置。因爲後續將會用圖層的transform對它們進行重新佈局。並且用interfaceBuilder在容器視圖之外擺放他們可以讓我們容易看清楚他們的 內容。如果把他們一個疊着一個都塞進主視圖,將會變得很難看。

我們把一個有顏色的UILabel放到視圖內部,是爲了清楚的辨別他們之間的關係,並且UIButton被放置在第三個視圖裏面,後面會做簡單解釋。

把視圖組織成立方體的代碼。

   @interface LYCubeViewController ()

   @property(nonatomic, strong) NSMutableArray *viewArray;
   @property(nonatomic, strong) UIView *containerView;

   @end

   @implementation LYCubeViewController

 - (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor grayColor];
//繪製六個視圖
UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(0, 60, 100, 100)];
view1.backgroundColor = [UIColor whiteColor];
UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(150, 60, 100, 100)];
view2.backgroundColor = [UIColor whiteColor];
UIView *view3 = [[UIView alloc] initWithFrame:CGRectMake(0, 200, 100, 100)];
view3.backgroundColor = [UIColor whiteColor];
UIView *view4 = [[UIView alloc] initWithFrame:CGRectMake(150, 200, 100, 100)];
view4.backgroundColor = [UIColor whiteColor];
UIView *view5 = [[UIView alloc] initWithFrame:CGRectMake(0, 350, 100, 100)];
view5.backgroundColor = [UIColor whiteColor];
UIView *view6 = [[UIView alloc] initWithFrame:CGRectMake(150, 350, 100, 100)];
view6.backgroundColor = [UIColor whiteColor];

UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label1.text = @"1";
label1.textAlignment = NSTextAlignmentCenter;
label1.textColor = [UIColor blackColor];
UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label2.text = @"2";
label2.textAlignment = NSTextAlignmentCenter;
label2.textColor = [UIColor redColor];
UILabel *label3 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label3.text = @"3";
label3.textAlignment = NSTextAlignmentCenter;
label3.textColor = [UIColor blueColor];
UILabel *label4 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label4.text = @"4";
label4.textAlignment = NSTextAlignmentCenter;
label4.textColor = [UIColor yellowColor];
UILabel *label5 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label5.text = @"5";
label5.textAlignment = NSTextAlignmentCenter;
label5.textColor = [UIColor greenColor];
UILabel *label6 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label6.text = @"6";
label6.textAlignment = NSTextAlignmentCenter;
label6.textColor = [UIColor orangeColor];

[view1 addSubview:label1];
[view2 addSubview:label2];
[view3 addSubview:label3];
[view4 addSubview:label4];
[view5 addSubview:label5];
[view6 addSubview:label6];

_viewArray = [[NSMutableArray alloc] init];
[_viewArray addObject:view1];
[_viewArray addObject:view2];
[_viewArray addObject:view3];
[_viewArray addObject:view4];
[_viewArray addObject:view5];
[_viewArray addObject:view6];


self.containerView = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];

[self.view addSubview:self.containerView];
[self cubeInit];
}

- (void)cubeInit
 {
//摺合立方體
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0/500.0;
self.containerView.layer.sublayerTransform = perspective;  //爲所有的子圖層設置透視變換

//第一面視圖
CATransform3D transform = CATransform3DMakeTranslation( 0, 0, 50);//特別注意變換是一個嶄新的變換對象
[self addFace:0 withTransform:transform];
//第二面視圖
transform = CATransform3DMakeTranslation(50, 0, 0); //特別注意變換是一個嶄新的變換對象
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
[self addFace:1 withTransform:transform];
//第三面視圖
transform = CATransform3DMakeTranslation(-50, 0, 0);//特別注意變換是一個嶄新的變換對象
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1 ,0);
[self addFace:2 withTransform:transform];
//第四面視圖
transform = CATransform3DMakeTranslation( 0, 50, 0);//特別注意變換是一個嶄新的變換對象
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
[self addFace:3 withTransform:transform];

//第五面視圖
transform = CATransform3DMakeTranslation( 0, -50, 0);//特別注意變換是一個嶄新的變換對象
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
[self addFace:4 withTransform:transform];

//第六面視圖
transform = CATransform3DMakeTranslation( 0, 0, -50);//特別注意變換是一個嶄新的變換對象
transform = CATransform3DRotate(transform, M_PI, 1, 0, 0);
[self addFace:5 withTransform:transform];
 }


   - (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
{
//get the face view and add it to the container
UIView *face = _viewArray[index];
[self.containerView addSubview:face];
//因爲有透視效果,需要先設置視圖的中心點是父視圖的重點
CGSize containerSize = self.containerView.bounds.size;
face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
// 應用變換
face.layer.transform = transform;
}

立方體效果

立方體.png

從這個角度看立方體並不是很明顯。看起來只是一個方塊,爲了更好的欣賞它,我們將更換一個更好的角度。

旋轉這個立方體會顯得很笨重,因爲我們需要單獨對每一個視圖進行旋轉,另一個簡單的方案是通過調整容器的sublayerTransform去旋轉照相機。

添加如下幾行去旋轉containerView圖層的perspective變換矩陣。

perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);

這就對相機(或者相對相機的整個場景,你也可以這麼認爲)圍繞Y軸旋轉了45度,並且圍繞X軸旋轉了45度,現在從另一個角度去觀察立方體,就能看出它的真實面貌。

調整後的代碼:

   @interface LYCubeViewController ()

    @property(nonatomic, strong) NSMutableArray *viewArray;
    @property(nonatomic, strong) UIView *containerView;

     @end

      @implementation LYCubeViewController

 - (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor grayColor];
//繪製六個視圖
UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(0, 60, 100, 100)];
view1.backgroundColor = [UIColor whiteColor];
UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(150, 60, 100, 100)];
view2.backgroundColor = [UIColor whiteColor];
UIView *view3 = [[UIView alloc] initWithFrame:CGRectMake(0, 200, 100, 100)];
view3.backgroundColor = [UIColor whiteColor];
UIView *view4 = [[UIView alloc] initWithFrame:CGRectMake(150, 200, 100, 100)];
view4.backgroundColor = [UIColor whiteColor];
UIView *view5 = [[UIView alloc] initWithFrame:CGRectMake(0, 350, 100, 100)];
view5.backgroundColor = [UIColor whiteColor];
UIView *view6 = [[UIView alloc] initWithFrame:CGRectMake(150, 350, 100, 100)];
view6.backgroundColor = [UIColor whiteColor];

UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label1.text = @"1";
label1.textAlignment = NSTextAlignmentCenter;
label1.textColor = [UIColor blackColor];
UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label2.text = @"2";
label2.textAlignment = NSTextAlignmentCenter;
label2.textColor = [UIColor redColor];
UILabel *label3 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label3.text = @"3";
label3.textAlignment = NSTextAlignmentCenter;
label3.textColor = [UIColor blueColor];
UILabel *label4 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label4.text = @"4";
label4.textAlignment = NSTextAlignmentCenter;
label4.textColor = [UIColor yellowColor];
UILabel *label5 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label5.text = @"5";
label5.textAlignment = NSTextAlignmentCenter;
label5.textColor = [UIColor greenColor];
UILabel *label6 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label6.text = @"6";
label6.textAlignment = NSTextAlignmentCenter;
label6.textColor = [UIColor orangeColor];

[view1 addSubview:label1];
[view2 addSubview:label2];
[view3 addSubview:label3];
[view4 addSubview:label4];
[view5 addSubview:label5];
[view6 addSubview:label6];

_viewArray = [[NSMutableArray alloc] init];
[_viewArray addObject:view1];
[_viewArray addObject:view2];
[_viewArray addObject:view3];
[_viewArray addObject:view4];
[_viewArray addObject:view5];
[_viewArray addObject:view6];


self.containerView = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];

[self.view addSubview:self.containerView];
[self cubeInit];
 }

- (void)cubeInit
 {
//摺合立方體
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0/500.0;
perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);
self.containerView.layer.sublayerTransform = perspective;  //爲所有的子圖層設置透視變換


//第一面視圖
CATransform3D transform = CATransform3DMakeTranslation( 0, 0, 50);//特別注意變換是一個嶄新的變換對象
[self addFace:0 withTransform:transform];
//第二面視圖
transform = CATransform3DMakeTranslation(50, 0, 0); //特別注意變換是一個嶄新的變換對象
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
[self addFace:1 withTransform:transform];
//第三面視圖
transform = CATransform3DMakeTranslation(-50, 0, 0);//特別注意變換是一個嶄新的變換對象
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1 ,0);
[self addFace:2 withTransform:transform];
//第四面視圖
transform = CATransform3DMakeTranslation( 0, 50, 0);//特別注意變換是一個嶄新的變換對象
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
[self addFace:3 withTransform:transform];

//第五面視圖
transform = CATransform3DMakeTranslation( 0, -50, 0);//特別注意變換是一個嶄新的變換對象
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
[self addFace:4 withTransform:transform];

//第六面視圖
transform = CATransform3DMakeTranslation( 0, 0, -50);//特別注意變換是一個嶄新的變換對象
transform = CATransform3DRotate(transform, M_PI, 1, 0, 0);
[self addFace:5 withTransform:transform];
 }


 - (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
 {
//get the face view and add it to the container
UIView *face = _viewArray[index];
[self.containerView addSubview:face];
//因爲有透視效果,需要先設置視圖的中心點是父視圖的重點
CGSize containerSize = self.containerView.bounds.size;
face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
// 應用變換
face.layer.transform = transform;
}

從一個邊角觀察到的立方體效果:

從一個邊角觀察到的立方體

光亮和陰影

現在它看起來更像一個立方體沒錯了,但是對每個面之間的連接還是很難分辨。Core Animation可以用3D顯示圖形,但是它對光線並沒有概念。如果想讓這個立方體看起來更加真實,需要自己做一個陰影效果。你可以通過改變每個面的背景顏色或者直接用帶光亮效果的圖片來調整。

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

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

試着通過調整LIGHT_DIRECTION和AMBIENT_LIGHT的值來切換光線效果

//添加光線效果

- (void)applyLightingToFace:(CALayer *)face
{
//add lighting layer
CALayer *layer = [CALayer layer];
layer.frame = face.bounds;
[face addSublayer:layer];
//convert the face transform to matrix
//(GLKMatrix4 has the same structure as CATransform3D)
//譯者注:GLKMatrix4和CATransform3D內存結構一致,但座標類型有長度區別,所以理論上應該做一次float到CGFloat的轉換,感謝[@zihuyishi](https://github.com/zihuyishi)同學~
CATransform3D transform = face.transform;
GLKMatrix4 matrix4 = *(GLKMatrix4 *)&transform;
GLKMatrix3 matrix3 = GLKMatrix4GetMatrix3(matrix4);
//get face normal
GLKVector3 normal = GLKVector3Make(0, 0, 1);
normal = GLKMatrix3MultiplyVector3(matrix3, normal);
normal = GLKVector3Normalize(normal);
//get dot product with light direction
GLKVector3 light = GLKVector3Normalize(GLKVector3Make(LIGHT_DIRECTION));
float dotProduct = GLKVector3DotProduct(light, normal);
//set lighting layer opacity
CGFloat shadow = 1 + dotProduct - AMBIENT_LIGHT;
UIColor *color = [UIColor colorWithWhite:0 alpha:shadow];
layer.backgroundColor = color.CGColor;
}


 - (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
{
//get the face view and add it to the container
UIView *face = _viewArray[index];
[self.containerView addSubview:face];
//因爲有透視效果,需要先設置視圖的中心點是父視圖的重點
CGSize containerSize = self.containerView.bounds.size;
face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
// 應用變換
face.layer.transform = transform;
 [self applyLightingToFace:face.layer];
 }

動態計算光線效果之後的立方體

5.22.jpeg

點擊事件

你應該能注意到現在可以在第5️⃣個表面頂部看到按鈕了,點擊它什麼都沒有發生,爲什麼呢?

這並不是因爲ios在3D場景下不能正確的響應事件,實際上是可以做到的。問題在於視圖順序,在第三章中我們簡要提到過,點擊事件的處理是由視圖在父視圖中的順序決定的,並不是3D空間中的Z軸順序當給立方體添加視圖的時候,我們實際上是按照一個順序添加的,所以按照視圖/圖層順序來說,1,2,3,4,都在5的前面

既然我們看不到3,4,6的表面(因爲被1,2,5遮住了)ios在事件響應上仍然按照之前的順序,當試圖點擊5表面的按鈕時,表面1,2,3,4截斷了點擊事件(取決於點擊的位置),這就和普通的2D佈局在按鈕上覆蓋物體一樣

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

這裏有幾種正確的方案:把除了5視圖之外的其他視圖的userInteractionEnable設置成NO來禁止事件傳遞。或者簡單通過代碼將視圖5覆蓋到視圖3,4上,無論怎樣都可以點擊按鈕了。

如圖:
響應事件.png

總結

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

在第六章中我們研究一下CoreAnimation提供的具有不同功能的具體的CALayer;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章