iOS核心動(dòng)畫(huà)高級(jí)技巧3.2(變換)

目錄
  • 仿射變換
  • 3D變換
  • 固體對(duì)象
  • 總結(jié)
一 仿射變換

我們使用了UIView的transform屬性旋轉(zhuǎn)了鐘的指針级历,但并沒(méi)有解釋背后運(yùn)作的原理,實(shí)際上UIView的transform屬性是一個(gè)CGAffineTransform類型厦凤,用于在二維空間做旋轉(zhuǎn)縮放平移。CGAffineTransform是一個(gè)可以和二維空間向量(例如CGPoint)做乘法的3X2的矩陣。

用矩陣表示的CGAffineTransform和CGPoint.png

CGPoint每一列CGAffineTransform矩陣的每一行對(duì)應(yīng)元素相乘再求和青伤,就形成了一個(gè)新的CGPoint類型的結(jié)果。要解釋一下圖中顯示的灰色元素殴瘦,為了能讓矩陣做乘法狠角,左邊矩陣的列數(shù)一定要和右邊矩陣的行數(shù)個(gè)數(shù)相同,所以要給矩陣填充一些標(biāo)志值痴施,使得既可以讓矩陣做乘法擎厢,又不改變運(yùn)算結(jié)果究流,并且沒(méi)必要存儲(chǔ)這些添加的值辣吃,因?yàn)樗鼈兊闹挡粫?huì)發(fā)生變化,但是要用來(lái)做運(yùn)算芬探。

因此神得,通常會(huì)用3×3(而不是2×3)的矩陣來(lái)做二維變換,你可能會(huì)見(jiàn)到3行2列格式的矩陣偷仿,這是所謂的以列為主的格式哩簿,圖5.1所示的是以行為主的格式宵蕉,只要能保持一致,用哪種格式都無(wú)所謂节榜。

當(dāng)對(duì)圖層應(yīng)用變換矩陣羡玛,圖層矩形內(nèi)的每一個(gè)點(diǎn)都被相應(yīng)地做變換,從而形成一個(gè)新的四邊形的形狀宗苍。CGAffineTransform中的仿射的意思是無(wú)論變換矩陣用什么值稼稿,圖層中平行的兩條線在變換之后任然保持平行,CGAffineTransform可以做出任意符合上述標(biāo)注的變換讳窟,圖5.2顯示了一些仿射的和非仿射的變換:

image.png
1.1 創(chuàng)建一個(gè)CGAffineTransform

對(duì)矩陣數(shù)學(xué)做一個(gè)全面的闡述就超出本書(shū)的討論范圍了让歼,不過(guò)如果你對(duì)矩陣完全不熟悉的話,矩陣變換可能會(huì)使你感到畏懼丽啡。幸運(yùn)的是谋右,Core Graphics提供了一系列函數(shù),對(duì)完全沒(méi)有數(shù)學(xué)基礎(chǔ)的開(kāi)發(fā)者也能夠簡(jiǎn)單地做一些變換补箍。如下幾個(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)都移動(dòng)了向量指定的x或者y值--所以如果向量代表了一個(gè)點(diǎn),那它就平移了這個(gè)點(diǎn)的距離馏予。

我們用一個(gè)很簡(jiǎn)單的項(xiàng)目來(lái)做個(gè)demo天梧,把一個(gè)原始視圖旋轉(zhuǎn)45度角度

UIView可以通過(guò)設(shè)置transform屬性做變換,但實(shí)際上它只是封裝了內(nèi)部圖層的變換霞丧。

CALayer同樣也有一個(gè)transform屬性呢岗,但它的類型是CATransform3D,而不是CGAffineTransform蛹尝,本章后續(xù)將會(huì)詳細(xì)解釋后豫。CALayer對(duì)應(yīng)于UIView的transform屬性叫做affineTransform

- (void)transform {
    UIView *catView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    catView.center = self.view.center;
    catView.layer.contents = (__bridge id)[UIImage imageNamed:@"cat"].CGImage;
    [self.view addSubview:catView];
    
    // transform
    CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
    catView.layer.affineTransform = transform;
}
  • 運(yùn)行效果如下
transform.png

C的數(shù)學(xué)函數(shù)庫(kù)(iOS會(huì)自動(dòng)引入)提供了pi的一些簡(jiǎn)便的換算突那,M_PI_4于是就是pi的四分之一挫酿,如果對(duì)換算不太清楚的話,可以用如下的宏做換算:

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

Core Graphics提供了一系列的函數(shù)可以在一個(gè)變換的基礎(chǔ)上做更深層次的變換愕难,如果做一個(gè)既要縮放又要旋轉(zhuǎn)的變換早龟,這就會(huì)非常有用了。例如下面幾個(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);

我們來(lái)用這些函數(shù)組合一個(gè)更加復(fù)雜的變換射窒,先縮小50%藏杖,再旋轉(zhuǎn)30度将塑,最后向右移動(dòng)200個(gè)像素

/// 混合變換
- (void)transformConcat {
    UIView *catView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    catView.center = self.view.center;
    catView.layer.contents = (__bridge id)[UIImage imageNamed:@"cat"].CGImage;
    [self.view addSubview:catView];
    
    // 混合變換
    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);
    // appley transform to layer
    catView.layer.affineTransform = transform;
}
  • 運(yùn)行效果如下
image.png

有些需要注意的地方:圖片向右邊發(fā)生了平移,但并沒(méi)有指定距離那么遠(yuǎn)(200像素)蝌麸,另外它還有點(diǎn)向下發(fā)生了平移点寥。原因在于當(dāng)你按順序做了變換,上一個(gè)變換的結(jié)果將會(huì)影響之后的變換来吩,所以200像素的向右平移同樣也被旋轉(zhuǎn)了30度开财,縮小了50%,所以它實(shí)際上是斜向移動(dòng)了100像素误褪。

這意味著變換的順序會(huì)影響最終的結(jié)果责鳍,也就是說(shuō)旋轉(zhuǎn)之后的平移和平移之后的旋轉(zhuǎn)結(jié)果可能不同。

二 3D變換

CG的前綴告訴我們兽间,CGAffineTransform類型屬于Core Graphics框架历葛,Core Graphics實(shí)際上是一個(gè)嚴(yán)格意義上的2D繪圖API,并且CGAffineTransform僅僅對(duì)2D變換有效嘀略。

前面我們提到了zPosition屬性恤溶,可以用來(lái)讓圖層靠近或者遠(yuǎn)離相機(jī)(用戶視角),transform屬性(CATransform3D類型)可以真正做到這點(diǎn)帜羊,即讓圖層在3D空間內(nèi)移動(dòng)或者旋轉(zhuǎn)咒程。

CGAffineTransform類似,CATransform3D也是一個(gè)矩陣讼育,但是和2x3的矩陣不同帐姻,CATransform3D是一個(gè)可以在3維空間內(nèi)做變換的4x4的矩陣。

對(duì)一個(gè)3D像素點(diǎn)做CATransform3D矩陣變換.png

CGAffineTransform矩陣類似奶段,Core Animation提供了一系列的方法用來(lái)創(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)該對(duì)X軸和Y軸比較熟悉了,分別以右和下為正方向蹲缠,Z軸和這兩個(gè)軸分別垂直棺克,指向視角外為正方向,如下圖所示

X线定,Y娜谊,Z軸,以及圍繞它們旋轉(zhuǎn)的方向.png

由圖所見(jiàn)渔肩,繞Z軸的旋轉(zhuǎn)等同于之前二維空間的仿射旋轉(zhuǎn)因俐,但是繞X軸和Y軸的旋轉(zhuǎn)就突破了屏幕的二維空間拇惋,并且在用戶視角看來(lái)發(fā)生了傾斜周偎。

使用CATransform3DMakeRotation對(duì)視圖內(nèi)的圖層繞Y軸做了45度角的旋轉(zhuǎn)抹剩,我們可以把視圖向右傾斜,這樣會(huì)看得更清晰蓉坎。

- (void)transform3D {
    UIView *catView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    catView.center = self.view.center;
    catView.layer.contents = (__bridge id)[UIImage imageNamed:@"cat"].CGImage;
    [self.view addSubview:catView];
    
    // rotate the layer 45 degrees along the Y axis
    CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
    catView.layer.transform = transform;
}
繞y軸旋轉(zhuǎn)45度的視圖.png

看起來(lái)圖層并沒(méi)有被旋轉(zhuǎn)澳眷,而是僅僅在水平方向上的一個(gè)壓縮,是哪里出了問(wèn)題呢蛉艾?

其實(shí)完全沒(méi)錯(cuò)钳踊,視圖看起來(lái)更窄實(shí)際上是因?yàn)槲覀冊(cè)谟靡粋€(gè)斜向的視角看它,而不是透視勿侯。

2.1 透視投影

在真實(shí)世界中拓瞪,當(dāng)物體遠(yuǎn)離我們的時(shí)候,由于視角的原因看起來(lái)會(huì)變小助琐,理論上說(shuō)遠(yuǎn)離我們的視圖的邊要比靠近視角的邊跟短祭埂,但實(shí)際上并沒(méi)有發(fā)生,而我們當(dāng)前的視角是等距離的兵钮,也就是在3D變換中任然保持平行蛆橡,和之前提到的仿射變換類似。

在等距投影中掘譬,遠(yuǎn)處的物體和近處的物體保持同樣的縮放比例泰演,這種投影也有它自己的用處(例如建筑繪圖,顛倒葱轩,和偽3D視頻)睦焕,但當(dāng)前我們并不需要。

為了做一些修正靴拱,我們需要引入投影變換(又稱作z變換)來(lái)對(duì)除了旋轉(zhuǎn)之外的變換矩陣做一些修改复亏,Core Animation并沒(méi)有給我們提供設(shè)置透視變換的函數(shù),因此我們需要手動(dòng)修改矩陣值缭嫡,幸運(yùn)的是缔御,很簡(jiǎn)單:

CATransform3D的透視效果通過(guò)一個(gè)矩陣中一個(gè)很簡(jiǎn)單的元素來(lái)控制:m34。m34用于按比例縮放X和Y的值來(lái)計(jì)算到底要離視角多遠(yuǎn)妇蛀。

CATransform3D的m34元素耕突,用來(lái)做透視.png

m34的默認(rèn)值是0,我們可以通過(guò)設(shè)置m34為-1.0 / d來(lái)應(yīng)用透視效果评架,d代表了想象中視角相機(jī)和屏幕之間的距離眷茁,以像素為單位,那應(yīng)該如何計(jì)算這個(gè)距離呢纵诞?實(shí)際上并不需要上祈,大概估算一個(gè)就好了。

因?yàn)橐暯窍鄼C(jī)實(shí)際上并不存在,所以可以根據(jù)屏幕上的顯示效果自由決定它的防止的位置。通常500-1000就已經(jīng)很好了亡鼠,但對(duì)于特定的圖層有時(shí)候更小后者更大的值會(huì)看起來(lái)更舒服河劝,減少距離的值會(huì)增強(qiáng)透視效果,所以一個(gè)非常微小的值會(huì)讓它看起來(lái)更加失真皇耗,然而一個(gè)非常大的值會(huì)讓它基本失去透視效果,對(duì)視圖應(yīng)用透視的代碼如下

- (void)transformM34 {
    UIView *catView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    catView.center = self.view.center;
    catView.layer.contents = (__bridge id)[UIImage imageNamed:@"cat"].CGImage;
    [self.view addSubview:catView];
    
    // 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
    catView.layer.transform = transform;
}
應(yīng)用透視效果之后再次對(duì)圖層做旋轉(zhuǎn).png
2.2 滅點(diǎn)

當(dāng)在透視角度繪圖的時(shí)候揍很,遠(yuǎn)離相機(jī)視角的物體將會(huì)變小變遠(yuǎn)郎楼,當(dāng)遠(yuǎn)離到一個(gè)極限距離,它們可能就縮成了一個(gè)點(diǎn)窒悔,于是所有的物體最后都匯聚消失在同一個(gè)點(diǎn)呜袁。

在現(xiàn)實(shí)中,這個(gè)點(diǎn)通常是視圖的中心简珠,于是為了在應(yīng)用中創(chuàng)建擬真效果的透視傅寡,這個(gè)點(diǎn)應(yīng)該聚在屏幕中點(diǎn),或者至少是包含所有3D對(duì)象的視圖中點(diǎn)北救。

滅點(diǎn).png

Core Animation定義了這個(gè)點(diǎn)位于變換圖層的anchorPoint(通常位于圖層中心荐操,但也有例外)。這就是說(shuō)珍策,當(dāng)圖層發(fā)生變換時(shí)托启,這個(gè)點(diǎn)永遠(yuǎn)位于圖層變換之前anchorPoint的位置。

當(dāng)改變一個(gè)圖層的position攘宙,你也改變了它的滅點(diǎn)屯耸,做3D變換的時(shí)候要時(shí)刻記住這一點(diǎn),當(dāng)你視圖通過(guò)調(diào)整m34來(lái)讓它更加有3D效果蹭劈,應(yīng)該首先把它放置于屏幕中央疗绣,然后通過(guò)平移來(lái)把它移動(dòng)到指定位置(而不是直接改變它的position),這樣所有的3D圖層都共享一個(gè)滅點(diǎn)铺韧。

2.3 sublayerTransform屬性

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

CALayer有一個(gè)屬性叫做sublayerTransform立轧。它也是CATransform3D類型格粪,但和對(duì)一個(gè)圖層的變換不同躏吊,它影響到所有的子圖層。這意味著你可以一次性對(duì)包含這些圖層的容器做變換帐萎,于是所有的子圖層都自動(dòng)繼承了這個(gè)變換方法比伏。

相較而言,通過(guò)在一個(gè)地方設(shè)置透視變換會(huì)很方便吓肋,同時(shí)它會(huì)帶來(lái)另一個(gè)顯著的優(yōu)勢(shì):滅點(diǎn)被設(shè)置在容器圖層的中點(diǎn),從而不需要再對(duì)子圖層分別設(shè)置了瑰艘。這意味著你可以隨意使用position和frame來(lái)放置子圖層是鬼,而不需要把它們放置在屏幕中點(diǎn),然后為了保證統(tǒng)一的滅點(diǎn)用變換來(lái)做平移紫新。

- (void)sublayerTransform {
    UIView *catView = [[UIView alloc] initWithFrame:CGRectMake(0, 200, 150, 150)];
    catView.layer.contents = (__bridge id)[UIImage imageNamed:@"cat"].CGImage;
    [self.view addSubview:catView];
    
    UIView *catView1 = [[UIView alloc] initWithFrame:CGRectMake(200, 200, 150, 150)];
    catView1.layer.contents = (__bridge id)[UIImage imageNamed:@"cat"].CGImage;
    [self.view addSubview:catView1];
    
    // apply perspective transform to container
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -1.0 / 500;
    
    self.view.layer.sublayerTransform = perspective;
    
    // rotate layerView1 by 45 degrees along the Y axis
    CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
    catView.layer.transform = transform1;
    
    // rotate layerView2 by 45 degrees along the Y axis
    CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
    catView1.layer.transform = transform2;
}
  • 運(yùn)行結(jié)果
通過(guò)相同的透視效果分別對(duì)視圖做變換.png
2.4 背面

我們既然可以在3D場(chǎng)景下旋轉(zhuǎn)圖層均蜜,那么也可以從背面去觀察它。如果我們把角度修改為M_PI(180度)而不是當(dāng)前的M_PI_4(45度)芒率,那么將會(huì)把圖層完全旋轉(zhuǎn)一個(gè)半圈囤耳,于是完全背對(duì)了相機(jī)視角。

CALayer有一個(gè)叫做doubleSided的屬性來(lái)控制圖層的背面是否要被繪制偶芍。這是一個(gè)BOOL類型充择,默認(rèn)為YES,如果設(shè)置為NO匪蟀,那么當(dāng)圖層正面從相機(jī)視角消失的時(shí)候椎麦,它將不會(huì)被繪制。

- (void)doubleSided {
    UIView *catView = [[UIView alloc] initWithFrame:CGRectMake(0, 200, 150, 150)];
    catView.layer.contents = (__bridge id)[UIImage imageNamed:@"cat"].CGImage;
    [self.view addSubview:catView];
    
    // rotate layerView1 by 45 degrees along the Y axis
    CATransform3D transform1 = CATransform3DMakeRotation(M_PI_2, 0, 1, 0);
    catView.layer.transform = transform1;
    
    catView.layer.doubleSided = NO;
}
  • 運(yùn)行結(jié)果如下
image.png
image.png
image.png
2.5 扁平化圖層

如果對(duì)包含已經(jīng)做過(guò)變換的圖層的圖層做反方向的變換將會(huì)發(fā)什么什么呢材彪?是不是有點(diǎn)困惑观挎?見(jiàn)下圖。

反方向變換的嵌套圖層 .png

注意做了-45度旋轉(zhuǎn)的內(nèi)部圖層是怎樣抵消旋轉(zhuǎn)45度的圖層段化,從而恢復(fù)正常狀態(tài)的嘁捷。

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

繞Z軸做相反的旋轉(zhuǎn)變換代碼如下

- (void)innerOuter {
    UIView *catView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    catView.layer.contents = (__bridge id)[UIImage imageNamed:@"cat"].CGImage;
    catView.center = self.view.center;
    [self.view addSubview:catView];
    
    // rotate the outer layer 45 degrees
    CATransform3D outer = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);
    catView.layer.transform = outer;
    
    UIView *catView1 = [[UIView alloc] initWithFrame:CGRectMake(0, 200, 100, 100)];
    catView1.layer.contents = (__bridge id)[UIImage imageNamed:@"cat"].CGImage;
    catView1.center = self.view.center;
    [self.view addSubview:catView1];
    
    // rotate the inner layer -45 degrees
    CATransform3D inner = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1);
    catView1.layer.transform = inner;
}
  • 運(yùn)行結(jié)果如下
旋轉(zhuǎn)后的視圖.png

運(yùn)行結(jié)果和我們預(yù)期的一致⌒巯現(xiàn)在在3D情況下再試一次。修改代碼喘蟆,讓內(nèi)外兩個(gè)視圖繞Y軸旋轉(zhuǎn)而不是Z軸现诀,再加上透視效果,以便我們觀察履肃。注意不能用sublayerTransform屬性仔沿,因?yàn)閮?nèi)部的圖層并不直接是容器圖層的子圖層,所以這里分別對(duì)圖層設(shè)置透視變換尺棋。

- (void)innerOuterY {
    UIView *catView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    catView.layer.contents = (__bridge id)[UIImage imageNamed:@"cat"].CGImage;
    catView.center = self.view.center;
    [self.view addSubview:catView];
    
    // rotate the outer layer 45 degrees
    CATransform3D outer = CATransform3DIdentity;
    outer.m34 = -1.0 / 500.0;
    outer = CATransform3DRotate(outer, M_PI_4, 0, 1, 0);
    catView.layer.transform = outer;
    
    UIView *catView1 = [[UIView alloc] initWithFrame:CGRectMake(0, 200, 100, 100)];
    catView1.layer.contents = (__bridge id)[UIImage imageNamed:@"cat"].CGImage;
    catView1.center = self.view.center;
    [self.view addSubview:catView1];
    
    // rotate the inner layer -45 degrees
    CATransform3D inner = CATransform3DIdentity;
    inner.m34 = -1.0 / 500.0;
    inner = CATransform3DRotate(inner, -M_PI_4, 0, 1, 0);
    catView1.layer.transform = inner;
}
  • 運(yùn)行結(jié)果如下
image.png

預(yù)期的效果如下圖所示

繞Y軸做相反旋轉(zhuǎn)的預(yù)期結(jié)果.png

但其實(shí)這并不是我們所看到的封锉,相反绵跷,我們看到的結(jié)果如下圖所示。發(fā)什么了什么呢成福??jī)?nèi)部的圖層仍然向左側(cè)旋轉(zhuǎn)碾局,并且發(fā)生了扭曲,但按道理說(shuō)它應(yīng)該保持正面朝上奴艾,并且顯示正常的方塊净当。

繞Y軸做相反旋轉(zhuǎn)的真實(shí)結(jié)果.png

這是由于盡管Core Animation圖層存在于3D空間之內(nèi),但它們并不都存在同一個(gè)3D空間蕴潦。每個(gè)圖層的3D場(chǎng)景其實(shí)是扁平化的像啼,當(dāng)你從正面觀察一個(gè)圖層,看到的實(shí)際上由子圖層創(chuàng)建的想象出來(lái)的3D場(chǎng)景潭苞,但當(dāng)你傾斜這個(gè)圖層忽冻,你會(huì)發(fā)現(xiàn)實(shí)際上這個(gè)3D場(chǎng)景僅僅是被繪制在圖層的表面

類似的,當(dāng)你在玩一個(gè)3D游戲此疹,實(shí)際上僅僅是把屏幕做了一次傾斜僧诚,或許在游戲中可以看見(jiàn)有一面墻在你面前,但是傾斜屏幕并不能夠看見(jiàn)墻里面的東西蝗碎。所有場(chǎng)景里面繪制的東西并不會(huì)隨著你觀察它的角度改變而發(fā)生變化湖笨;圖層也是同樣的道理。

這使得用Core Animation創(chuàng)建非常復(fù)雜的3D場(chǎng)景變得十分困難蹦骑。你不能夠使用圖層樹(shù)去創(chuàng)建一個(gè)3D結(jié)構(gòu)的層級(jí)關(guān)系--在相同場(chǎng)景下的任何3D表面必須和同樣的圖層保持一致赶么,這是因?yàn)槊總€(gè)的父視圖都把它的子視圖扁平化了。

至少當(dāng)你用正常的CALayer的時(shí)候是這樣脊串,CALayer有一個(gè)叫做CATransformLayer的子類來(lái)解決這個(gè)問(wèn)題辫呻。后面會(huì)討論。

三 固體對(duì)象

現(xiàn)在你懂得了在3D空間的一些圖層布局的基礎(chǔ)琼锋,我們來(lái)試著創(chuàng)建一個(gè)固態(tài)的3D對(duì)象(實(shí)際上是一個(gè)技術(shù)上所謂的空洞對(duì)象放闺,但它以固態(tài)呈現(xiàn))。我們用六個(gè)獨(dú)立的視圖來(lái)構(gòu)建一個(gè)立方體的各個(gè)面缕坎。

image.png

創(chuàng)建一個(gè)立方體

/// 創(chuàng)建一個(gè)立方體
- (void)createCube {
    [self addCubeView];
    
    // set up the container sublayer transform
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -1.0 / 500.0;
    self.view.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_2, 0, 1, 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 = self.faces[index];
    [self.view addSubview:face];
    
    // center the face view within the container
    CGSize containerSize = self.view.bounds.size;
    face.center = CGPointMake(containerSize.width * 0.5, containerSize.height * 0.5);
    // apply the transform
    face.layer.transform = transform;
}

- (void)addCubeView {
    self.faces = [NSMutableArray array];
    
    for (int i = 0; i < 6; i++) {
        UIView *cubeView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
        cubeView.layer.contents = (__bridge id)[UIImage imageNamed:[NSString stringWithFormat:@"%d",i + 1]].CGImage;
        [self.faces addObject:cubeView];
    }
}
  • 運(yùn)行結(jié)果如下
正面朝上的立方體.png

從這個(gè)角度看立方體并不是很明顯怖侦;看起來(lái)只是一個(gè)方塊,為了更好地欣賞它谜叹,我們將更換一個(gè)不同的視角匾寝。

旋轉(zhuǎn)這個(gè)立方體將會(huì)顯得很笨重,因?yàn)槲覀円獑为?dú)對(duì)每個(gè)面做旋轉(zhuǎn)荷腊。另一個(gè)簡(jiǎn)單的方案是通過(guò)調(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);

這就對(duì)相機(jī)(或者相對(duì)相機(jī)的整個(gè)場(chǎng)景,你也可以這么認(rèn)為)繞Y軸旋轉(zhuǎn)45度女仰,并且繞X軸旋轉(zhuǎn)45度〔履辏現(xiàn)在從另一個(gè)角度去觀察立方體抡锈,就能看出它的真實(shí)面貌

  • 運(yùn)行結(jié)果如下
從一個(gè)邊角觀察的立方體.png
3.2 光亮和陰影

現(xiàn)在它看起來(lái)更像是一個(gè)立方體沒(méi)錯(cuò)了,但是對(duì)每個(gè)面之間的連接還是很難分辨乔外。Core Animation可以用3D顯示圖層床三,但是它對(duì)光線并沒(méi)有概念。如果想讓立方體看起來(lái)更加真實(shí)杨幼,需要自己做一個(gè)陰影效果撇簿。你可以通過(guò)改變每個(gè)面的背景顏色或者直接用帶光亮效果的圖片來(lái)調(diào)整。

如果需要?jiǎng)討B(tài)地創(chuàng)建光線效果差购,你可以根據(jù)每個(gè)視圖的方向應(yīng)用不同的alpha值做出半透明的陰影圖層四瘫,但為了計(jì)算陰影圖層的不透明度,你需要得到每個(gè)面的正太向量(垂直于表面的向量)歹撒,然后根據(jù)一個(gè)想象的光源計(jì)算出兩個(gè)向量叉乘結(jié)果莲组。叉乘代表了光源和圖層之間的角度诊胞,從而決定了它有多大程度上的光亮暖夭。

下面實(shí)現(xiàn)了這樣一個(gè)結(jié)果,我們用GLKit框架來(lái)做向量的計(jì)算(你需要引入GLKit庫(kù)來(lái)運(yùn)行代碼)撵孤,每個(gè)面的CATransform3D都被轉(zhuǎn)換成GLKMatrix4迈着,然后通過(guò)GLKMatrix4GetMatrix3函數(shù)得出一個(gè)3×3的旋轉(zhuǎn)矩陣。這個(gè)旋轉(zhuǎn)矩陣指定了圖層的方向邪码,然后可以用它來(lái)得到正太向量的值裕菠。

結(jié)果如下圖所示,試著調(diào)整LIGHT_DIRECTIONAMBIENT_LIGHT的值來(lái)切換光線效果闭专。

#import <GLKit/GLKMatrix4.h>
#import <GLKit/GLKMatrix3.h>

#define LIGHT_DIRECTION 0,1,-0.5
#define AMBIENT_LIGHT 0.5

#pragma mark - 光亮和陰影

- (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)類型有長(zhǎng)度區(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;
}
  • 運(yùn)行結(jié)果如下
image.png
3.3 點(diǎn)擊事件

你應(yīng)該能注意到現(xiàn)在可以在第三個(gè)表面的頂部看見(jiàn)按鈕了画髓,點(diǎn)擊它,什么都沒(méi)發(fā)生平委,為什么呢奈虾?

這并不是因?yàn)閕OS在3D場(chǎng)景下正確地處理響應(yīng)事件,實(shí)際上是可以做到的廉赔。問(wèn)題在于視圖順序肉微。在前面中我們簡(jiǎn)要提到過(guò),點(diǎn)擊事件的處理由視圖父視圖中的順序決定的蜡塌,并不是3D空間中的Z軸順序碉纳。當(dāng)給立方體添加視圖的時(shí)候,我們實(shí)際上是按照一個(gè)順序添加馏艾,所以按照視圖/圖層順序來(lái)說(shuō)村象,4笆环,5,6在3的前面厚者。

即使我們看不見(jiàn)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è)問(wèn)題馆蠕,因?yàn)樗辉黉秩疽晥D后面的內(nèi)容期升,但實(shí)際上并不起作用。因?yàn)楸硨?duì)相機(jī)而隱藏的視圖仍然會(huì)響應(yīng)點(diǎn)擊事件(這和通過(guò)設(shè)置hidden屬性或者設(shè)置alpha為0而隱藏的視圖不同互躬,那兩種方式將不會(huì)響應(yīng)事件)播赁。所以即使禁止了雙面渲染仍然不能解決這個(gè)問(wèn)題(雖然由于性能問(wèn)題,還是需要把它設(shè)置成NO)吼渡。

這里有幾種正確的方案:把除了表面3的其他視圖userInteractionEnabled屬性都設(shè)置成NO來(lái)禁止事件傳遞容为。或者簡(jiǎn)單通過(guò)代碼把視圖3覆蓋在視圖6上寺酪。無(wú)論怎樣都可以點(diǎn)擊按鈕了坎背。

image.png
四 總結(jié)

這一章涉及了一些2D3D的變換。你學(xué)習(xí)了一些矩陣計(jì)算的基礎(chǔ)寄雀,以及如何用Core Animation創(chuàng)建3D場(chǎng)景得滤。你看到了圖層背后到底是如何呈現(xiàn)的,并且知道了不能把扁平的圖片做成真實(shí)的立體效果咙俩,最后我們用demo說(shuō)明了觸摸事件的處理耿戚,視圖中圖層添加的層級(jí)順序會(huì)比屏幕上顯示的順序更有意義。


本文摘自 iOS核心動(dòng)畫(huà)高級(jí)技巧 - 變換


項(xiàng)目鏈接地址 - AnimateConversion_4


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末阿趁,一起剝皮案震驚了整個(gè)濱河市膜蛔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌脖阵,老刑警劉巖皂股,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異命黔,居然都是意外死亡呜呐,警方通過(guò)查閱死者的電腦和手機(jī)就斤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蘑辑,“玉大人洋机,你說(shuō)我怎么就攤上這事⊙蠡辏” “怎么了绷旗?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)副砍。 經(jīng)常有香客問(wèn)我衔肢,道長(zhǎng),這世上最難降的妖魔是什么豁翎? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任角骤,我火速辦了婚禮,結(jié)果婚禮上心剥,老公的妹妹穿的比我還像新娘邦尊。我一直安慰自己,他們只是感情好刘陶,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布胳赌。 她就那樣靜靜地躺著牢撼,像睡著了一般匙隔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上熏版,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天纷责,我揣著相機(jī)與錄音,去河邊找鬼撼短。 笑死再膳,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的曲横。 我是一名探鬼主播喂柒,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼禾嫉!你這毒婦竟也來(lái)了灾杰?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤熙参,失蹤者是張志新(化名)和其女友劉穎艳吠,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體孽椰,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡昭娩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年凛篙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片栏渺。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡呛梆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出磕诊,到底是詐尸還是另有隱情削彬,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布秀仲,位于F島的核電站融痛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏神僵。R本人自食惡果不足惜雁刷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望保礼。 院中可真熱鬧沛励,春花似錦、人聲如沸炮障。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)胁赢。三九已至企蹭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間智末,已是汗流浹背谅摄。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留系馆,地道東北人送漠。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像由蘑,于是被迫代替她去往敵國(guó)和親闽寡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容