本文轉載自:http://www.cocoachina.com/ios/20150104/10816.html? 為了防止cocochina以后刪除該文章,故轉載至此;
(五)變換
很不幸,沒人能告訴你母體是什么,你只能自己體會 -- 駭客帝國
在第四章“可視效果”中,我們研究了一些增強圖層和它的內容顯示效果的一些技術,在這一章中,我們將要研究可以用來對圖層旋轉,擺放或者扭曲的CGAffineTransform,以及可以將扁平物體轉換成三維空間對象的CATransform3D(而不是僅僅對圓角矩形添加下沉陰影)。
仿射變換
在第三章“圖層幾何學”中,我們使用了UIView的transform屬性旋轉了鐘的指針,但并沒有解釋背后運作的原理,實際上UIView的transform屬性是一個CGAffineTransform類型,用于在二維空間做旋轉,縮放和平移。CGAffineTransform是一個可以和二維空間向量(例如CGPoint)做乘法的3X2的矩陣(見圖5.1)。
圖5.1 用矩陣表示的CGAffineTransform和CGPoint
用CGPoint的每一列和CGAffineTransform矩陣的每一行對應元素相乘再求和,就形成了一個新的CGPoint類型的結果。要解釋一下圖中顯示的灰色元素,為了能讓矩陣做乘法,左邊矩陣的列數一定要和右邊矩陣的行數個數相同,所以要給矩陣填充一些標志值,使得既可以讓矩陣做乘法,又不改變運算結果,并且沒必要存儲這些添加的值,因為它們的值不會發生變化,但是要用來做運算。
因此,通常會用3×3(而不是2×3)的矩陣來做二維變換,你可能會見到3行2列格式的矩陣,這是所謂的以列為主的格式,圖5.1所示的是以行為主的格式,只要能保持一致,用哪種格式都無所謂。
當對圖層應用變換矩陣,圖層矩形內的每一個點都被相應地做變換,從而形成一個新的四邊形的形狀。CGAffineTransform中的“仿射”的意思是無論變換矩陣用什么值,圖層中平行的兩條線在變換之后任然保持平行,CGAffineTransform可以做出任意符合上述標注的變換,圖5.2顯示了一些仿射的和非仿射的變換:
圖5.2 仿射和非仿射變換
創建一個CGAffineTransform
對矩陣數學做一個全面的闡述就超出本書的討論范圍了,不過如果你對矩陣完全不熟悉的話,矩陣變換可能會使你感到畏懼。幸運的是,Core Graphics提供了一系列函數,對完全沒有數學基礎的開發者也能夠簡單地做一些變換。如下幾個函數都創建了一個CGAffineTransform實例:
CGAffineTransformMakeRotation(CGFloat?angle)
CGAffineTransformMakeScale(CGFloat?sx,?CGFloat?sy)
CGAffineTransformMakeTranslation(CGFloat?tx,?CGFloat?ty)
旋轉和縮放變換都可以很好解釋--分別旋轉或者縮放一個向量的值。平移變換是指每個點都移動了向量指定的x或者y值--所以如果向量代表了一個點,那它就平移了這個點的距離。
我們用一個很簡單的項目來做個demo,把一個原始視圖旋轉45度角度(圖5.3)
圖5.3 使用仿射變換旋轉45度角之后的視圖
UIView可以通過設置transform屬性做變換,但實際上它只是封裝了內部圖層的變換。
CALayer同樣也有一個transform屬性,但它的類型是CATransform3D,而不是CGAffineTransform,本章后續將會詳細解釋。CALayer對應于UIView的transform屬性叫做affineTransform,清單5.1的例子就是使用affineTransform對圖層做了45度順時針旋轉。
清單5.1 使用affineTransform對圖層旋轉45度
@interface?ViewController?()
@property?(nonatomic,?weak)?IBOutlet?UIView?*layerView;
@end
@implementation?ViewController
-?(void)viewDidLoad
{
[superviewDidLoad];
//rotate?the?layer?45?degrees
CGAffineTransform?transform?=?CGAffineTransformMakeRotation(M_PI_4);
self.layerView.layer.affineTransform?=?transform;
}
@end
注意我們使用的旋轉常量是M_PI_4,而不是你想象的45,因為iOS的變換函數使用弧度而不是角度作為單位。弧度用數學常量pi的倍數表示,一個pi代表180度,所以四分之一的pi就是45度。
C的數學函數庫(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提供了一系列的函數可以在一個變換的基礎上做更深層次的變換,如果做一個既要縮放又要旋轉的變換,這就會非常有用了。例如下面幾個函數:
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);
我們來用這些函數組合一個更加復雜的變換,先縮小50%,再旋轉30度,最后向右移動200個像素(清單5.2)。圖5.4顯示了圖層變換最后的結果。
清單5.2 使用若干方法創建一個復合變換
-?(void)viewDidLoad
{
[superviewDidLoad];//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 順序應用多個仿射變換之后的結果
圖5.4中有些需要注意的地方:圖片向右邊發生了平移,但并沒有指定距離那么遠(200像素),另外它還有點向下發生了平移。原因在于當你按順序做了變換,上一個變換的結果將會影響之后的變換,所以200像素的向右平移同樣也被旋轉了30度,縮小了50%,所以它實際上是斜向移動了100像素。
這意味著變換的順序會影響最終的結果,也就是說旋轉之后的平移和平移之后的旋轉結果可能不同。
剪切變換
Core Graphics為你提供了計算變換矩陣的一些方法,所以很少需要直接設置CGAffineTransform的值。除非需要創建一個斜切的變換,Core Graphics并沒有提供直接的函數。
斜切變換是放射變換的第四種類型,較于平移,旋轉和縮放并不常用(這也是Core Graphics沒有提供相應函數的原因),但有些時候也會很有用。我們用一張圖片可以很直接的說明效果(圖5.5)。也許用“傾斜”描述更加恰當,具體做變換的代碼見清單5.3。
圖5.5 水平方向的斜切變換
清單5.3 實現一個斜切變換
@implementation?ViewController
CGAffineTransform?CGAffineTransformMakeShear(CGFloat?x,?CGFloat?y)
{
CGAffineTransform?transform?=?CGAffineTransformIdentity;
transform.c?=?-x;
transform.b?=?y;
returntransform;
}
-?(void)viewDidLoad
{
[superviewDidLoad];
//shear?the?layer?at?a?45-degree?angle
self.layerView.layer.affineTransform?=?CGAffineTransformMakeShear(1,?0);
}
@end
3D變換
CG的前綴告訴我們,CGAffineTransform類型屬于Core Graphics框架,Core Graphics實際上是一個嚴格意義上的2D繪圖API,并且CGAffineTransform僅僅對2D變換有效。
在第三章中,我們提到了zPosition屬性,可以用來讓圖層靠近或者遠離相機(用戶視角),transform屬性(CATransform3D類型)可以真正做到這點,即讓圖層在3D空間內移動或者旋轉。
和CGAffineTransform類似,CATransform3D也是一個矩陣,但是和2x3的矩陣不同,CATransform3D是一個可以在3維空間內做變換的4x4的矩陣(圖5.6)。
圖5.6 對一個3D像素點做CATransform3D矩陣變換
和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軸比較熟悉了,分別以右和下為正方向(回憶第三章,這是iOS上的標準結構,在Mac OS,Y軸朝上為正方向),Z軸和這兩個軸分別垂直,指向視角外為正方向(圖5.7)。
圖5.7 X,Y,Z軸,以及圍繞它們旋轉的方向
由圖所見,繞Z軸的旋轉等同于之前二維空間的仿射旋轉,但是繞X軸和Y軸的旋轉就突破了屏幕的二維空間,并且在用戶視角看來發生了傾斜。
舉個例子:清單5.4的代碼使用了CATransform3DMakeRotation對視圖內的圖層繞Y軸做了45度角的旋轉,我們可以把視圖向右傾斜,這樣會看得更清晰。
結果見圖5.8,但并不像我們期待的那樣。
清單5.4 繞Y軸旋轉圖層
@implementation?ViewController
-?(void)viewDidLoad
{
[superviewDidLoad];
//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軸旋轉45度的視圖
看起來圖層并沒有被旋轉,而是僅僅在水平方向上的一個壓縮,是哪里出了問題呢?
其實完全沒錯,視圖看起來更窄實際上是因為我們在用一個斜向的視角看它,而不是透視。
透視投影
在真實世界中,當物體原理我們的時候,由于視角的原因看起來會變小,理論上說遠離我們的視圖的邊要比靠近視角的邊跟短,但實際上并沒有發生,而我們當前的視角是等距離的,也就是在3D變換中任然保持平行,和之前提到的仿射變換類似。
在等距投影中,遠處的物體和近處的物體保持同樣的縮放比例,這種投影也有它自己的用處(例如建筑繪圖,顛倒,和偽3D視頻),但當前我們并不需要。
為了做一些修正,我們需要引入投影變換(又稱作z變換)來對除了旋轉之外的變換矩陣做一些修改,Core Animation并沒有給我們提供設置透視變換的函數,因此我們需要手動修改矩陣值,幸運的是,很簡單:
CATransform3D的透視效果通過一個矩陣中一個很簡單的元素來控制:m34。m34(圖5.9)用于按比例縮放X和Y的值來計算到底要離視角多遠。
圖5.9 CATransform3D的m34元素,用來做透視
m34的默認值是0,我們可以通過設置m34為-1.0 / d來應用透視效果,d代表了想象中視角相機和屏幕之間的距離,以像素為單位,那應該如何計算這個距離呢?實際上并不需要,大概估算一個就好了。
因為視角相機實際上并不存在,所以可以根據屏幕上的顯示效果自由決定它的防止的位置。通常500-1000就已經很好了,但對于特定的圖層有時候更小后者更大的值會看起來更舒服,減少距離的值會增強透視效果,所以一個非常微小的值會讓它看起來更加失真,然而一個非常大的值會讓它基本失去透視效果,對視圖應用透視的代碼見清單5.5,結果見圖5.10。
清單5.5 對變換應用透視效果
@implementation?ViewController
-?(void)viewDidLoad
{
[superviewDidLoad];
//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 應用透視效果之后再次對圖層做旋轉
消亡點
當在透視角度繪圖的時候,遠離相機視角的物體將會變小變遠,當遠離到一個極限距離,它們可能就縮成了一個點,于是所有的物體最后都匯聚消失在同一個點。
在現實中,這個點通常是視圖的中心(圖5.11),于是為了在應用中創建擬真效果的透視,這個店應該聚在屏幕中點,或者至少是包含所有3D對象的視圖中點。
圖5.11 消亡點
Core Animation定義了這個點位于變換圖層的anchorPoint(通常位于圖層中心,但也有例外,見第三章)。這就是說,當圖層發生變換時,這個點永遠位于圖層變換之前anchorPoint的位置。
當改變一個圖層的position,你也改變了它的消亡點,做3D變換的時候要時刻記住這一點,當你視圖通過調整m34來讓它更加有3D效果,應該首先把它放置于屏幕中央,然后通過平移來把它移動到指定位置(而不是直接改變它的position),這樣所有的3D圖層都共享一個消亡點。
sublayerTransform屬性
如果有多個視圖或者圖層,每個都做3D變換,那就需要分別設置相同的m34值,并且確保在變換之前都在屏幕中央共享同一個position,如果用一個函數封裝這些操作的確會更加方便,但仍然有限制(例如,你不能在Interface Builder中擺放視圖),這里有一個更好的方法。
CALayer有一個屬性叫做sublayerTransform。它也是CATransform3D類型,但和對一個圖層的變換不同,它影響到所有的子圖層。這意味著你可以一次性對包含這些圖層的容器做變換,于是所有的子圖層都自動繼承了這個變換方法。
相較而言,通過在一個地方設置透視變換會很方便,同時它會帶來另一個顯著的優勢:消亡點被設置在容器圖層的中點,從而不需要再對子圖層分別設置了。這意味著你可以隨意使用position和frame來放置子圖層,而不需要把它們放置在屏幕中點,然后為了保證統一的消亡點用變換來做平移。
我們來用一個demo舉例說明。這里用Interface Builder并排放置兩個視圖(圖5.12),然后通過設置它們容器視圖的透視變換,我們可以保證它們有相同的透視和消亡點,代碼見清單5.6,結果見圖5.13。
圖5.12 在一個視圖容器內并排放置兩個視圖
清單5.6 應用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
{
[superviewDidLoad];
//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場景下旋轉圖層,那么也可以從背面去觀察它。如果我們在清單5.4中把角度修改為M_PI(180度)而不是當前的M_PI_4(45度),那么將會把圖層完全旋轉一個半圈,于是完全背對了相機視角。
那么從背部看圖層是什么樣的呢,見圖5.14
圖5.14 視圖的背面,一個鏡像對稱的圖片
如你所見,圖層是雙面繪制的,反面顯示的是正面的一個鏡像圖片。
但這并不是一個很好的特性,因為如果圖層包含文本或者其他控件,那用戶看到這些內容的鏡像圖片當然會感到困惑。另外也有可能造成資源的浪費:想象用這些圖層形成一個不透明的固態立方體,既然永遠都看不見這些圖層的背面,那為什么浪費GPU來繪制它們呢?
CALayer有一個叫做doubleSided的屬性來控制圖層的背面是否要被繪制。這是一個BOOL類型,默認為YES,如果設置為NO,那么當圖層正面從相機視角消失的時候,它將不會被繪制。
扁平化圖層
如果對包含已經做過變換的圖層的圖層做反方向的變換將會發什么什么呢?是不是有點困惑?見圖5.15
圖5.15 反方向變換的嵌套圖層
注意做了-45度旋轉的內部圖層是怎樣抵消旋轉45度的圖層,從而恢復正常狀態的。
如果內部圖層相對外部圖層做了相反的變換(這里是繞Z軸的旋轉),那么按照邏輯這兩個變換將被相互抵消。
驗證一下,相應代碼見清單5.7,結果見5.16
清單5.7 繞Z軸做相反的旋轉變換
@interface?ViewController?()
@property?(nonatomic,?weak)?IBOutlet?UIView?*outerView;
@property?(nonatomic,?weak)?IBOutlet?UIView?*innerView;
@end
@implementation?ViewController
-?(void)viewDidLoad
{
[superviewDidLoad];
//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 旋轉后的視圖
運行結果和我們預期的一致。現在在3D情況下再試一次。修改代碼,讓內外兩個視圖繞Y軸旋轉而不是Z軸,再加上透視效果,以便我們觀察。注意不能用sublayerTransform屬性,因為內部的圖層并不直接是容器圖層的子圖層,所以這里分別對圖層設置透視變換(清單5.8)。
清單5.8 繞Y軸相反的旋轉變換
-?(void)viewDidLoad
{
[superviewDidLoad];
//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;
}
預期的效果應該如圖5.17所示。
圖5.17 繞Y軸做相反旋轉的預期結果。
但其實這并不是我們所看到的,相反,我們看到的結果如圖5.18所示。發什么了什么呢?內部的圖層仍然向左側旋轉,并且發生了扭曲,但按道理說它應該保持正面朝上,并且顯示正常的方塊。
這是由于盡管Core Animation圖層存在于3D空間之內,但它們并不都存在同一個3D空間。每個圖層的3D場景其實是扁平化的,當你從正面觀察一個圖層,看到的實際上由子圖層創建的想象出來的3D場景,但當你傾斜這個圖層,你會發現實際上這個3D場景僅僅是被繪制在圖層的表面。
圖5.18 繞Y軸做相反旋轉的真實結果
類似的,當你在玩一個3D游戲,實際上僅僅是把屏幕做了一次傾斜,或許在游戲中可以看見有一面墻在你面前,但是傾斜屏幕并不能夠看見墻里面的東西。所有場景里面繪制的東西并不會隨著你觀察它的角度改變而發生變化;圖層也是同樣的道理。
這使得用Core Animation創建非常復雜的3D場景變得十分困難。你不能夠使用圖層樹去創建一個3D結構的層級關系--在相同場景下的任何3D表面必須和同樣的圖層保持一致,這是因為每個的父視圖都把它的子視圖扁平化了。
至少當你用正常的CALayer的時候是這樣,CALayer有一個叫做CATransformLayer的子類來解決這個問題。具體在第六章“特殊的圖層”中將會具體討論。
固體對象
現在你懂得了在3D空間的一些圖層布局的基礎,我們來試著創建一個固態的3D對象(實際上是一個技術上所謂的空洞對象,但它以固態呈現)。我們用六個獨立的視圖來構建一個立方體的各個面。
在這個例子中,我們用Interface Builder來構建立方體的面(圖5.19),我們當然可以用代碼來寫,但是用Interface Builder的好處是可以方便的在每一個面上添加子視圖。記住這些面僅僅是包含視圖和控件的普通的用戶界面元素,它們完全是我們界面交互的部分,并且當把它折成一個立方體之后也不會改變這個性質。
圖5.19 用Interface Builder對立方體的六個面進行布局
這些面視圖并沒有放置在主視圖當中,而是松散地排列在根nib文件里面。我們并不關心在這個容器中如何擺放它們的位置,因為后續將會用圖層的transform對它們進行重新布局,并且用Interface Builder在容器視圖之外擺放他們可以讓我們容易看清楚它們的內容,如果把它們一個疊著一個都塞進主視圖,將會變得很難看。
我們把一個有顏色的UILabel放置在視圖內部,是為了清楚的辨別它們之間的關系,并且UIButton被放置在第三個面視圖里面,后面會做簡單的解釋。
具體把視圖組織成立方體的代碼見清單5.9,結果見圖5.20
清單5.9 創建一個立方體
@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
{
[superviewDidLoad];
//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 正面朝上的立方體
從這個角度看立方體并不是很明顯;看起來只是一個方塊,為了更好地欣賞它,我們將更換一個不同的視角。
旋轉這個立方體將會顯得很笨重,因為我們要單獨對每個面做旋轉。另一個簡單的方案是通過調整容器視圖的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度。現在從另一個角度去觀察立方體,就能看出它的真實面貌(圖5.21)。
圖5.21 從一個邊角觀察的立方體
光亮和陰影
現在它看起來更像是一個立方體沒錯了,但是對每個面之間的連接還是很難分辨。Core Animation可以用3D顯示圖層,但是它對光線并沒有概念。如果想讓立方體看起來更加真實,需要自己做一個陰影效果。你可以通過改變每個面的背景顏色或者直接用帶光亮效果的圖片來調整。
如果需要動態地創建光線效果,你可以根據每個視圖的方向應用不同的alpha值做出半透明的陰影圖層,但為了計算陰影圖層的不透明度,你需要得到每個面的正太向量(垂直于表面的向量),然后根據一個想象的光源計算出兩個向量叉乘結果。叉乘代表了光源和圖層之間的角度,從而決定了它有多大程度上的光亮。
清單5.10實現了這樣一個結果,我們用GLKit框架來做向量的計算(你需要引入GLKit庫來運行代碼),每個面的CATransform3D都被轉換成GLKMatrix4,然后通過GLKMatrix4GetMatrix3函數得出一個3×3的旋轉矩陣。這個旋轉矩陣指定了圖層的方向,然后可以用它來得到正太向量的值。
結果如圖5.22所示,試著調整LIGHT_DIRECTION和AMBIENT_LIGHT的值來切換光線效果
清單5.10 對立方體的表面應用動態的光線效果
#import?"ViewController.h"
#import
#import
#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)
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
{
[superviewDidLoad];
//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 動態計算光線效果之后的立方體
點擊事件
你應該能注意到現在可以在第三個表面的頂部看見按鈕了,點擊它,什么都沒發生,為什么呢?
這并不是因為iOS在3D場景下正確地處理響應事件,實際上是可以做到的。問題在于視圖順序。在第三章中我們簡要提到過,點擊事件的處理由視圖在父視圖中的順序決定的,并不是3D空間中的Z軸順序。當給立方體添加視圖的時候,我們實際上是按照一個順序添加,所以按照視圖/圖層順序來說,4,5,6在3的前面。
即使我們看不見4,5,6的表面(因為被1,2,3遮住了),iOS在事件響應上仍然保持之前的順序。當試圖點擊表面3上的按鈕,表面4,5,6截斷了點擊事件(取決于點擊的位置),這就和普通的2D布局在按鈕上覆蓋物體一樣。
你也許認為把doubleSided設置成NO可以解決這個問題,因為它不再渲染視圖后面的內容,但實際上并不起作用。因為背對相機而隱藏的視圖仍然會響應點擊事件(這和通過設置hidden屬性或者設置alpha為0而隱藏的視圖不同,那兩種方式將不會響應事件)。所以即使禁止了雙面渲染仍然不能解決這個問題(雖然由于性能問題,還是需要把它設置成NO)。
這里有幾種正確的方案:把除了表面3的其他視圖userInteractionEnabled屬性都設置成NO來禁止事件傳遞。或者簡單通過代碼把視圖3覆蓋在視圖6上。無論怎樣都可以點擊按鈕了(圖5.23)。
圖5.23 背景視圖不再阻礙按鈕,我們可以點擊它了
總結
這一章涉及了一些2D和3D的變換。你學習了一些矩陣計算的基礎,以及如何用Core Animation創建3D場景。你看到了圖層背后到底是如何呈現的,并且知道了不能把扁平的圖片做成真實的立體效果,最后我們用demo說明了觸摸事件的處理,視圖中圖層添加的層級順序會比屏幕上顯示的順序更有意義。
第六章我們會研究一些Core Animation提供不同功能的具體的CALayer子類。