圓角
圓角矩形是iOS的一個(gè)標(biāo)志性審美特性。這在iOS的每一個(gè)地方都得到了體現(xiàn),不論是主屏幕圖標(biāo),還是警告彈框,甚至是文本框。按照這流行程度,你可能會(huì)認(rèn)為一定有不借助photoshop就能輕易創(chuàng)建圓角舉行的方法。恭喜你,猜對(duì)了。
CALayer有一個(gè)叫做conrnerRadius的屬性控制著圖層角的曲率。它是一個(gè)浮點(diǎn)數(shù),默認(rèn)為0(為0的時(shí)候就是直角),但是你可以把它設(shè)置成任意值。默認(rèn)情況下,這個(gè)曲率值只影響背景顏色而不影響背景圖片或是子圖層。不過,如果把masksToBounds設(shè)置成YES的話,圖層里面的所有東西都會(huì)被截取。
我們可以通過一個(gè)簡(jiǎn)單的項(xiàng)目來演示這個(gè)效果。在Interface Builder中,我們放置一些視圖,他們有一些子視圖。而且這些子視圖有一些超出了邊界(如圖)。你可能無法看到他們超出了邊界,因?yàn)樵诰庉嫿缑娴臅r(shí)候,超出的部分總是被Interface Builder裁切掉了。)

然后在代碼中,我們?cè)O(shè)置角的半徑為20個(gè)點(diǎn),并裁剪掉第一個(gè)視圖的超出部分(見清單)。技術(shù)上來說,這些屬性都可以在Interface Builder的探測(cè)板中分別通過『用戶定義運(yùn)行時(shí)屬性』和勾選『裁剪子視圖』(Clip Subviews)選擇框來直接設(shè)置屬性的值。不過,在這個(gè)示例中,代碼能夠表示得更清楚。圖下是運(yùn)行代碼的結(jié)果
@interface ViewController ()@PRoperty (nonatomic, weak) IBOutlet UIView *layerView1;@property (nonatomic, weak) IBOutlet UIView *layerView2;@end@implementation ViewController- (void)viewDidLoad{??? [super viewDidLoad]; //set the corner radius on our layers self.layerView1.layer.cornerRadius = 20.0f; self.layerView2.layer.cornerRadius = 20.0f; //enable clipping on the second layer self.layerView2.layer.masksToBounds = YES;}@end

右圖中,紅色的子視圖沿角半徑被裁剪了
如你所見,右邊的子視圖沿邊界被裁剪了。
單獨(dú)控制每個(gè)層的圓角曲率也不是不可能的。如果想創(chuàng)建有些圓角有些直角的圖層或視圖時(shí),你可能需要一些不同的方法。比如使用一個(gè)圖層蒙板(稍后會(huì)講到)或者是CAShapeLayer。
圖層邊框
CALayer另外兩個(gè)非常有用屬性就是borderWidth和borderColor。二者共同定義了圖層邊的繪制樣式。這條線(也被稱作stroke)沿著圖層的bounds繪制,同時(shí)也包含圖層的角。
borderWidth是以點(diǎn)為單位的定義邊框粗細(xì)的浮點(diǎn)數(shù),默認(rèn)為0.borderColor定義了邊框的顏色,默認(rèn)為黑色。
borderColor是CGColorRef類型,而不是UIColor,所以它不是Cocoa的內(nèi)置對(duì)象。不過呢,你肯定也清楚圖層引用了borderColor,雖然屬性聲明并不能證明這一點(diǎn)。CGColorRef在引用/釋放時(shí)候的行為表現(xiàn)得與NSObject極其相似。但是Objective-C語法并不支持這一做法,所以CGColorRef屬性即便是強(qiáng)引用也只能通過assign關(guān)鍵字來聲明。
邊框是繪制在圖層邊界里面的,而且在所有子內(nèi)容之前,也在子圖層之前。如果我們?cè)谥暗氖纠校ㄇ鍐危┘尤雸D層的邊框,你就能看到到底是怎么一回事了(如圖).
@implementation ViewController- (void)viewDidLoad{ [super viewDidLoad]; //set the corner radius on our layers self.layerView1.layer.cornerRadius = 20.0f; self.layerView2.layer.cornerRadius = 20.0f; //add a border to our layers self.layerView1.layer.borderWidth = 5.0f; self.layerView2.layer.borderWidth = 5.0f; //enable clipping on the second layer self.layerView2.layer.masksToBounds = YES;}@end

圖4.3 給圖層增加一個(gè)邊框
仔細(xì)觀察會(huì)發(fā)現(xiàn)邊框并不會(huì)把寄宿圖或子圖層的形狀計(jì)算進(jìn)來,如果圖層的子圖層超過了邊界,或者是寄宿圖在透明區(qū)域有一個(gè)透明蒙板,邊框仍然會(huì)沿著圖層的邊界繪制出來(如圖4.4).

圖4.4 邊框是跟隨圖層的邊界變化的,而不是圖層里面的內(nèi)容
陰影
iOS的另一個(gè)常見特性呢,就是陰影。陰影往往可以達(dá)到圖層深度暗示的效果。也能夠用來強(qiáng)調(diào)正在顯示的圖層和優(yōu)先級(jí)(比如說一個(gè)在其他視圖之前的彈出框),不過有時(shí)候他們只是單純的裝飾目的。
給shadowOpacity屬性一個(gè)大于默認(rèn)值(也就是0)的值,陰影就可以顯示在任意圖層之下。shadowOpacity是一個(gè)必須在0.0(不可見)和1.0(完全不透明)之間的浮點(diǎn)數(shù)。如果設(shè)置為1.0,將會(huì)顯示一個(gè)有輕微模糊的黑色陰影稍微在圖層之上。若要改動(dòng)陰影的表現(xiàn),你可以使用CALayer的另外三個(gè)屬性:shadowColor,shadowOffset和shadowRadius。
顯而易見,shadowColor屬性控制著陰影的顏色,和borderColor和backgroundColor一樣,它的類型也是CGColorRef。陰影默認(rèn)是黑色,大多數(shù)時(shí)候你需要的陰影也是黑色的(其他顏色的陰影看起來是不是有一點(diǎn)點(diǎn)奇怪。。)。
shadowOffset屬性控制著陰影的方向和距離。它是一個(gè)CGSize的值,寬度控制這陰影橫向的位移,高度控制著縱向的位移。shadowOffset的默認(rèn)值是 {0, -3},意即陰影相對(duì)于Y軸有3個(gè)點(diǎn)的向上位移。
為什么要默認(rèn)向上的陰影呢?盡管Core Animation是從圖層套裝演變而來(可以認(rèn)為是為iOS創(chuàng)建的私有動(dòng)畫框架),但是呢,它卻是在Mac OS上面世的,前面有提到,二者的Y軸是顛倒的。這就導(dǎo)致了默認(rèn)的3個(gè)點(diǎn)位移的陰影是向上的。在Mac上,shadowOffset的默認(rèn)值是陰影向下的,這樣你就能理解為什么iOS上的陰影方向是向上的了(如圖4.5).

圖4.5 在iOS(左)和Mac OS(右)上shadowOffset的表現(xiàn)。
蘋果更傾向于用戶界面的陰影應(yīng)該是垂直向下的,所以在iOS把陰影寬度設(shè)為0,然后高度設(shè)為一個(gè)正值不失為一個(gè)做法。
shadowRadius屬性控制著陰影的模糊度,當(dāng)它的值是0的時(shí)候,陰影就和視圖一樣有一個(gè)非常確定的邊界線。當(dāng)值越來越大的時(shí)候,邊界線看上去就會(huì)越來越模糊和自然。蘋果自家的應(yīng)用設(shè)計(jì)更偏向于自然的陰影,所以一個(gè)非零值再合適不過了。
通常來講,如果你想讓視圖或控件非常醒目獨(dú)立于背景之外(比如彈出框遮罩層),你就應(yīng)該給shadowRadius設(shè)置一個(gè)稍大的值。陰影越模糊,圖層的深度看上去就會(huì)更明顯(如圖4.6).

圖4.6 大一些的陰影位移和角半徑會(huì)增加圖層的深度即視感
陰影裁剪
和圖層邊框不同,圖層的陰影繼承自內(nèi)容的外形,而不是根據(jù)邊界和角半徑來確定。為了計(jì)算出陰影的形狀,Core Animation會(huì)將寄宿圖(包括子視圖,如果有的話)考慮在內(nèi),然后通過這些來完美搭配圖層形狀從而創(chuàng)建一個(gè)陰影(見圖4.7)。

圖4.7 陰影是根據(jù)寄宿圖的輪廓來確定的
當(dāng)陰影和裁剪扯上關(guān)系的時(shí)候就有一個(gè)頭疼的限制:陰影通常就是在Layer的邊界之外,如果你開啟了masksToBounds屬性,所有從圖層中突出來的內(nèi)容都會(huì)被才剪掉。如果我們?cè)谖覀冎暗倪吙蚴纠?xiàng)目中增加圖層的陰影屬性時(shí),你就會(huì)發(fā)現(xiàn)問題所在(見圖4.8).

圖4.8 maskToBounds屬性裁剪掉了陰影和內(nèi)容
從技術(shù)角度來說,這個(gè)結(jié)果是可以是可以理解的,但確實(shí)又不是我們想要的效果。如果你想沿著內(nèi)容裁切,你需要用到兩個(gè)圖層:一個(gè)只畫陰影的空的外圖層,和一個(gè)用masksToBounds裁剪內(nèi)容的內(nèi)圖層。
如果我們把之前項(xiàng)目的右邊用單獨(dú)的視圖把裁剪的視圖包起來,我們就可以解決這個(gè)問題(如圖4.9).

@interface ViewController ()@property (nonatomic, weak) IBOutlet UIView *layerView1;@property (nonatomic, weak) IBOutlet UIView *layerView2;@property (nonatomic, weak) IBOutlet UIView *shadowView;@end@implementation ViewController?- (void)viewDidLoad{ [super viewDidLoad]; //set the corner radius on our layers self.layerView1.layer.cornerRadius = 20.0f; self.layerView2.layer.cornerRadius = 20.0f; //add a border to our layers self.layerView1.layer.borderWidth = 5.0f; self.layerView2.layer.borderWidth = 5.0f; //add a shadow to layerView1 self.layerView1.layer.shadowOpacity = 0.5f; self.layerView1.layer.shadowOffset = CGSizeMake(0.0f, 5.0f); self.layerView1.layer.shadowRadius = 5.0f; //add same shadow to shadowView (not layerView2) self.shadowView.layer.shadowOpacity = 0.5f; self.shadowView.layer.shadowOffset = CGSizeMake(0.0f, 5.0f); self.shadowView.layer.shadowRadius = 5.0f; //enable clipping on the second layer self.layerView2.layer.masksToBounds = YES;}@end

圖4.10 右邊視圖,不受裁切陰影的陰影視圖。
shadowPath屬性
我們已經(jīng)知道圖層陰影并不總是方的,而是從圖層內(nèi)容的形狀繼承而來。這看上去不錯(cuò),但是實(shí)時(shí)計(jì)算陰影也是一個(gè)非常消耗資源的,尤其是圖層有多個(gè)子圖層,每個(gè)圖層還有一個(gè)有透明效果的寄宿圖的時(shí)候。
如果你事先知道你的陰影形狀會(huì)是什么樣子的,你可以通過指定一個(gè)shadowPath來提高性能。shadowPath是一個(gè)CGPathRef類型(一個(gè)指向CGPath的指針)。CGPath是一個(gè)Core Graphics對(duì)象,用來指定任意的一個(gè)矢量圖形。我們可以通過這個(gè)屬性單獨(dú)于圖層形狀之外指定陰影的形狀。
圖4.11 展示了同一寄宿圖的不同陰影設(shè)定。如你所見,我們使用的圖形很簡(jiǎn)單,但是它的陰影可以是你想要的任何形狀。清單4.4是代碼實(shí)現(xiàn)。

@interface ViewController ()@property (nonatomic, weak) IBOutlet UIView *layerView1;@property (nonatomic, weak) IBOutlet UIView *layerView2;@end@implementation ViewController- (void)viewDidLoad{ [super viewDidLoad]; //enable layer shadows self.layerView1.layer.shadowOpacity = 0.5f; self.layerView2.layer.shadowOpacity = 0.5f; //create a square shadow CGMutablePathRef squarePath = CGPathCreateMutable(); CGPathAddRect(squarePath, NULL, self.layerView1.bounds); self.layerView1.layer.shadowPath = squarePath; CGPathRelease(squarePath); ?//create a circular shadow CGMutablePathRef circlePath = CGPathCreateMutable(); CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds); self.layerView2.layer.shadowPath = circlePath; CGPathRelease(circlePath);}@end
如果是一個(gè)舉行或是圓,用CGPath會(huì)相當(dāng)簡(jiǎn)單明了。但是如果是更加復(fù)雜一點(diǎn)的圖形,UIBezierPath類會(huì)更合適,它是一個(gè)由UIKit提供的在CGPath基礎(chǔ)上的Objective-C包裝類。
圖層蒙板
通過masksToBounds屬性,我們可以沿邊界裁剪圖形;通過cornerRadius屬性,我們還可以設(shè)定一個(gè)圓角。但是有時(shí)候你希望展現(xiàn)的內(nèi)容不是在一個(gè)矩形或圓角矩形。比如,你想展示一個(gè)有星形框架的圖片,又或者想讓一些古卷文字慢慢漸變成背景色,而不是一個(gè)突兀的邊界。
使用一個(gè)32位有alpha通道的png圖片通常是創(chuàng)建一個(gè)無矩形視圖最方便的方法,你可以給它指定一個(gè)透明蒙板來實(shí)現(xiàn)。但是這個(gè)方法不能讓你以編碼的方式動(dòng)態(tài)地生成蒙板,也不能讓子圖層或子視圖裁剪成同樣的形狀。
CALayer有一個(gè)屬性叫做mask可以解決這個(gè)問題。這個(gè)屬性本身就是個(gè)CALayer類型,有和其他圖層一樣的繪制和布局屬性。它類似于一個(gè)子圖層,相對(duì)于父圖層(即擁有該屬性的圖層)布局,但是它卻不是一個(gè)普通的子圖層。不同于那些繪制在父圖層中的子圖層,mask圖層定義了父圖層的部分可見區(qū)域。
mask圖層的Color屬性是無關(guān)緊要的,真正重要的是圖層的輪廓。mask屬性就像是一個(gè)餅干切割機(jī),mask圖層實(shí)心的部分會(huì)被保留下來,其他的則會(huì)被拋棄。(如圖4.12)
如果mask圖層比父圖層要小,只有在mask圖層里面的內(nèi)容才是它關(guān)心的,除此以外的一切都會(huì)被隱藏起來。

我們將代碼演示一下這個(gè)過程,創(chuàng)建一個(gè)簡(jiǎn)單的項(xiàng)目,通過圖層的mask屬性來作用于圖片之上。為了簡(jiǎn)便一些,我們用Interface Builder來創(chuàng)建一個(gè)包含UIImageView的圖片圖層。這樣我們就只要代碼實(shí)現(xiàn)蒙板圖層了。清單4.5是最終的代碼,圖4.13是運(yùn)行后的結(jié)果。
@interface ViewController ()@property (nonatomic, weak) IBOutlet UIImageView *imageView;@end@implementation ViewController- (void)viewDidLoad{ [super viewDidLoad]; //create mask layer CALayer *maskLayer = [CALayer layer]; maskLayer.frame = self.layerView.bounds; UIImage *maskImage = [UIImage imageNamed:@"Cone.png"]; maskLayer.contents = (__bridge id)maskImage.CGImage; //apply mask to image layer? self.imageView.layer.mask = maskLayer;}@end

圖4.13 使用了mask之后的UIImageView
CALayer蒙板圖層真正厲害的地方在于蒙板圖不局限于靜態(tài)圖。任何有圖層構(gòu)成的都可以作為mask屬性,這意味著你的蒙板可以通過代碼甚至是動(dòng)畫實(shí)時(shí)生成。
拉伸過濾
最后我們?cè)賮碚務(wù)刴inificationFilter和magnificationFilter屬性。總得來講,當(dāng)我們視圖顯示一個(gè)圖片的時(shí)候,都應(yīng)該正確地顯示這個(gè)圖片(意即:以正確的比例和正確的1:1像素顯示在屏幕上)。原因如下:
能夠顯示最好的畫質(zhì),像素既沒有被壓縮也沒有被拉伸。
能更好的使用內(nèi)存,因?yàn)檫@就是所有你要存儲(chǔ)的東西。
最好的性能表現(xiàn),CPU不需要為此額外的計(jì)算。
不過有時(shí)候,顯示一個(gè)非真實(shí)大小的圖片確實(shí)是我們需要的效果。比如說一個(gè)頭像或是圖片的縮略圖,再比如說一個(gè)可以被拖拽和伸縮的大圖。這些情況下,為同一圖片的不同大小存儲(chǔ)不同的圖片顯得又不切實(shí)際。
當(dāng)圖片需要顯示不同的大小的時(shí)候,有一種叫做拉伸過濾的算法就起到作用了。它作用于原圖的像素上并根據(jù)需要生成新的像素顯示在屏幕上。
事實(shí)上,重繪圖片大小也沒有一個(gè)統(tǒng)一的通用算法。這取決于需要拉伸的內(nèi)容,放大或是縮小的需求等這些因素。CALayer為此提供了三種拉伸過濾方法,他們是:
kCAFilterLinear
kCAFilterNearest
kCAFilterTrilinear
minification(縮小圖片)和magnification(放大圖片)默認(rèn)的過濾器都是kCAFilterLinear,這個(gè)過濾器采用雙線性濾波算法,它在大多數(shù)情況下都表現(xiàn)良好。雙線性濾波算法通過對(duì)多個(gè)像素取樣最終生成新的值,得到一個(gè)平滑的表現(xiàn)不錯(cuò)的拉伸。但是當(dāng)放大倍數(shù)比較大的時(shí)候圖片就模糊不清了。
kCAFilterTrilinear和kCAFilterLinear非常相似,大部分情況下二者都看不出來有什么差別。但是,較雙線性濾波算法而言,三線性濾波算法存儲(chǔ)了多個(gè)大小情況下的圖片(也叫多重貼圖),并三維取樣,同時(shí)結(jié)合大圖和小圖的存儲(chǔ)進(jìn)而得到最后的結(jié)果。
這個(gè)方法的好處在于算法能夠從一系列已經(jīng)接近于最終大小的圖片中得到想要的結(jié)果,也就是說不要對(duì)很多像素同步取樣。這不僅提高了性能,也避免了小概率因舍入錯(cuò)誤引起的取樣失靈的問題

圖4.14 對(duì)于大圖來說,雙線性濾波和三線性濾波表現(xiàn)得更出色
kCAFilterNearest是一種比較武斷的方法。從名字不難看出,這個(gè)算法(也叫最近過濾)就是取最近的單像素點(diǎn)而不管其他的顏色。這樣做非常快,也不會(huì)使圖片模糊。但是,最明顯的效果就是,會(huì)使得壓縮圖片更糟,圖片放大之后也顯得塊狀或是馬賽克嚴(yán)重。

組透明
UIView有一個(gè)叫做alpha的屬性來確定視圖的透明度。CALayer有一個(gè)等同的屬性叫做opacity,這兩個(gè)屬性都是影響子層級(jí)的。也就是說,如果你給一個(gè)圖層設(shè)置了opacity屬性,那它的子圖層都會(huì)受此影響。
iOS常見的做法是把一個(gè)空間的alpha值設(shè)置為0.5(50%)以使其看上去呈現(xiàn)為不可用狀態(tài)。對(duì)于獨(dú)立的視圖來說還不錯(cuò),但是當(dāng)一個(gè)控件有子視圖的時(shí)候就有點(diǎn)奇怪了,圖4.20展示了一個(gè)內(nèi)嵌了UILabel的自定義UIButton;左邊是一個(gè)不透明的按鈕,右邊是50%透明度的相同按鈕。我們可以注意到,里面的標(biāo)簽的輪廓跟按鈕的背景很不搭調(diào)。

圖4.20 右邊的漸隱按鈕中,里面的標(biāo)簽清晰可見
這是由透明度的混合疊加造成的,當(dāng)你顯示一個(gè)50%透明度的圖層時(shí),圖層的每個(gè)像素都會(huì)一般顯示自己的顏色,另一半顯示圖層下面的顏色。這是正常的透明度的表現(xiàn)。但是如果圖層包含一個(gè)同樣顯示50%透明的子圖層時(shí),你所看到的視圖,50%來自子視圖,25%來了圖層本身的顏色,另外的25%則來自背景色。
在我們的示例中,按鈕和表情都是白色背景。雖然他們都死50%的可見度,但是合起來的可見度是75%,所以標(biāo)簽所在的區(qū)域看上去就沒有周圍的部分那么透明。所以看上去子視圖就高粱了,使得這個(gè)顯示效果都糟透了。
理想狀況下,當(dāng)你設(shè)置了一個(gè)圖層的透明度,你希望它包含的整個(gè)圖層樹像一個(gè)整體一樣的透明效果。你可以通過設(shè)置Info.plist文件中的UIViewGroupOpacity為YES來達(dá)到這個(gè)效果,但是這個(gè)設(shè)置會(huì)影響到這個(gè)應(yīng)用,整個(gè)app可能會(huì)受到不良影響。如果UIViewGroupOpacity并未設(shè)置,iOS 6和以前的版本會(huì)默認(rèn)為NO(也許以后的版本會(huì)有一些改變)。
另一個(gè)方法就是,你可以設(shè)置CALayer的一個(gè)叫做shouldRasterize屬性(見清單4.7)來實(shí)現(xiàn)組透明的效果,如果它被設(shè)置為YES,在應(yīng)用透明度之前,圖層及其子圖層都會(huì)被整合成一個(gè)整體的圖片,這樣就沒有透明度混合的問題了(如圖4.21)。
為了啟用shouldRasterize屬性,我們?cè)O(shè)置了圖層的rasterizationScale屬性。默認(rèn)情況下,所有圖層拉伸都是1.0, 所以如果你使用了shouldRasterize屬性,你就要確保你設(shè)置了rasterizationScale屬性去匹配屏幕,以防止出現(xiàn)Retina屏幕像素化的問題。
當(dāng)shouldRasterize和UIViewGroupOpacity一起的時(shí)候,性能問題就出現(xiàn)了。
清單4.7 使用shouldRasterize屬性解決組透明問題
@interface ViewController ()@property (nonatomic, weak) IBOutlet UIView *containerView;@end@implementation ViewController- (UIButton *)customButton{ //create button CGRect frame = CGRectMake(0, 0, 150, 50); UIButton *button = [[UIButton alloc] initWithFrame:frame]; button.backgroundColor = [UIColor whiteColor]; button.layer.cornerRadius = 10; //add label frame = CGRectMake(20, 10, 110, 30); UILabel *label = [[UILabel alloc] initWithFrame:frame]; label.text = @"Hello World"; label.textAlignment = NSTextAlignmentCenter; [button addSubview:label]; return button;}- (void)viewDidLoad{ [super viewDidLoad]; //create opaque button UIButton *button1 = [self customButton]; button1.center = CGPointMake(50, 150); [self.containerView addSubview:button1]; //create translucent button UIButton *button2 = [self customButton]; ? button2.center = CGPointMake(250, 150); button2.alpha = 0.5; [self.containerView addSubview:button2]; //enable rasterization for the translucent button button2.layer.shouldRasterize = YES; button2.layer.rasterizationScale = [UIScreen mainScreen].scale;}@end

圖4.21 修正后的圖
總結(jié)
這一章介紹了一些可以通過代碼應(yīng)用到圖層上的視覺效果,比如圓角,陰影和蒙板。我們也了解了拉伸過濾器和組透明。
變換
仿射變換
在前面文章中,我們使用了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矩陣的每一行對(duì)應(yīng)元素相乘再求和,就形成了一個(gè)新的CGPoint類型的結(jié)果。要解釋一下圖中顯示的灰色元素,為了能讓矩陣做乘法,左邊矩陣的列數(shù)一定要和右邊矩陣的行數(shù)個(gè)數(shù)相同,所以要給矩陣填充一些標(biāo)志值,使得既可以讓矩陣做乘法,又不改變運(yùn)算結(jié)果,并且沒必要存儲(chǔ)這些添加的值,因?yàn)樗鼈兊闹挡粫?huì)發(fā)生變化,但是要用來做運(yùn)算。
因此,通常會(huì)用3×3(而不是2×3)的矩陣來做二維變換,你可能會(huì)見到3行2列格式的矩陣,這是所謂的以列為主的格式,圖5.1所示的是以行為主的格式,只要能保持一致,用哪種格式都無所謂。
當(dāng)對(duì)圖層應(yīng)用變換矩陣,圖層矩形內(nèi)的每一個(gè)點(diǎn)都被相應(yīng)地做變換,從而形成一個(gè)新的四邊形的形狀。CGAffineTransform中的“仿射”的意思是無論變換矩陣用什么值,圖層中平行的兩條線在變換之后任然保持平行,CGAffineTransform可以做出任意符合上述標(biāo)注的變換,圖5.2顯示了一些仿射的和非仿射的變換:

圖5.2 仿射和非仿射變換
創(chuàng)建一個(gè)CGAffineTransform
如果你對(duì)矩陣完全不熟悉的話,矩陣變換可能會(huì)使你感到畏懼。幸運(yùn)的是,Core Graphics提供了一系列函數(shù),對(duì)完全沒有數(shù)學(xué)基礎(chǔ)的開發(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)的距離。
UIView可以通過設(shè)置transform屬性做變換,但實(shí)際上它只是封裝了內(nèi)部圖層的變換。
CALayer同樣也有一個(gè)transform屬性,但它的類型是CATransform3D,而不是CGAffineTransform,本章后續(xù)將會(huì)詳細(xì)解釋。CALayer對(duì)應(yīng)于UIView的transform屬性叫做affineTransform,清單5.1的例子就是使用affineTransform對(duì)圖層做了45度順時(shí)針旋轉(zhuǎn)。
清單5.1 使用affineTransform對(duì)圖層旋轉(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會(huì)自動(dòng)引入)提供了pi的一些簡(jiǎn)便的換算,M_PI_4于是就是pi的四分之一
混合變換
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);
我們來用這些函數(shù)組合一個(gè)更加復(fù)雜的變換,先縮小50%,再旋轉(zhuǎn)30度,最后向右移動(dòng)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é)果將會(huì)影響之后的變換,所以200像素的向右平移同樣也被旋轉(zhuǎn)了30度,縮小了50%,所以它實(shí)際上是斜向移動(dòng)了100像素。
這意味著變換的順序會(huì)影響最終的結(jié)果,也就是說旋轉(zhuǎn)之后的平移和平移之后的旋轉(zhuǎn)結(jié)果可能不同。
剪切變換
Core Graphics為你提供了計(jì)算變換矩陣的一些方法,所以很少需要直接設(shè)置CGAffineTransform的值。除非需要?jiǎng)?chuàng)建一個(gè)斜切的變換,Core Graphics并沒有提供直接的函數(shù)。
斜切變換是放射變換的第四種類型,較于平移,旋轉(zhuǎn)和縮放并不常用(這也是Core Graphics沒有提供相應(yīng)函數(shù)的原因),但有些時(shí)候也會(huì)很有用。我們用一張圖片可以很直接的說明效果(圖5.5)。也許用“傾斜”描述更加恰當(dāng),具體做變換的代碼見清單5.3。

圖5.5 水平方向的斜切變換
清單5.3 實(shí)現(xiàn)一個(gè)斜切變換
@implementation ViewControllerCGAffineTransform 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
3D變換
CG的前綴告訴我們,CGAffineTransform類型屬于Core Graphics框架,Core Graphics實(shí)際上是一個(gè)嚴(yán)格意義上的2D繪圖API,并且CGAffineTransform僅僅對(duì)2D變換有效。
在前面文章中,我們提到了zPosition屬性,可以用來讓圖層靠近或者遠(yuǎn)離相機(jī)(用戶視角),transform屬性(CATransform3D類型)可以真正做到這點(diǎn),即讓圖層在3D空間內(nèi)移動(dòng)或者旋轉(zhuǎn)。
和CGAffineTransform類似,CATransform3D也是一個(gè)矩陣,但是和2x3的矩陣不同,CATransform3D是一個(gè)可以在3維空間內(nèi)做變換的4x4的矩陣(圖5.6)。

圖5.6 對(duì)一個(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)該對(duì)X軸和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對(duì)視圖內(nèi)的圖層繞Y軸做了45度角的旋轉(zhuǎn),我們可以把視圖向右傾斜,這樣會(huì)看得更清晰。
結(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í)完全沒錯(cuò),視圖看起來更窄實(shí)際上是因?yàn)槲覀冊(cè)谟靡粋€(gè)斜向的視角看它,而不是透視。
透視投影
在真實(shí)世界中,當(dāng)物體原理我們的時(shí)候,由于視角的原因看起來會(huì)變小,理論上說遠(yuǎn)離我們的視圖的邊要比靠近視角的邊跟短,但實(shí)際上并沒有發(fā)生,而我們當(dāng)前的視角是等距離的,也就是在3D變換中任然保持平行,和之前提到的仿射變換類似。
在等距投影中,遠(yuǎn)處的物體和近處的物體保持同樣的縮放比例,這種投影也有它自己的用處(例如建筑繪圖,顛倒,和偽3D視頻),但當(dāng)前我們并不需要。
為了做一些修正,我們需要引入投影變換(又稱作z變換)來對(duì)除了旋轉(zhuǎn)之外的變換矩陣做一些修改,Core Animation并沒有給我們提供設(shè)置透視變換的函數(shù),因此我們需要手動(dòng)修改矩陣值,幸運(yùn)的是,很簡(jiǎn)單:
CATransform3D的透視效果通過一個(gè)矩陣中一個(gè)很簡(jiǎn)單的元素來控制: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)很好了,但對(duì)于特定的圖層有時(shí)候更小后者更大的值會(huì)看起來更舒服,減少距離的值會(huì)增強(qiáng)透視效果,所以一個(gè)非常微小的值會(huì)讓它看起來更加失真,然而一個(gè)非常大的值會(huì)讓它基本失去透視效果,對(duì)視圖應(yīng)用透視的代碼見清單5.5,結(jié)果見圖5.10。
清單5.5 對(duì)變換應(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

消亡點(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)通常是視圖的中心(圖5.11),于是為了在應(yīng)用中創(chuàng)建擬真效果的透視,這個(gè)店應(yīng)該聚在屏幕中點(diǎn),或者至少是包含所有3D對(duì)象的視圖中點(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)該首先把它放置于屏幕中央,然后通過平移來把它移動(dòng)到指定位置(而不是直接改變它的position),這樣所有的3D圖層都共享一個(gè)消亡點(diǎn)。
sublayerTransform屬性
如果有多個(gè)視圖或者圖層,每個(gè)都做3D變換,那就需要分別設(shè)置相同的m34值,并且確保在變換之前都在屏幕中央共享同一個(gè)position,如果用一個(gè)函數(shù)封裝這些操作的確會(huì)更加方便,但仍然有限制(例如,你不能在Interface Builder中擺放視圖),這里有一個(gè)更好的方法。
CALayer有一個(gè)屬性叫做sublayerTransform。它也是CATransform3D類型,但和對(duì)一個(gè)圖層的變換不同,它影響到所有的子圖層。這意味著你可以一次性對(duì)包含這些圖層的容器做變換,于是所有的子圖層都自動(dòng)繼承了這個(gè)變換方法。
相較而言,通過在一個(gè)地方設(shè)置透視變換會(huì)很方便,同時(shí)它會(huì)帶來另一個(gè)顯著的優(yōu)勢(shì):消亡點(diǎn)被設(shè)置在容器圖層的中點(diǎn),從而不需要再對(duì)子圖層分別設(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 通過相同的透視效果分別對(duì)視圖做變換
背面
我們既然可以在3D場(chǎng)景下旋轉(zhuǎn)圖層,那么也可以從背面去觀察它。如果我們?cè)谇鍐?.4中把角度修改為M_PI(180度)而不是當(dāng)前的M_PI_4(45度),那么將會(huì)把圖層完全旋轉(zhuǎn)一個(gè)半圈,于是完全背對(duì)了相機(jī)視角。
那么從背部看圖層是什么樣的呢,見圖5.14

圖5.14 視圖的背面,一個(gè)鏡像對(duì)稱的圖片
如你所見,圖層是雙面繪制的,反面顯示的是正面的一個(gè)鏡像圖片。
但這并不是一個(gè)很好的特性,因?yàn)槿绻麍D層包含文本或者其他控件,那用戶看到這些內(nèi)容的鏡像圖片當(dāng)然會(huì)感到困惑。另外也有可能造成資源的浪費(fèi):想象用這些圖層形成一個(gè)不透明的固態(tài)立方體,既然永遠(yuǎn)都看不見這些圖層的背面,那為什么浪費(fèi)GPU來繪制它們呢?
CALayer有一個(gè)叫做doubleSided的屬性來控制圖層的背面是否要被繪制。這是一個(gè)BOOL類型,默認(rèn)為YES,如果設(shè)置為NO,那么當(dāng)圖層正面從相機(jī)視角消失的時(shí)候,它將不會(huì)被繪制。
扁平化圖層
如果對(duì)包含已經(jīng)做過變換的圖層的圖層做反方向的變換將會(huì)發(fā)什么什么呢?是不是有點(diǎn)困惑?見圖5.15

圖5.15 反方向變換的嵌套圖層
注意做了-45度旋轉(zhuǎn)的內(nèi)部圖層是怎樣抵消旋轉(zhuǎn)45度的圖層,從而恢復(fù)正常狀態(tài)的。
如果內(nèi)部圖層相對(duì)外部圖層做了相反的變換(這里是繞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)部的圖層并不直接是容器圖層的子圖層,所以這里分別對(duì)圖層設(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ā)什么了什么呢??jī)?nèi)部的圖層仍然向左側(cè)旋轉(zhuǎn),并且發(fā)生了扭曲,但按道理說它應(yīng)該保持正面朝上,并且顯示正常的方塊。
這是由于盡管Core Animation圖層存在于3D空間之內(nèi),但它們并不都存在同一個(gè)3D空間。每個(gè)圖層的3D場(chǎng)景其實(shí)是扁平化的,當(dāng)你從正面觀察一個(gè)圖層,看到的實(shí)際上由子圖層創(chuàng)建的想象出來的3D場(chǎng)景,但當(dāng)你傾斜這個(gè)圖層,你會(huì)發(fā)現(xiàn)實(shí)際上這個(gè)3D場(chǎng)景僅僅是被繪制在圖層的表面。

圖5.18 繞Y軸做相反旋轉(zhuǎn)的真實(shí)結(jié)果
類似的,當(dāng)你在玩一個(gè)3D游戲,實(shí)際上僅僅是把屏幕做了一次傾斜,或許在游戲中可以看見有一面墻在你面前,但是傾斜屏幕并不能夠看見墻里面的東西。所有場(chǎng)景里面繪制的東西并不會(huì)隨著你觀察它的角度改變而發(fā)生變化;圖層也是同樣的道理。
這使得用Core Animation創(chuàng)建非常復(fù)雜的3D場(chǎng)景變得十分困難。你不能夠使用圖層樹去創(chuàng)建一個(gè)3D結(jié)構(gòu)的層級(jí)關(guān)系--在相同場(chǎng)景下的任何3D表面必須和同樣的圖層保持一致,這是因?yàn)槊總€(gè)的父視圖都把它的子視圖扁平化了。
至少當(dāng)你用正常的CALayer的時(shí)候是這樣,CALayer有一個(gè)叫做CATransformLayer的子類來解決這個(gè)問題。具體會(huì)在后面討論。
固體對(duì)象
現(xiàn)在你懂得了在3D空間的一些圖層布局的基礎(chǔ),我們來試著創(chuàng)建一個(gè)固態(tài)的3D對(duì)象(實(shí)際上是一個(gè)技術(shù)上所謂的空洞對(duì)象,但它以固態(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è)立方體之后也不會(huì)改變這個(gè)性質(zhì)。

圖5.19 用Interface Builder對(duì)立方體的六個(gè)面進(jìn)行布局
這些面視圖并沒有放置在主視圖當(dāng)中,而是松散地排列在根nib文件里面。我們并不關(guān)心在這個(gè)容器中如何擺放它們的位置,因?yàn)楹罄m(xù)將會(huì)用圖層的transform對(duì)它們進(jìn)行重新布局,并且用Interface Builder在容器視圖之外擺放他們可以讓我們?nèi)菀卓辞宄鼈兊膬?nèi)容,如果把它們一個(gè)疊著一個(gè)都塞進(jìn)主視圖,將會(huì)變得很難看。
我們把一個(gè)有顏色的UILabel放置在視圖內(nèi)部,是為了清楚的辨別它們之間的關(guān)系,并且UIButton被放置在第三個(gè)面視圖里面,后面會(huì)做簡(jiǎn)單的解釋。
具體把視圖組織成立方體的代碼見清單5.9,結(jié)果見圖5.20
@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è)立方體將會(huì)顯得很笨重,因?yàn)槲覀円獑为?dú)對(duì)每個(gè)面做旋轉(zhuǎn)。另一個(gè)簡(jiǎn)單的方案是通過調(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í)面貌(圖5.21)。

點(diǎn)擊事件
你應(yīng)該能注意到現(xiàn)在可以在第三個(gè)表面的頂部看見按鈕了,點(diǎn)擊它,什么都沒發(fā)生,為什么呢?
這并不是因?yàn)閕OS在3D場(chǎng)景下正確地處理響應(yīng)事件,實(shí)際上是可以做到的。問題在于視圖順序。在第三章中我們簡(jiǎn)要提到過,點(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)楸硨?duì)相機(jī)而隱藏的視圖仍然會(huì)響應(yīng)點(diǎn)擊事件(這和通過設(shè)置hidden屬性或者設(shè)置alpha為0而隱藏的視圖不同,那兩種方式將不會(huì)響應(yīng)事件)。所以即使禁止了雙面渲染仍然不能解決這個(gè)問題(雖然由于性能問題,還是需要把它設(shè)置成NO)。
這里有幾種正確的方案:把除了表面3的其他視圖userInteractionEnabled屬性都設(shè)置成NO來禁止事件傳遞。或者簡(jiǎn)單通過代碼把視圖3覆蓋在視圖6上。無論怎樣都可以點(diǎn)擊按鈕了(圖5.23)。

圖5.23 背景視圖不再阻礙按鈕,我們可以點(diǎn)擊它了
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注