很不幸,沒人能告訴你母體是什么,你只能自己體會 -- 駭客帝國
在第四章“可視效果”中,我們研究了一些增強(qiáng)圖層和它的內(nèi)容顯示效果的一些技術(shù),在這一章中,我們將要研究可以用來對圖層旋轉(zhuǎn),擺放或者扭曲的CGAffineTransform
,以及可以將扁平物體轉(zhuǎn)換成三維空間對象的CATransform3D
(而不是僅僅對圓角矩形添加下沉陰影)。
在第三章“圖層幾何學(xué)”中,我們使用了UIView
的transform
屬性旋轉(zhuǎn)了鐘的指針,但并沒有解釋背后運(yùn)作的原理,實(shí)際上UIView
的transform
屬性是一個(gè)CGAffineTransform
類型,用于在二維空間做旋轉(zhuǎn),縮放和平移。CGAffineTransform
是一個(gè)可以和二維空間向量(例如CGPoint
)做乘法的3X2的矩陣(見圖5.1)。
圖5.1 用矩陣表示的CGAffineTransform
和CGPoint
用CGPoint
的每一列和CGAffineTransform
矩陣的每一行對應(yīng)元素相乘再求和,就形成了一個(gè)新的CGPoint
類型的結(jié)果。要解釋一下圖中顯示的灰色元素,為了能讓矩陣做乘法,左邊矩陣的列數(shù)一定要和右邊矩陣的行數(shù)個(gè)數(shù)相同,所以要給矩陣填充一些標(biāo)志值,使得既可以讓矩陣做乘法,又不改變運(yùn)算結(jié)果,并且沒必要存儲這些添加的值,因?yàn)樗鼈兊闹挡粫l(fā)生變化,但是要用來做運(yùn)算。
因此,通常會用3×3(而不是2×3)的矩陣來做二維變換,你可能會見到3行2列格式的矩陣,這是所謂的以列為主的格式,圖5.1所示的是以行為主的格式,只要能保持一致,用哪種格式都無所謂。
當(dāng)對圖層應(yīng)用變換矩陣,圖層矩形內(nèi)的每一個(gè)點(diǎn)都被相應(yīng)地做變換,從而形成一個(gè)新的四邊形的形狀。CGAffineTransform
中的“仿射”的意思是無論變換矩陣用什么值,圖層中平行的兩條線在變換之后任然保持平行,CGAffineTransform
可以做出任意符合上述標(biāo)注的變換,圖5.2顯示了一些仿射的和非仿射的變換:
圖5.2 仿射和非仿射變換
CGAffineTransform
對矩陣數(shù)學(xué)做一個(gè)全面的闡述就超出本書的討論范圍了,不過如果你對矩陣完全不熟悉的話,矩陣變換可能會使你感到畏懼。幸運(yùn)的是,Core Graphics提供了一系列函數(shù),對完全沒有數(shù)學(xué)基礎(chǔ)的開發(fā)者也能夠簡單地做一些變換。如下幾個(gè)函數(shù)都創(chuàng)建了一個(gè)CGAffineTransform
實(shí)例:
CGAffineTransformMakeRotation(CGFloat angle)
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)
旋轉(zhuǎn)和縮放變換都可以很好解釋--分別旋轉(zhuǎn)或者縮放一個(gè)向量的值。平移變換是指每個(gè)點(diǎn)都移動了向量指定的x或者y值--所以如果向量代表了一個(gè)點(diǎn),那它就平移了這個(gè)點(diǎn)的距離。
我們用一個(gè)很簡單的項(xiàng)目來做個(gè)demo,把一個(gè)原始視圖旋轉(zhuǎn)45度角度(圖5.3)
圖5.3 使用仿射變換旋轉(zhuǎn)45度角之后的視圖
UIView
可以通過設(shè)置transform
屬性做變換,但實(shí)際上它只是封裝了內(nèi)部圖層的變換。
CALayer
同樣也有一個(gè)transform
屬性,但它的類型是CATransform3D
,而不是CGAffineTransform
,本章后續(xù)將會詳細(xì)解釋。CALayer
對應(yīng)于UIView
的transform
屬性叫做affineTransform
,清單5.1的例子就是使用affineTransform
對圖層做了45度順時(shí)針旋轉(zhuǎn)。
清單5.1 使用affineTransform
對圖層旋轉(zhuǎn)45度
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//rotate the layer 45 degrees
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
self.layerView.layer.affineTransform = transform;
}
@end
注意我們使用的旋轉(zhuǎn)常量是M_PI_4
,而不是你想象的45,因?yàn)閕OS的變換函數(shù)使用弧度而不是角度作為單位?;《扔脭?shù)學(xué)常量pi的倍數(shù)表示,一個(gè)pi代表180度,所以四分之一的pi就是45度。
C的數(shù)學(xué)函數(shù)庫(iOS會自動引入)提供了pi的一些簡便的換算,M_PI_4
于是就是pi的四分之一,如果對換算不太清楚的話,可以用如下的宏做換算:
#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)
#define DEGREES_TO_RADIANS(x) ((x)/180.0*M_PI)
Core Graphics提供了一系列的函數(shù)可以在一個(gè)變換的基礎(chǔ)上做更深層次的變換,如果做一個(gè)既要縮放又要旋轉(zhuǎn)的變換,這就會非常有用了。例如下面幾個(gè)函數(shù):
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)
當(dāng)操縱一個(gè)變換的時(shí)候,初始生成一個(gè)什么都不做的變換很重要--也就是創(chuàng)建一個(gè)CGAffineTransform
類型的空值,矩陣論中稱作單位矩陣,Core Graphics同樣也提供了一個(gè)方便的常量:
CGAffineTransformIdentity
最后,如果需要混合兩個(gè)已經(jīng)存在的變換矩陣,就可以使用如下方法,在兩個(gè)變換的基礎(chǔ)上創(chuàng)建一個(gè)新的變換:
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);
我們來用這些函數(shù)組合一個(gè)更加復(fù)雜的變換,先縮小50%,再旋轉(zhuǎn)30度,最后向右移動200個(gè)像素(清單5.2)。圖5.4顯示了圖層變換最后的結(jié)果。
清單5.2 使用若干方法創(chuàng)建一個(gè)復(fù)合變換
- (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.0); //translate by 200 points
transform = CGAffineTransformTranslate(transform, 200, 0);
//apply transform to layer
self.layerView.layer.affineTransform = transform;
}
圖5.4 順序應(yīng)用多個(gè)仿射變換之后的結(jié)果
圖5.4中有些需要注意的地方:圖片向右邊發(fā)生了平移,但并沒有指定距離那么遠(yuǎn)(200像素),另外它還有點(diǎn)向下發(fā)生了平移。原因在于當(dāng)你按順序做了變換,上一個(gè)變換的結(jié)果將會影響之后的變換,所以200像素的向右平移同樣也被旋轉(zhuǎn)了30度,縮小了50%,所以它實(shí)際上是斜向移動了100像素。
這意味著變換的順序會影響最終的結(jié)果,也就是說旋轉(zhuǎn)之后的平移和平移之后的旋轉(zhuǎn)結(jié)果可能不同。
Core Graphics為你提供了計(jì)算變換矩陣的一些方法,所以很少需要直接設(shè)置CGAffineTransform
的值。除非需要創(chuàng)建一個(gè)斜切的變換,Core Graphics并沒有提供直接的函數(shù)。
斜切變換是放射變換的第四種類型,較于平移,旋轉(zhuǎn)和縮放并不常用(這也是Core Graphics沒有提供相應(yīng)函數(shù)的原因),但有些時(shí)候也會很有用。我們用一張圖片可以很直接的說明效果(圖5.5)。也許用“傾斜”描述更加恰當(dāng),具體做變換的代碼見清單5.3。
圖5.5 水平方向的斜切變換
清單5.3 實(shí)現(xiàn)一個(gè)斜切變換
@implementation ViewController
CGAffineTransform CGAffineTransformMakeShear(CGFloat x, CGFloat y)
{
CGAffineTransform transform = CGAffineTransformIdentity;
transform.c = -x;
transform.b = y;
return transform;
}
- (void)viewDidLoad
{
[super viewDidLoad];
//shear the layer at a 45-degree angle
self.layerView.layer.affineTransform = CGAffineTransformMakeShear(1, 0);
}
@end
CG的前綴告訴我們,CGAffineTransform
類型屬于Core Graphics框架,Core Graphics實(shí)際上是一個(gè)嚴(yán)格意義上的2D繪圖API,并且CGAffineTransform
僅僅對2D變換有效。
在第三章中,我們提到了zPosition
屬性,可以用來讓圖層靠近或者遠(yuǎn)離相機(jī)(用戶視角),transform
屬性(CATransform3D
類型)可以真正做到這點(diǎn),即讓圖層在3D空間內(nèi)移動或者旋轉(zhuǎn)。
和CGAffineTransform
類似,CATransform3D
也是一個(gè)矩陣,但是和2x3的矩陣不同,CATransform3D
是一個(gè)可以在3維空間內(nèi)做變換的4x4的矩陣(圖5.6)。
圖5.6 對一個(gè)3D像素點(diǎn)做CATransform3D
矩陣變換
和CGAffineTransform
矩陣類似,Core Animation提供了一系列的方法用來創(chuàng)建和組合CATransform3D
類型的矩陣,和Core Graphics的函數(shù)類似,但是3D的平移和旋轉(zhuǎn)多處了一個(gè)z
參數(shù),并且旋轉(zhuǎn)函數(shù)除了angle
之外多出了x
,y
,z
三個(gè)參數(shù),分別決定了每個(gè)坐標(biāo)軸方向上的旋轉(zhuǎn):
CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)
你應(yīng)該對X軸和Y軸比較熟悉了,分別以右和下為正方向(回憶第三章,這是iOS上的標(biāo)準(zhǔn)結(jié)構(gòu),在Mac OS,Y軸朝上為正方向),Z軸和這兩個(gè)軸分別垂直,指向視角外為正方向(圖5.7)。
圖5.7 X,Y,Z軸,以及圍繞它們旋轉(zhuǎn)的方向
由圖所見,繞Z軸的旋轉(zhuǎn)等同于之前二維空間的仿射旋轉(zhuǎn),但是繞X軸和Y軸的旋轉(zhuǎn)就突破了屏幕的二維空間,并且在用戶視角看來發(fā)生了傾斜。
舉個(gè)例子:清單5.4的代碼使用了CATransform3DMakeRotation
對視圖內(nèi)的圖層繞Y軸做了45度角的旋轉(zhuǎn),我們可以把視圖向右傾斜,這樣會看得更清晰。
結(jié)果見圖5.8,但并不像我們期待的那樣。
清單5.4 繞Y軸旋轉(zhuǎn)圖層
@implementation ViewController
- (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;
}
@end
圖5.8 繞y軸旋轉(zhuǎn)45度的視圖
看起來圖層并沒有被旋轉(zhuǎn),而是僅僅在水平方向上的一個(gè)壓縮,是哪里出了問題呢?
其實(shí)完全沒錯,視圖看起來更窄實(shí)際上是因?yàn)槲覀冊谟靡粋€(gè)斜向的視角看它,而不是透視。
在真實(shí)世界中,當(dāng)物體遠(yuǎn)離我們的時(shí)候,由于視角的原因看起來會變小,理論上說遠(yuǎn)離我們的視圖的邊要比靠近視角的邊跟短,但實(shí)際上并沒有發(fā)生,而我們當(dāng)前的視角是等距離的,也就是在3D變換中任然保持平行,和之前提到的仿射變換類似。
在等距投影中,遠(yuǎn)處的物體和近處的物體保持同樣的縮放比例,這種投影也有它自己的用處(例如建筑繪圖,顛倒,和偽3D視頻),但當(dāng)前我們并不需要。
為了做一些修正,我們需要引入投影變換(又稱作z變換)來對除了旋轉(zhuǎn)之外的變換矩陣做一些修改,Core Animation并沒有給我們提供設(shè)置透視變換的函數(shù),因此我們需要手動修改矩陣值,幸運(yùn)的是,很簡單:
CATransform3D
的透視效果通過一個(gè)矩陣中一個(gè)很簡單的元素來控制:m34
。m34
(圖5.9)用于按比例縮放X和Y的值來計(jì)算到底要離視角多遠(yuǎn)。
圖5.9 CATransform3D
的m34
元素,用來做透視
m34
的默認(rèn)值是0,我們可以通過設(shè)置m34
為-1.0 / d
來應(yīng)用透視效果,d
代表了想象中視角相機(jī)和屏幕之間的距離,以像素為單位,那應(yīng)該如何計(jì)算這個(gè)距離呢?實(shí)際上并不需要,大概估算一個(gè)就好了。
因?yàn)橐暯窍鄼C(jī)實(shí)際上并不存在,所以可以根據(jù)屏幕上的顯示效果自由決定它的防止的位置。通常500-1000就已經(jīng)很好了,但對于特定的圖層有時(shí)候更小后者更大的值會看起來更舒服,減少距離的值會增強(qiáng)透視效果,所以一個(gè)非常微小的值會讓它看起來更加失真,然而一個(gè)非常大的值會讓它基本失去透視效果,對視圖應(yīng)用透視的代碼見清單5.5,結(jié)果見圖5.10。
清單5.5 對變換應(yīng)用透視效果
@implementation ViewController
- (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;
}
@end
圖5.10 應(yīng)用透視效果之后再次對圖層做旋轉(zhuǎn)
當(dāng)在透視角度繪圖的時(shí)候,遠(yuǎn)離相機(jī)視角的物體將會變小變遠(yuǎn),當(dāng)遠(yuǎn)離到一個(gè)極限距離,它們可能就縮成了一個(gè)點(diǎn),于是所有的物體最后都匯聚消失在同一個(gè)點(diǎn)。
在現(xiàn)實(shí)中,這個(gè)點(diǎn)通常是視圖的中心(圖5.11),于是為了在應(yīng)用中創(chuàng)建擬真效果的透視,這個(gè)點(diǎn)應(yīng)該聚在屏幕中點(diǎn),或者至少是包含所有3D對象的視圖中點(diǎn)。
圖5.11 滅點(diǎn)
Core Animation定義了這個(gè)點(diǎn)位于變換圖層的anchorPoint
(通常位于圖層中心,但也有例外,見第三章)。這就是說,當(dāng)圖層發(fā)生變換時(shí),這個(gè)點(diǎn)永遠(yuǎn)位于圖層變換之前anchorPoint
的位置。
當(dāng)改變一個(gè)圖層的position
,你也改變了它的滅點(diǎn),做3D變換的時(shí)候要時(shí)刻記住這一點(diǎn),當(dāng)你視圖通過調(diào)整m34
來讓它更加有3D效果,應(yīng)該首先把它放置于屏幕中央,然后通過平移來把它移動到指定位置(而不是直接改變它的position
),這樣所有的3D圖層都共享一個(gè)滅點(diǎn)。
sublayerTransform
屬性如果有多個(gè)視圖或者圖層,每個(gè)都做3D變換,那就需要分別設(shè)置相同的m34值,并且確保在變換之前都在屏幕中央共享同一個(gè)position
,如果用一個(gè)函數(shù)封裝這些操作的確會更加方便,但仍然有限制(例如,你不能在Interface Builder中擺放視圖),這里有一個(gè)更好的方法。
CALayer
有一個(gè)屬性叫做sublayerTransform
。它也是CATransform3D
類型,但和對一個(gè)圖層的變換不同,它影響到所有的子圖層。這意味著你可以一次性對包含這些圖層的容器做變換,于是所有的子圖層都自動繼承了這個(gè)變換方法。
相較而言,通過在一個(gè)地方設(shè)置透視變換會很方便,同時(shí)它會帶來另一個(gè)顯著的優(yōu)勢:滅點(diǎn)被設(shè)置在容器圖層的中點(diǎn),從而不需要再對子圖層分別設(shè)置了。這意味著你可以隨意使用position
和frame
來放置子圖層,而不需要把它們放置在屏幕中點(diǎn),然后為了保證統(tǒng)一的滅點(diǎn)用變換來做平移。
我們來用一個(gè)demo舉例說明。這里用Interface Builder并排放置兩個(gè)視圖(圖5.12),然后通過設(shè)置它們?nèi)萜饕晥D的透視變換,我們可以保證它們有相同的透視和滅點(diǎn),代碼見清單5.6,結(jié)果見圖5.13。
圖5.12 在一個(gè)視圖容器內(nèi)并排放置兩個(gè)視圖
清單5.6 應(yīng)用sublayerTransform
@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;
}
圖5.13 通過相同的透視效果分別對視圖做變換
我們既然可以在3D場景下旋轉(zhuǎn)圖層,那么也可以從背面去觀察它。如果我們在清單5.4中把角度修改為M_PI
(180度)而不是當(dāng)前的M_PI_4
(45度),那么將會把圖層完全旋轉(zhuǎn)一個(gè)半圈,于是完全背對了相機(jī)視角。
那么從背部看圖層是什么樣的呢,見圖5.14
圖5.14 視圖的背面,一個(gè)鏡像對稱的圖片
如你所見,圖層是雙面繪制的,反面顯示的是正面的一個(gè)鏡像圖片。
但這并不是一個(gè)很好的特性,因?yàn)槿绻麍D層包含文本或者其他控件,那用戶看到這些內(nèi)容的鏡像圖片當(dāng)然會感到困惑。另外也有可能造成資源的浪費(fèi):想象用這些圖層形成一個(gè)不透明的固態(tài)立方體,既然永遠(yuǎn)都看不見這些圖層的背面,那為什么浪費(fèi)GPU來繪制它們呢?
CALayer
有一個(gè)叫做doubleSided
的屬性來控制圖層的背面是否要被繪制。這是一個(gè)BOOL
類型,默認(rèn)為YES
,如果設(shè)置為NO
,那么當(dāng)圖層正面從相機(jī)視角消失的時(shí)候,它將不會被繪制。
如果對包含已經(jīng)做過變換的圖層的圖層做反方向的變換將會發(fā)什么什么呢?是不是有點(diǎn)困惑?見圖5.15
圖5.15 反方向變換的嵌套圖層
注意做了-45度旋轉(zhuǎn)的內(nèi)部圖層是怎樣抵消旋轉(zhuǎn)45度的圖層,從而恢復(fù)正常狀態(tài)的。
如果內(nèi)部圖層相對外部圖層做了相反的變換(這里是繞Z軸的旋轉(zhuǎn)),那么按照邏輯這兩個(gè)變換將被相互抵消。
驗(yàn)證一下,相應(yīng)代碼見清單5.7,結(jié)果見5.16
清單5.7 繞Z軸做相反的旋轉(zhuǎn)變換
@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;
}
@end
圖5.16 旋轉(zhuǎn)后的視圖
運(yùn)行結(jié)果和我們預(yù)期的一致。現(xiàn)在在3D情況下再試一次。修改代碼,讓內(nèi)外兩個(gè)視圖繞Y軸旋轉(zhuǎn)而不是Z軸,再加上透視效果,以便我們觀察。注意不能用sublayerTransform
屬性,因?yàn)閮?nèi)部的圖層并不直接是容器圖層的子圖層,所以這里分別對圖層設(shè)置透視變換(清單5.8)。
清單5.8 繞Y軸相反的旋轉(zhuǎn)變換
- (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ù)期的效果應(yīng)該如圖5.17所示。
圖5.17 繞Y軸做相反旋轉(zhuǎn)的預(yù)期結(jié)果。
但其實(shí)這并不是我們所看到的,相反,我們看到的結(jié)果如圖5.18所示。發(fā)什么了什么呢?內(nèi)部的圖層仍然向左側(cè)旋轉(zhuǎn),并且發(fā)生了扭曲,但按道理說它應(yīng)該保持正面朝上,并且顯示正常的方塊。
這是由于盡管Core Animation圖層存在于3D空間之內(nèi),但它們并不都存在同一個(gè)3D空間。每個(gè)圖層的3D場景其實(shí)是扁平化的,當(dāng)你從正面觀察一個(gè)圖層,看到的實(shí)際上由子圖層創(chuàng)建的想象出來的3D場景,但當(dāng)你傾斜這個(gè)圖層,你會發(fā)現(xiàn)實(shí)際上這個(gè)3D場景僅僅是被繪制在圖層的表面。
圖5.18 繞Y軸做相反旋轉(zhuǎn)的真實(shí)結(jié)果
類似的,當(dāng)你在玩一個(gè)3D游戲,實(shí)際上僅僅是把屏幕做了一次傾斜,或許在游戲中可以看見有一面墻在你面前,但是傾斜屏幕并不能夠看見墻里面的東西。所有場景里面繪制的東西并不會隨著你觀察它的角度改變而發(fā)生變化;圖層也是同樣的道理。
這使得用Core Animation創(chuàng)建非常復(fù)雜的3D場景變得十分困難。你不能夠使用圖層樹去創(chuàng)建一個(gè)3D結(jié)構(gòu)的層級關(guān)系--在相同場景下的任何3D表面必須和同樣的圖層保持一致,這是因?yàn)槊總€(gè)的父視圖都把它的子視圖扁平化了。
至少當(dāng)你用正常的CALayer
的時(shí)候是這樣,CALayer
有一個(gè)叫做CATransformLayer
的子類來解決這個(gè)問題。具體在第六章“特殊的圖層”中將會具體討論。
現(xiàn)在你懂得了在3D空間的一些圖層布局的基礎(chǔ),我們來試著創(chuàng)建一個(gè)固態(tài)的3D對象(實(shí)際上是一個(gè)技術(shù)上所謂的空洞對象,但它以固態(tài)呈現(xiàn))。我們用六個(gè)獨(dú)立的視圖來構(gòu)建一個(gè)立方體的各個(gè)面。
在這個(gè)例子中,我們用Interface Builder來構(gòu)建立方體的面(圖5.19),我們當(dāng)然可以用代碼來寫,但是用Interface Builder的好處是可以方便的在每一個(gè)面上添加子視圖。記住這些面僅僅是包含視圖和控件的普通的用戶界面元素,它們完全是我們界面交互的部分,并且當(dāng)把它折成一個(gè)立方體之后也不會改變這個(gè)性質(zhì)。
圖5.19 用Interface Builder對立方體的六個(gè)面進(jìn)行布局
這些面視圖并沒有放置在主視圖當(dāng)中,而是松散地排列在根nib文件里面。我們并不關(guān)心在這個(gè)容器中如何擺放它們的位置,因?yàn)楹罄m(xù)將會用圖層的transform
對它們進(jìn)行重新布局,并且用Interface Builder在容器視圖之外擺放他們可以讓我們?nèi)菀卓辞宄鼈兊膬?nèi)容,如果把它們一個(gè)疊著一個(gè)都塞進(jìn)主視圖,將會變得很難看。
我們把一個(gè)有顏色的UILabel
放置在視圖內(nèi)部,是為了清楚的辨別它們之間的關(guān)系,并且UIButton
被放置在第三個(gè)面視圖里面,后面會做簡單的解釋。
具體把視圖組織成立方體的代碼見清單5.9,結(jié)果見圖5.20
清單5.9 創(chuàng)建一個(gè)立方體
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//rotate the layer 45 degrees
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
self.layerView.layer.affineTransform = transform;
}
@end
注意我們使用的旋轉(zhuǎn)常量是M_PI_4
,而不是你想象的45,因?yàn)閕OS的變換函數(shù)使用弧度而不是角度作為單位?;《扔脭?shù)學(xué)常量pi的倍數(shù)表示,一個(gè)pi代表180度,所以四分之一的pi就是45度。
C的數(shù)學(xué)函數(shù)庫(iOS會自動引入)提供了pi的一些簡便的換算,M_PI_4
于是就是pi的四分之一,如果對換算不太清楚的話,可以用如下的宏做換算:
#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)
#define DEGREES_TO_RADIANS(x) ((x)/180.0*M_PI)
Core Graphics提供了一系列的函數(shù)可以在一個(gè)變換的基礎(chǔ)上做更深層次的變換,如果做一個(gè)既要縮放又要旋轉(zhuǎn)的變換,這就會非常有用了。例如下面幾個(gè)函數(shù):
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)
當(dāng)操縱一個(gè)變換的時(shí)候,初始生成一個(gè)什么都不做的變換很重要--也就是創(chuàng)建一個(gè)CGAffineTransform
類型的空值,矩陣論中稱作單位矩陣,Core Graphics同樣也提供了一個(gè)方便的常量:
CGAffineTransformIdentity
最后,如果需要混合兩個(gè)已經(jīng)存在的變換矩陣,就可以使用如下方法,在兩個(gè)變換的基礎(chǔ)上創(chuàng)建一個(gè)新的變換:
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);
我們來用這些函數(shù)組合一個(gè)更加復(fù)雜的變換,先縮小50%,再旋轉(zhuǎn)30度,最后向右移動200個(gè)像素(清單5.2)。圖5.4顯示了圖層變換最后的結(jié)果。
清單5.2 使用若干方法創(chuàng)建一個(gè)復(fù)合變換
- (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.0); //translate by 200 points
transform = CGAffineTransformTranslate(transform, 200, 0);
//apply transform to layer
self.layerView.layer.affineTransform = transform;
}
圖5.4 順序應(yīng)用多個(gè)仿射變換之后的結(jié)果
圖5.4中有些需要注意的地方:圖片向右邊發(fā)生了平移,但并沒有指定距離那么遠(yuǎn)(200像素),另外它還有點(diǎn)向下發(fā)生了平移。原因在于當(dāng)你按順序做了變換,上一個(gè)變換的結(jié)果將會影響之后的變換,所以200像素的向右平移同樣也被旋轉(zhuǎn)了30度,縮小了50%,所以它實(shí)際上是斜向移動了100像素。
這意味著變換的順序會影響最終的結(jié)果,也就是說旋轉(zhuǎn)之后的平移和平移之后的旋轉(zhuǎn)結(jié)果可能不同。
Core Graphics為你提供了計(jì)算變換矩陣的一些方法,所以很少需要直接設(shè)置CGAffineTransform
的值。除非需要創(chuàng)建一個(gè)斜切的變換,Core Graphics并沒有提供直接的函數(shù)。
斜切變換是放射變換的第四種類型,較于平移,旋轉(zhuǎn)和縮放并不常用(這也是Core Graphics沒有提供相應(yīng)函數(shù)的原因),但有些時(shí)候也會很有用。我們用一張圖片可以很直接的說明效果(圖5.5)。也許用“傾斜”描述更加恰當(dāng),具體做變換的代碼見清單5.3。
圖5.5 水平方向的斜切變換
清單5.3 實(shí)現(xiàn)一個(gè)斜切變換
@implementation ViewController
CGAffineTransform CGAffineTransformMakeShear(CGFloat x, CGFloat y)
{
CGAffineTransform transform = CGAffineTransformIdentity;
transform.c = -x;
transform.b = y;
return transform;
}
- (void)viewDidLoad
{
[super viewDidLoad];
//shear the layer at a 45-degree angle
self.layerView.layer.affineTransform = CGAffineTransformMakeShear(1, 0);
}
@end
CG的前綴告訴我們,CGAffineTransform
類型屬于Core Graphics框架,Core Graphics實(shí)際上是一個(gè)嚴(yán)格意義上的2D繪圖API,并且CGAffineTransform
僅僅對2D變換有效。
在第三章中,我們提到了zPosition
屬性,可以用來讓圖層靠近或者遠(yuǎn)離相機(jī)(用戶視角),transform
屬性(CATransform3D
類型)可以真正做到這點(diǎn),即讓圖層在3D空間內(nèi)移動或者旋轉(zhuǎn)。
和CGAffineTransform
類似,CATransform3D
也是一個(gè)矩陣,但是和2x3的矩陣不同,CATransform3D
是一個(gè)可以在3維空間內(nèi)做變換的4x4的矩陣(圖5.6)。
圖5.6 對一個(gè)3D像素點(diǎn)做CATransform3D
矩陣變換
和CGAffineTransform
矩陣類似,Core Animation提供了一系列的方法用來創(chuàng)建和組合CATransform3D
類型的矩陣,和Core Graphics的函數(shù)類似,但是3D的平移和旋轉(zhuǎn)多處了一個(gè)z
參數(shù),并且旋轉(zhuǎn)函數(shù)除了angle
之外多出了x
,y
,z
三個(gè)參數(shù),分別決定了每個(gè)坐標(biāo)軸方向上的旋轉(zhuǎn):
CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)
你應(yīng)該對X軸和Y軸比較熟悉了,分別以右和下為正方向(回憶第三章,這是iOS上的標(biāo)準(zhǔn)結(jié)構(gòu),在Mac OS,Y軸朝上為正方向),Z軸和這兩個(gè)軸分別垂直,指向視角外為正方向(圖5.7)。
圖5.7 X,Y,Z軸,以及圍繞它們旋轉(zhuǎn)的方向
由圖所見,繞Z軸的旋轉(zhuǎn)等同于之前二維空間的仿射旋轉(zhuǎn),但是繞X軸和Y軸的旋轉(zhuǎn)就突破了屏幕的二維空間,并且在用戶視角看來發(fā)生了傾斜。
舉個(gè)例子:清單5.4的代碼使用了CATransform3DMakeRotation
對視圖內(nèi)的圖層繞Y軸做了45度角的旋轉(zhuǎn),我們可以把視圖向右傾斜,這樣會看得更清晰。
結(jié)果見圖5.8,但并不像我們期待的那樣。
清單5.4 繞Y軸旋轉(zhuǎn)圖層
@implementation ViewController
- (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;
}
@end
圖5.8 繞y軸旋轉(zhuǎn)45度的視圖
看起來圖層并沒有被旋轉(zhuǎn),而是僅僅在水平方向上的一個(gè)壓縮,是哪里出了問題呢?
其實(shí)完全沒錯,視圖看起來更窄實(shí)際上是因?yàn)槲覀冊谟靡粋€(gè)斜向的視角看它,而不是透視。
在真實(shí)世界中,當(dāng)物體遠(yuǎn)離我們的時(shí)候,由于視角的原因看起來會變小,理論上說遠(yuǎn)離我們的視圖的邊要比靠近視角的邊跟短,但實(shí)際上并沒有發(fā)生,而我們當(dāng)前的視角是等距離的,也就是在3D變換中任然保持平行,和之前提到的仿射變換類似。
在等距投影中,遠(yuǎn)處的物體和近處的物體保持同樣的縮放比例,這種投影也有它自己的用處(例如建筑繪圖,顛倒,和偽3D視頻),但當(dāng)前我們并不需要。
為了做一些修正,我們需要引入投影變換(又稱作z變換)來對除了旋轉(zhuǎn)之外的變換矩陣做一些修改,Core Animation并沒有給我們提供設(shè)置透視變換的函數(shù),因此我們需要手動修改矩陣值,幸運(yùn)的是,很簡單:
CATransform3D
的透視效果通過一個(gè)矩陣中一個(gè)很簡單的元素來控制:m34
。m34
(圖5.9)用于按比例縮放X和Y的值來計(jì)算到底要離視角多遠(yuǎn)。
圖5.9 CATransform3D
的m34
元素,用來做透視
m34
的默認(rèn)值是0,我們可以通過設(shè)置m34
為-1.0 / d
來應(yīng)用透視效果,d
代表了想象中視角相機(jī)和屏幕之間的距離,以像素為單位,那應(yīng)該如何計(jì)算這個(gè)距離呢?實(shí)際上并不需要,大概估算一個(gè)就好了。
因?yàn)橐暯窍鄼C(jī)實(shí)際上并不存在,所以可以根據(jù)屏幕上的顯示效果自由決定它的放置的位置。通常500-1000就已經(jīng)很好了,但對于特定的圖層有時(shí)候更小或者更大的值會看起來更舒服,減少距離的值會增強(qiáng)透視效果,所以一個(gè)非常微小的值會讓它看起來更加失真,然而一個(gè)非常大的值會讓它基本失去透視效果,對視圖應(yīng)用透視的代碼見清單5.5,結(jié)果見圖5.10。
清單5.5 對變換應(yīng)用透視效果
@implementation ViewController
- (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;
}
@end
圖5.10 應(yīng)用透視效果之后再次對圖層做旋轉(zhuǎn)
當(dāng)在透視角度繪圖的時(shí)候,遠(yuǎn)離相機(jī)視角的物體將會變小變遠(yuǎn),當(dāng)遠(yuǎn)離到一個(gè)極限距離,它們可能就縮成了一個(gè)點(diǎn),于是所有的物體最后都匯聚消失在同一個(gè)點(diǎn)。
在現(xiàn)實(shí)中,這個(gè)點(diǎn)通常是視圖的中心(圖5.11),于是為了在應(yīng)用中創(chuàng)建擬真效果的透視,這個(gè)點(diǎn)應(yīng)該聚在屏幕中點(diǎn),或者至少是包含所有3D對象的視圖中點(diǎn)。
圖5.11 滅點(diǎn)
Core Animation定義了這個(gè)點(diǎn)位于變換圖層的anchorPoint
(通常位于圖層中心,但也有例外,見第三章)。這就是說,當(dāng)圖層發(fā)生變換時(shí),這個(gè)點(diǎn)永遠(yuǎn)位于圖層變換之前anchorPoint
的位置。
當(dāng)改變一個(gè)圖層的position
,你也改變了它的滅點(diǎn),做3D變換的時(shí)候要時(shí)刻記住這一點(diǎn),當(dāng)你視圖通過調(diào)整m34
來讓它更加有3D效果,應(yīng)該首先把它放置于屏幕中央,然后通過平移來把它移動到指定位置(而不是直接改變它的position
),這樣所有的3D圖層都共享一個(gè)滅點(diǎn)。
sublayerTransform
屬性如果有多個(gè)視圖或者圖層,每個(gè)都做3D變換,那就需要分別設(shè)置相同的m34值,并且確保在變換之前都在屏幕中央共享同一個(gè)position
,如果用一個(gè)函數(shù)封裝這些操作的確會更加方便,但仍然有限制(例如,你不能在Interface Builder中擺放視圖),這里有一個(gè)更好的方法。
CALayer
有一個(gè)屬性叫做sublayerTransform
。它也是CATransform3D
類型,但和對一個(gè)圖層的變換不同,它影響到所有的子圖層。這意味著你可以一次性對包含這些圖層的容器做變換,于是所有的子圖層都自動繼承了這個(gè)變換方法。
相較而言,通過在一個(gè)地方設(shè)置透視變換會很方便,同時(shí)它會帶來另一個(gè)顯著的優(yōu)勢:滅點(diǎn)被設(shè)置在容器圖層的中點(diǎn),從而不需要再對子圖層分別設(shè)置了。這意味著你可以隨意使用position
和frame
來放置子圖層,而不需要把它們放置在屏幕中點(diǎn),然后為了保證統(tǒng)一的滅點(diǎn)用變換來做平移。
我們來用一個(gè)demo舉例說明。這里用Interface Builder并排放置兩個(gè)視圖(圖5.12),然后通過設(shè)置它們?nèi)萜饕晥D的透視變換,我們可以保證它們有相同的透視和滅點(diǎn),代碼見清單5.6,結(jié)果見圖5.13。
圖5.12 在一個(gè)視圖容器內(nèi)并排放置兩個(gè)視圖
清單5.6 應(yīng)用sublayerTransform
@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;
}
圖5.13 通過相同的透視效果分別對視圖做變換
我們既然可以在3D場景下旋轉(zhuǎn)圖層,那么也可以從背面去觀察它。如果我們在清單5.4中把角度修改為M_PI
(180度)而不是當(dāng)前的M_PI_4
(45度),那么將會把圖層完全旋轉(zhuǎn)一個(gè)半圈,于是完全背對了相機(jī)視角。
那么從背部看圖層是什么樣的呢,見圖5.14
圖5.14 視圖的背面,一個(gè)鏡像對稱的圖片
如你所見,圖層是雙面繪制的,反面顯示的是正面的一個(gè)鏡像圖片。
但這并不是一個(gè)很好的特性,因?yàn)槿绻麍D層包含文本或者其他控件,那用戶看到這些內(nèi)容的鏡像圖片當(dāng)然會感到困惑。另外也有可能造成資源的浪費(fèi):想象用這些圖層形成一個(gè)不透明的固態(tài)立方體,既然永遠(yuǎn)都看不見這些圖層的背面,那為什么浪費(fèi)GPU來繪制它們呢?
CALayer
有一個(gè)叫做doubleSided
的屬性來控制圖層的背面是否要被繪制。這是一個(gè)BOOL
類型,默認(rèn)為YES
,如果設(shè)置為NO
,那么當(dāng)圖層正面從相機(jī)視角消失的時(shí)候,它將不會被繪制。
如果對包含已經(jīng)做過變換的圖層的圖層做反方向的變換將會發(fā)什么什么呢?是不是有點(diǎn)困惑?見圖5.15
圖5.15 反方向變換的嵌套圖層
注意做了-45度旋轉(zhuǎn)的內(nèi)部圖層是怎樣抵消旋轉(zhuǎn)45度的圖層,從而恢復(fù)正常狀態(tài)的。
如果內(nèi)部圖層相對外部圖層做了相反的變換(這里是繞Z軸的旋轉(zhuǎn)),那么按照邏輯這兩個(gè)變換將被相互抵消。
驗(yàn)證一下,相應(yīng)代碼見清單5.7,結(jié)果見5.16
清單5.7 繞Z軸做相反的旋轉(zhuǎn)變換
@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;
}
@end
圖5.16 旋轉(zhuǎn)后的視圖
運(yùn)行結(jié)果和我們預(yù)期的一致?,F(xiàn)在在3D情況下再試一次。修改代碼,讓內(nèi)外兩個(gè)視圖繞Y軸旋轉(zhuǎn)而不是Z軸,再加上透視效果,以便我們觀察。注意不能用sublayerTransform
屬性,因?yàn)閮?nèi)部的圖層并不直接是容器圖層的子圖層,所以這里分別對圖層設(shè)置透視變換(清單5.8)。
清單5.8 繞Y軸相反的旋轉(zhuǎn)變換
- (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ù)期的效果應(yīng)該如圖5.17所示。
圖5.17 繞Y軸做相反旋轉(zhuǎn)的預(yù)期結(jié)果。
但其實(shí)這并不是我們所看到的,相反,我們看到的結(jié)果如圖5.18所示。發(fā)什么了什么呢?內(nèi)部的圖層仍然向左側(cè)旋轉(zhuǎn),并且發(fā)生了扭曲,但按道理說它應(yīng)該保持正面朝上,并且顯示正常的方塊。
這是由于盡管Core Animation圖層存在于3D空間之內(nèi),但它們并不都存在同一個(gè)3D空間。每個(gè)圖層的3D場景其實(shí)是扁平化的,當(dāng)你從正面觀察一個(gè)圖層,看到的實(shí)際上由子圖層創(chuàng)建的想象出來的3D場景,但當(dāng)你傾斜這個(gè)圖層,你會發(fā)現(xiàn)實(shí)際上這個(gè)3D場景僅僅是被繪制在圖層的表面。
圖5.18 繞Y軸做相反旋轉(zhuǎn)的真實(shí)結(jié)果
類似的,當(dāng)你在玩一個(gè)3D游戲,實(shí)際上僅僅是把屏幕做了一次傾斜,或許在游戲中可以看見有一面墻在你面前,但是傾斜屏幕并不能夠看見墻里面的東西。所有場景里面繪制的東西并不會隨著你觀察它的角度改變而發(fā)生變化;圖層也是同樣的道理。
這使得用Core Animation創(chuàng)建非常復(fù)雜的3D場景變得十分困難。你不能夠使用圖層樹去創(chuàng)建一個(gè)3D結(jié)構(gòu)的層級關(guān)系--在相同場景下的任何3D表面必須和同樣的圖層保持一致,這是因?yàn)槊總€(gè)的父視圖都把它的子視圖扁平化了。
至少當(dāng)你用正常的CALayer
的時(shí)候是這樣,CALayer
有一個(gè)叫做CATransformLayer
的子類來解決這個(gè)問題。具體在第六章“特殊的圖層”中將會具體討論。
現(xiàn)在你懂得了在3D空間的一些圖層布局的基礎(chǔ),我們來試著創(chuàng)建一個(gè)固態(tài)的3D對象(實(shí)際上是一個(gè)技術(shù)上所謂的空洞對象,但它以固態(tài)呈現(xiàn))。我們用六個(gè)獨(dú)立的視圖來構(gòu)建一個(gè)立方體的各個(gè)面。
在這個(gè)例子中,我們用Interface Builder來構(gòu)建立方體的面(圖5.19),我們當(dāng)然可以用代碼來寫,但是用Interface Builder的好處是可以方便的在每一個(gè)面上添加子視圖。記住這些面僅僅是包含視圖和控件的普通的用戶界面元素,它們完全是我們界面交互的部分,并且當(dāng)把它折成一個(gè)立方體之后也不會改變這個(gè)性質(zhì)。
圖5.19 用Interface Builder對立方體的六個(gè)面進(jìn)行布局
這些面視圖并沒有放置在主視圖當(dāng)中,而是松散地排列在根nib文件里面。我們并不關(guān)心在這個(gè)容器中如何擺放它們的位置,因?yàn)楹罄m(xù)將會用圖層的transform
對它們進(jìn)行重新布局,并且用Interface Builder在容器視圖之外擺放他們可以讓我們?nèi)菀卓辞宄鼈兊膬?nèi)容,如果把它們一個(gè)疊著一個(gè)都塞進(jìn)主視圖,將會變得很難看。
我們把一個(gè)有顏色的UILabel
放置在視圖內(nèi)部,是為了清楚的辨別它們之間的關(guān)系,并且UIButton
被放置在第三個(gè)面視圖里面,后面會做簡單的解釋。
具體把視圖組織成立方體的代碼見清單5.9,結(jié)果見圖5.20
清單5.9 創(chuàng)建一個(gè)立方體
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;
@end
@implementation ViewController
- (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
{
//get the face view and add it to the container
UIView *face = self.faces[index];
[self.containerView addSubview:face];
//center the face view within the container
CGSize containerSize = self.containerView.bounds.size;
face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
// apply the transform
face.layer.transform = transform;
}
- (void)viewDidLoad
{
[super viewDidLoad];
//set up the container sublayer transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
//add cube face 1
CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);
[self addFace:0 withTransform:transform];
//add cube face 2
transform = CATransform3DMakeTranslation(100, 0, 0);
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
[self addFace:1 withTransform:transform];
//add cube face 3
transform = CATransform3DMakeTranslation(0, -100, 0);
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
[self addFace:2 withTransform:transform];
//add cube face 4
transform = CATransform3DMakeTranslation(0, 100, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
[self addFace:3 withTransform:transform];
//add cube face 5
transform = CATransform3DMakeTranslation(-100, 0, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
[self addFace:4 withTransform:transform];
//add cube face 6
transform = CATransform3DMakeTranslation(0, 0, -100);
transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
[self addFace:5 withTransform:transform];
}
@end
圖5.20 正面朝上的立方體
從這個(gè)角度看立方體并不是很明顯;看起來只是一個(gè)方塊,為了更好地欣賞它,我們將更換一個(gè)不同的視角。
旋轉(zhuǎn)這個(gè)立方體將會顯得很笨重,因?yàn)槲覀円獑为?dú)對每個(gè)面做旋轉(zhuǎn)。另一個(gè)簡單的方案是通過調(diào)整容器視圖的sublayerTransform
去旋轉(zhuǎn)照相機(jī)。
添加如下幾行去旋轉(zhuǎn)containerView
圖層的perspective
變換矩陣:
perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);
這就對相機(jī)(或者相對相機(jī)的整個(gè)場景,你也可以這么認(rèn)為)繞Y軸旋轉(zhuǎn)45度,并且繞X軸旋轉(zhuǎn)45度。現(xiàn)在從另一個(gè)角度去觀察立方體,就能看出它的真實(shí)面貌(圖5.21)。
圖5.21 從一個(gè)邊角觀察的立方體
現(xiàn)在它看起來更像是一個(gè)立方體沒錯了,但是對每個(gè)面之間的連接還是很難分辨。Core Animation可以用3D顯示圖層,但是它對光線并沒有概念。如果想讓立方體看起來更加真實(shí),需要自己做一個(gè)陰影效果。你可以通過改變每個(gè)面的背景顏色或者直接用帶光亮效果的圖片來調(diào)整。
如果需要動態(tài)地創(chuàng)建光線效果,你可以根據(jù)每個(gè)視圖的方向應(yīng)用不同的alpha值做出半透明的陰影圖層,但為了計(jì)算陰影圖層的不透明度,你需要得到每個(gè)面的正太向量(垂直于表面的向量),然后根據(jù)一個(gè)想象的光源計(jì)算出兩個(gè)向量叉乘結(jié)果。叉乘代表了光源和圖層之間的角度,從而決定了它有多大程度上的光亮。
清單5.10實(shí)現(xiàn)了這樣一個(gè)結(jié)果,我們用GLKit框架來做向量的計(jì)算(你需要引入GLKit庫來運(yùn)行代碼),每個(gè)面的CATransform3D
都被轉(zhuǎn)換成GLKMatrix4
,然后通過GLKMatrix4GetMatrix3
函數(shù)得出一個(gè)3×3的旋轉(zhuǎn)矩陣。這個(gè)旋轉(zhuǎn)矩陣指定了圖層的方向,然后可以用它來得到正太向量的值。
結(jié)果如圖5.22所示,試著調(diào)整LIGHT_DIRECTION
和AMBIENT_LIGHT
的值來切換光線效果
清單5.10 對立方體的表面應(yīng)用動態(tài)的光線效果
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#import <GLKit/GLKit.h>
#define LIGHT_DIRECTION 0, 1, -0.5
#define AMBIENT_LIGHT 0.5
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;
@end
@implementation ViewController
- (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內(nèi)存結(jié)構(gòu)一致,但坐標(biāo)類型有長度區(qū)別,所以理論上應(yīng)該做一次float到CGFloat的轉(zhuǎn)換,感謝[@zihuyishi](https://github.com/zihuyishi)同學(xué)~
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 = self.faces[index];
[self.containerView addSubview:face];
//center the face view within the container
CGSize containerSize = self.containerView.bounds.size;
face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
// apply the transform
face.layer.transform = transform;
//apply lighting
[self applyLightingToFace:face.layer];
}
- (void)viewDidLoad
{
[super viewDidLoad];
//set up the container sublayer transform
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;
//add cube face 1
CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);
[self addFace:0 withTransform:transform];
//add cube face 2
transform = CATransform3DMakeTranslation(100, 0, 0);
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
[self addFace:1 withTransform:transform];
//add cube face 3
transform = CATransform3DMakeTranslation(0, -100, 0);
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
[self addFace:2 withTransform:transform];
//add cube face 4
transform = CATransform3DMakeTranslation(0, 100, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
[self addFace:3 withTransform:transform];
//add cube face 5
transform = CATransform3DMakeTranslation(-100, 0, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
[self addFace:4 withTransform:transform];
//add cube face 6
transform = CATransform3DMakeTranslation(0, 0, -100);
transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
[self addFace:5 withTransform:transform];
}
@end
圖5.22 動態(tài)計(jì)算光線效果之后的立方體
你應(yīng)該能注意到現(xiàn)在可以在第三個(gè)表面的頂部看見按鈕了,點(diǎn)擊它,什么都沒發(fā)生,為什么呢?
這并不是因?yàn)閕OS在3D場景下正確地處理響應(yīng)事件,實(shí)際上是可以做到的。問題在于視圖順序。在第三章中我們簡要提到過,點(diǎn)擊事件的處理由視圖在父視圖中的順序決定的,并不是3D空間中的Z軸順序。當(dāng)給立方體添加視圖的時(shí)候,我們實(shí)際上是按照一個(gè)順序添加,所以按照視圖/圖層順序來說,4,5,6在3的前面。
即使我們看不見4,5,6的表面(因?yàn)楸?,2,3遮住了),iOS在事件響應(yīng)上仍然保持之前的順序。當(dāng)試圖點(diǎn)擊表面3上的按鈕,表面4,5,6截?cái)嗔它c(diǎn)擊事件(取決于點(diǎn)擊的位置),這就和普通的2D布局在按鈕上覆蓋物體一樣。
你也許認(rèn)為把doubleSided
設(shè)置成NO
可以解決這個(gè)問題,因?yàn)樗辉黉秩疽晥D后面的內(nèi)容,但實(shí)際上并不起作用。因?yàn)楸硨ο鄼C(jī)而隱藏的視圖仍然會響應(yīng)點(diǎn)擊事件(這和通過設(shè)置hidden
屬性或者設(shè)置alpha
為0而隱藏的視圖不同,那兩種方式將不會響應(yīng)事件)。所以即使禁止了雙面渲染仍然不能解決這個(gè)問題(雖然由于性能問題,還是需要把它設(shè)置成NO
)。
這里有幾種正確的方案:把除了表面3的其他視圖userInteractionEnabled
屬性都設(shè)置成NO
來禁止事件傳遞。或者簡單通過代碼把視圖3覆蓋在視圖6上。無論怎樣都可以點(diǎn)擊按鈕了(圖5.23)。
圖5.23 背景視圖不再阻礙按鈕,我們可以點(diǎn)擊它了
這一章涉及了一些2D和3D的變換。你學(xué)習(xí)了一些矩陣計(jì)算的基礎(chǔ),以及如何用Core Animation創(chuàng)建3D場景。你看到了圖層背后到底是如何呈現(xiàn)的,并且知道了不能把扁平的圖片做成真實(shí)的立體效果,最后我們用demo說明了觸摸事件的處理,視圖中圖層添加的層級順序會比屏幕上顯示的順序更有意義。
第六章我們會研究一些Core Animation提供不同功能的具體的CALayer
子類。
更多建議: