如今你懂得了在3D空間的一些圖層佈局的基礎,咱們來試着建立一個固態的3D對象(其實是一個技術上所謂的空洞對象,但它以固態呈現)。咱們用六個獨立的視圖來構建一個立方體的各個面。 git
在這個例子中,咱們用Interface Builder來構創建方體的面(圖5.19),咱們固然能夠用代碼來寫,可是用Interface Builder的好處是能夠方便的在每個面上添加子視圖。記住這些面僅僅是包含視圖和控件的普通的用戶界面元素,它們徹底是咱們界面交互的部分,而且當把它折成一個立方體以後也不會改變這個性質。 github
圖5.19 用Interface Builder對立方體的六個面進行佈局 app
這些面視圖並無放置在主視圖當中,而是鬆散地排列在根nib文件裏面。咱們並不關心在這個容器中如何擺放它們的位置,由於後續將會用圖層的transform對它們進行從新佈局,而且用Interface Builder在容器視圖以外擺放他們可讓咱們容易看清楚它們的內容,若是把它們一個疊着一個都塞進主視圖,將會變得很難看。 框架
咱們把一個有顏色的 UILabel 放置在視圖內部,是爲了清楚的辨別它們之間的關係,而且 UIButton 被放置在第三個面視圖裏面,後面會作簡單的解釋。 ide
具體把視圖組織成立方體的代碼見清單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 { [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 正面朝上的立方體 性能
從這個角度看立方體並非很明顯;看起來只是一個方塊,爲了更好地欣賞它,咱們將更換一個不一樣的視角。 學習
旋轉這個立方體將會顯得很笨重,由於咱們要單獨對每一個面作旋轉。另外一個簡單的方案是經過調整容器視圖的sublayerTransform去旋轉照相機。 ui
添加以下幾行去旋轉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 <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內存結構一致,但座標類型有長度區別,因此理論上應該作一次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 = 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 動態計算光線效果以後的立方體
你應該能注意到如今能夠在第三個表面的頂部看見按鈕了,點擊它,什麼都沒發生,爲何呢?
這並非由於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說明了觸摸事件的處理,視圖中圖層添加的層級順序會比屏幕上顯示的順序更有意義。