視覺效果和變換git
(四)視覺效果
算法
嗯,園和橢圓還不錯,但若是是帶圓角的矩形呢?數組
咱們如今能作到那樣了麼?app
史蒂芬·喬布斯框架
我 們在第三章『圖層幾何學』中討論了圖層的frame,第二章『寄宿圖』則討論了圖層的寄宿圖。可是圖層不只僅能夠是圖片或是顏色的容器;還有一系列內建的 特性使得創造美麗優雅的使人深入的界面元素成爲可能。在這一章,咱們將會探索一些可以經過使用CALayer屬性實現的視覺效果。ide
圓角函數
圓角矩形是iOS的一個標誌性審美特性。這在iOS的每個地方都獲得了體現,不管是主屏幕圖標,仍是警告彈框,甚至是文本框。按照這流行程度,你可能會認爲必定有不借助Photoshop就能輕易建立圓角舉行的方法。恭喜你,猜對了。佈局
CALayer 有一個叫作conrnerRadius的屬性控制着圖層角的曲率。它是一個浮點數,默認爲0(爲0的時候就是直角),可是你能夠把它設置成任意值。默認情 況下,這個曲率值隻影響背景顏色而不影響背景圖片或是子圖層。不過,若是把masksToBounds設置成YES的話,圖層裏面的全部東西都會被截取。性能
我 們能夠經過一個簡單的項目來演示這個效果。在Interface Builder中,咱們放置一些視圖,他們有一些子視圖。並且這些子視圖有一些超出了邊界(如圖4.1)。你可能沒法看到他們超出了邊界,由於在編輯界面 的時候,超出的部分老是被Interface Builder裁切掉了。不過,你相信我就行了 :)學習
圖4.1 兩個白色的大視圖,他們都包含了小一些的紅色視圖。
然 後在代碼中,咱們設置角的半徑爲20個點,並裁剪掉第一個視圖的超出部分(見清單4.1)。技術上來講,這些屬性均可以在Interface Builder的探測板中分別經過『用戶定義運行時屬性』和勾選『裁剪子視圖』(Clip Subviews)選擇框來直接設置屬性的值。不過,在這個示例中,代碼可以表示得更清楚。圖4.2是運行代碼的結果
清單4.1 設置cornerRadius和masksToBounds
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@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
|
右圖中,紅色的子視圖沿角半徑被裁剪了
如你所見,右邊的子視圖沿邊界被裁剪了。
單獨控制每一個層的圓角曲率也不是不可能的。若是想建立有些圓角有些直角的圖層或視圖時,你可能須要一些不一樣的方法。好比使用一個圖層蒙板(本章稍後會講到)或者是CAShapeLayer(見第六章『專用圖層』)。
圖層邊框
CALayer另外兩個很是有用屬性就是borderWidth和borderColor。兩者共同定義了圖層邊的繪製樣式。這條線(也被稱做stroke)沿着圖層的bounds繪製,同時也包含圖層的角。
borderWidth是以點爲單位的定義邊框粗細的浮點數,默認爲0.borderColor定義了邊框的顏色,默認爲黑色。
borderColor 是CGColorRef類型,而不是UIColor,因此它不是Cocoa的內置對象。不過呢,你確定也清楚圖層引用了borderColor,雖然屬性 聲明並不能證實這一點。CGColorRef在引用/釋放時候的行爲表現得與NSObject極其類似。可是Objective-C語法並不支持這一作 法,因此CGColorRef屬性即使是強引用也只能經過assign關鍵字來聲明。
邊框是繪製在圖層邊界裏面的,並且在全部子內容以前,也在子圖層以前。若是咱們在以前的示例中(清單4.2)加入圖層的邊框,你就能看到究竟是怎麼一回事了(如圖4.3).
清單4.2 加上邊框
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@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 給圖層增長一個邊框
仔細觀察會發現邊框並不會把寄宿圖或子圖層的形狀計算進來,若是圖層的子圖層超過了邊界,或者是寄宿圖在透明區域有一個透明蒙板,邊框仍然會沿着圖層的邊界繪製出來(如圖4.4).
圖4.4 邊框是跟隨圖層的邊界變化的,而不是圖層裏面的內容
陰影
iOS的另外一個常見特性呢,就是陰影。陰影每每能夠達到圖層深度暗示的效果。也可以用來強調正在顯示的圖層和優先級(好比說一個在其餘視圖以前的彈出框),不過有時候他們只是單純的裝飾目的。
給 shadowOpacity屬性一個大於默認值(也就是0)的值,陰影就能夠顯示在任意圖層之下。shadowOpacity是一個必須在0.0(不可 見)和1.0(徹底不透明)之間的浮點數。若是設置爲1.0,將會顯示一個有輕微模糊的黑色陰影稍微在圖層之上。若要改動陰影的表現,你可使用 CALayer的另外三個屬性:shadowColor,shadowOffset和shadowRadius。
顯而易見,shadowColor屬性控制着陰影的顏色,和borderColor和backgroundColor同樣,它的類型也是CGColorRef。陰影默認是黑色,大多數時候你須要的陰影也是黑色的(其餘顏色的陰影看起來是否是有一點點奇怪。。)。
shadowOffset屬性控制着陰影的方向和距離。它是一個CGSize的值,寬度控制這陰影橫向的位移,高度控制着縱向的位移。shadowOffset的默認值是 {0, -3},意即陰影相對於Y軸有3個點的向上位移。
爲 什麼要默認向上的陰影呢?儘管Core Animation是從圖層套裝演變而來(能夠認爲是爲iOS建立的私有動畫框架),可是呢,它倒是在Mac OS上面世的,前面有提到,兩者的Y軸是顛倒的。這就致使了默認的3個點位移的陰影是向上的。在Mac上,shadowOffset的默認值是陰影向下 的,這樣你就能理解爲何iOS上的陰影方向是向上的了(如圖4.5).
圖4.5 在iOS(左)和Mac OS(右)上shadowOffset的表現。
蘋果更傾向於用戶界面的陰影應該是垂直向下的,因此在iOS把陰影寬度設爲0,而後高度設爲一個正值不失爲一個作法。
shadowRadius屬性控制着陰影的模糊度,當它的值是0的時候,陰影就和視圖同樣有一個很是肯定的邊界線。當值愈來愈大的時候,邊界線看上去就會愈來愈模糊和天然。蘋果自家的應用設計更偏向於天然的陰影,因此一個非零值再合適不過了。
一般來說,若是你想讓視圖或控件很是醒目獨立於背景以外(好比彈出框遮罩層),你就應該給shadowRadius設置一個稍大的值。陰影越模糊,圖層的深度看上去就會更明顯(如圖4.6).
圖4.6 大一些的陰影位移和角半徑會增長圖層的深度即視感
陰影裁剪
和圖層邊框不一樣,圖層的陰影繼承自內容的外形,而不是根據邊界和角半徑來肯定。爲了計算出陰影的形狀,Core Animation會將寄宿圖(包括子視圖,若是有的話)考慮在內,而後經過這些來完美搭配圖層形狀從而建立一個陰影(見圖4.7)。
圖4.7 陰影是根據寄宿圖的輪廓來肯定的
當陰影和裁剪扯上關係的時候就有一個頭疼的限制:陰影一般就是在Layer的邊界以外,若是你開啓了masksToBounds屬性,全部從圖層中突出來的內容都會被才剪掉。若是咱們在咱們以前的邊框示例項目中增長圖層的陰影屬性時,你就會發現問題所在(見圖4.8).
圖4.8 maskToBounds屬性裁剪掉了陰影和內容
從技術角度來講,這個結果是能夠是能夠理解的,但確實又不是咱們想要的效果。若是你想沿着內容裁切,你須要用到兩個圖層:一個只畫陰影的空的外圖層,和一個用masksToBounds裁剪內容的內圖層。
若是咱們把以前項目的右邊用單獨的視圖把裁剪的視圖包起來,咱們就能夠解決這個問題(如圖4.9).
圖4.9 右邊,用額外的陰影轉換視圖包裹被裁剪的視圖
咱們只把陰影用在最外層的視圖上,內層視圖進行裁剪。清單4.3是代碼實現,圖4.10是運行結果。
清單4.3 用一個額外的視圖來解決陰影裁切的問題
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@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屬性
咱們已經知道圖層陰影並不老是方的,而是從圖層內容的形狀繼承而來。這看上去不錯,可是實時計算陰影也是一個很是消耗資源的,尤爲是圖層有多個子圖層,每一個圖層還有一個有透明效果的寄宿圖的時候。
如 果你事先知道你的陰影形狀會是什麼樣子的,你能夠經過指定一個shadowPath來提升性能。shadowPath是一個CGPathRef類型(一個 指向CGPath的指針)。CGPath是一個Core Graphics對象,用來指定任意的一個矢量圖形。咱們能夠經過這個屬性單獨於圖層形狀以外指定陰影的形狀。
圖4.11 展現了同一寄宿圖的不一樣陰影設定。如你所見,咱們使用的圖形很簡單,可是它的陰影能夠是你想要的任何形狀。清單4.4是代碼實現。
圖4.11 用shadowPath指定任意陰影形狀
清單4.4 建立簡單的陰影形狀
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@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
|
若是是一個舉行或是圓,用CGPath會至關簡單明瞭。可是若是是更加複雜一點的圖形,UIBezierPath類會更合適,它是一個由UIKit提供的在CGPath基礎上的Objective-C包裝類。
圖層蒙板
通 過masksToBounds屬性,咱們能夠沿邊界裁剪圖形;經過cornerRadius屬性,咱們還能夠設定一個圓角。可是有時候你但願展示的內容不 是在一個矩形或圓角矩形。好比,你想展現一個有星形框架的圖片,又或者想讓一些古卷文字慢慢漸變成背景色,而不是一個突兀的邊界。
使用一個32位有alpha通道的png圖片一般是建立一個無矩形視圖最方便的方法,你能夠給它指定一個透明蒙板來實現。可是這個方法不能讓你以編碼的方式動態地生成蒙板,也不能讓子圖層或子視圖裁剪成一樣的形狀。
CALayer 有一個屬性叫作mask能夠解決這個問題。這個屬性自己就是個CALayer類型,有和其餘圖層同樣的繪製和佈局屬性。它相似於一個子圖層,相對於父圖層 (即擁有該屬性的圖層)佈局,可是它卻不是一個普通的子圖層。不一樣於那些繪製在父圖層中的子圖層,mask圖層定義了父圖層的部分可見區域。
mask圖層的Color屬性是可有可無的,真正重要的是圖層的輪廓。mask屬性就像是一個餅乾切割機,mask圖層實心的部分會被保留下來,其餘的則會被拋棄。(如圖4.12)
若是mask圖層比父圖層要小,只有在mask圖層裏面的內容纔是它關心的,除此之外的一切都會被隱藏起來。
圖4.12 把圖片和蒙板圖層做用在一塊兒的效果
我 們將代碼演示一下這個過程,建立一個簡單的項目,經過圖層的mask屬性來做用於圖片之上。爲了簡便一些,咱們用Interface Builder來建立一個包含UIImageView的圖片圖層。這樣咱們就只要代碼實現蒙板圖層了。清單4.5是最終的代碼,圖4.13是運行後的結 果。
清單4.5 應用蒙板圖層
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@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蒙板圖層真正厲害的地方在於蒙板圖不侷限於靜態圖。任何有圖層構成的均可以做爲mask屬性,這意味着你的蒙板能夠經過代碼甚至是動畫實時生成。
拉伸過濾
最後咱們再來談談minificationFilter和magnificationFilter屬性。總得來說,當咱們視圖顯示一個圖片的時候,都應該正確地顯示這個圖片(意即:以正確的比例和正確的1:1像素顯示在屏幕上)。緣由以下:
可以顯示最好的畫質,像素既沒有被壓縮也沒有被拉伸。
能更好的使用內存,由於這就是全部你要存儲的東西。
最好的性能表現,CPU不須要爲此額外的計算。
不過有時候,顯示一個非真實大小的圖片確實是咱們須要的效果。好比說一個頭像或是圖片的縮略圖,再好比說一個能夠被拖拽和伸縮的大圖。這些狀況下,爲同一圖片的不一樣大小存儲不一樣的圖片顯得又不切實際。
當圖片須要顯示不一樣的大小的時候,有一種叫作拉伸過濾的算法就起到做用了。它做用於原圖的像素上並根據須要生成新的像素顯示在屏幕上。
事實上,重繪圖片大小也沒有一個統一的通用算法。這取決於須要拉伸的內容,放大或是縮小的需求等這些因素。CALayer爲此提供了三種拉伸過濾方法,他們是:
kCAFilterLinear
kCAFilterNearest
kCAFilterTrilinear
minification(縮 小圖片)和magnification(放大圖片)默認的過濾器都是kCAFilterLinear,這個過濾器採用雙線性濾波算法,它在大多數狀況下都 表現良好。雙線性濾波算法經過對多個像素取樣最終生成新的值,獲得一個平滑的表現不錯的拉伸。可是當放大倍數比較大的時候圖片就模糊不清了。
kCAFilterTrilinear 和kCAFilterLinear很是類似,大部分狀況下兩者都看不出來有什麼差異。可是,較雙線性濾波算法而言,三線性濾波算法存儲了多個大小狀況下的 圖片(也叫多重貼圖),並三維取樣,同時結合大圖和小圖的存儲進而獲得最後的結果。
這個方法的好處在於算法可以從一系列已經接近於最終大小的圖片中獲得想要的結果,也就是說不要對不少像素同步取樣。這不只提升了性能,也避免了小几率因舍入錯誤引發的取樣失靈的問題
圖4.14 對於大圖來講,雙線性濾波和三線性濾波表現得更出色
kCAFilterNearest是一種比較武斷的方法。從名字不難看出,這個算法(也叫最近過濾)就是取最近的單像素點而無論其餘的顏色。這樣作很是快,也不會使圖片模糊。可是,最明顯的效果就是,會使得壓縮圖片更糟,圖片放大以後也顯得塊狀或是馬賽克嚴重。
圖4.15 對於沒有斜線的小圖來講,最近過濾算法要好不少
總 的來講,對於比較小的圖或者是差別特別明顯,極少斜線的大圖,最近過濾算法會保留這種差別明顯的特質以呈現更好的結果。可是對於大多數的圖尤爲是有不少斜 線或是曲線輪廓的圖片來講,最近過濾算法會致使更差的結果。換句話說,線性過濾保留了形狀,最近過濾則保留了像素的差別。
讓咱們來實驗一下。咱們對第三章的時鐘項目改動一下,用LCD風格的數字方式顯示。咱們用簡單的像素字體(一種用像素構成字符的字體,而非矢量圖形)創造數字顯示方式,用圖片存儲起來,並且用第二章介紹過的拼合技術來顯示(如圖4.16)。
圖4.16 一個簡單的運用拼合技術顯示的LCD數字風格的像素字體
我 們在Interface Builder中放置了六個視圖,小時、分鐘、秒鐘各兩個,圖4.17顯示了這六個視圖是如何在Interface Builder中放置的。若是每一個都用一個淡出的outlets對象就會顯得太多了,因此咱們就用了一個IBOutletCollection對象把他們 和控制器聯繫起來,這樣咱們就能夠以數組的方式訪問視圖了。清單4.6是代碼實現。
清單4.6 顯示一個LCD風格的時鐘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
@interface ViewController ()
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *digitViews;
@property (nonatomic, weak) NSTimer *timer;
??
@end
@implementation ViewController
- (void)viewDidLoad
{
[
super
viewDidLoad];
//get spritesheet image
UIImage *digits = [UIImage imageNamed:@
"Digits.png"
];
//set up digit views
for
(UIView *view
in
self.digitViews) {
//set contents
view.layer.contents = (__bridge id)digits.CGImage;
view.layer.contentsRect = CGRectMake(0, 0, 0.1, 1.0);
view.layer.contentsGravity = kCAGravityResizeAspect;
}
//start timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
//set initial clock time
[self tick];
}
- (void)setDigit:(NSInteger)digit forView:(UIView *)view
{
//adjust contentsRect to select correct digit
view.layer.contentsRect = CGRectMake(digit * 0.1, 0, 0.1, 1.0);
}
- (void)tick
{
//convert time to hours, minutes and seconds
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier: NSGregorianCalendar];
NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
?
NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
//set hours
[self setDigit:components.hour / 10 forView:self.digitViews[0]];
[self setDigit:components.hour % 10 forView:self.digitViews[1]];
//set minutes
[self setDigit:components.minute / 10 forView:self.digitViews[2]];
[self setDigit:components.minute % 10 forView:self.digitViews[3]];
//set seconds
[self setDigit:components.second / 10 forView:self.digitViews[4]];
[self setDigit:components.second % 10 forView:self.digitViews[5]];
}
@end
|
如圖4.18,這樣作的確起了效果,可是圖片看起來模糊了。看起來默認的kCAFilterLinear選項讓咱們失望了。
圖4.18 一個模糊的時鐘,由默認的kCAFilterLinear引發
爲了能像圖4.19中那樣,咱們須要在for循環中加入以下代碼:
1
|
view.layer.magnificationFilter = kCAFilterNearest;
|
圖4.19 設置了最近過濾以後的清晰顯示
組透明
UIView有一個叫作alpha的屬性來肯定視圖的透明度。CALayer有一個等同的屬性叫作opacity,這兩個屬性都是影響子層級的。也就是說,若是你給一個圖層設置了opacity屬性,那它的子圖層都會受此影響。
iOS 常見的作法是把一個空間的alpha值設置爲0.5(50%)以使其看上去呈現爲不可用狀態。對於獨立的視圖來講還不錯,可是當一個控件有子視圖的時候就 有點奇怪了,圖4.20展現了一個內嵌了UILabel的自定義UIButton;左邊是一個不透明的按鈕,右邊是50%透明度的相同按鈕。咱們能夠注意 到,裏面的標籤的輪廓跟按鈕的背景很不搭調。
圖4.20 右邊的漸隱按鈕中,裏面的標籤清晰可見
這 是由透明度的混合疊加形成的,當你顯示一個50%透明度的圖層時,圖層的每一個像素都會通常顯示本身的顏色,另外一半顯示圖層下面的顏色。這是正常的透明度的 表現。可是若是圖層包含一個一樣顯示50%透明的子圖層時,你所看到的視圖,50%來自子視圖,25%來了圖層自己的顏色,另外的25%則來自背景色。
在咱們的示例中,按鈕和表情都是白色背景。雖然他們都死50%的可見度,可是合起來的可見度是75%,因此標籤所在的區域看上去就沒有周圍的部分那麼透明。因此看上去子視圖就高粱了,使得這個顯示效果都糟透了。
理 想情況下,當你設置了一個圖層的透明度,你但願它包含的整個圖層樹像一個總體同樣的透明效果。你能夠經過設置Info.plist文件中的 UIViewGroupOpacity爲YES來達到這個效果,可是這個設置會影響到這個應用,整個app可能會受到不良影響。若是 UIViewGroupOpacity並未設置,iOS 6和之前的版本會默認爲NO(也許之後的版本會有一些改變)。
另外一個方法就是,你能夠設置CALayer的一個叫作shouldRasterize屬性(見清單4.7)來實現組透明的效果,若是它被設置爲YES,在應用透明度以前,圖層及其子圖層都會被整合成一個總體的圖片,這樣就沒有透明度混合的問題了(如圖4.21)。
爲 了啓用shouldRasterize屬性,咱們設置了圖層的rasterizationScale屬性。默認狀況下,全部圖層拉伸都是1.0, 因此若是你使用了shouldRasterize屬性,你就要確保你設置了rasterizationScale屬性去匹配屏幕,以防止出現Retina 屏幕像素化的問題。
當shouldRasterize和UIViewGroupOpacity一塊兒的時候,性能問題就出現了(咱們在第12章『速度』和第15章『圖層性能』將作出介紹),可是性能碰撞都本地化了(譯者注:這句話須要再翻譯)。
清單4.7 使用shouldRasterize屬性解決組透明問題
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
@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 修正後的圖
總結
這一章介紹了一些能夠經過代碼應用到圖層上的視覺效果,好比圓角,陰影和蒙板。咱們也瞭解了拉伸過濾器和組透明。
在第五章,『變換』中,咱們將會研究圖層變化和3D轉換。
--------------------------------------------------------------------------------------------------------------------------------------------------------
(五)變換
很不幸,沒人能告訴你母體是什麼,你只能本身體會 -- 駭客帝國
在 第四章「可視效果」中,咱們研究了一些加強圖層和它的內容顯示效果的一些技術,在這一章中,咱們將要研究能夠用來對圖層旋轉,擺放或者扭曲的 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實 例:
1
2
3
|
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度
1
2
3
4
5
6
7
8
9
10
11
12
|
@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
|
注意咱們使用的旋轉常量是M_PI_4,而不是你想象的45,由於iOS的變換函數使用弧度而不是角度做爲單位。弧度用數學常量pi的倍數表示,一個pi表明180度,因此四分之一的pi就是45度。
C的數學函數庫(iOS會自動引入)提供了pi的一些簡便的換算,M_PI_4因而就是pi的四分之一,若是對換算不太清楚的話,能夠用以下的宏作換算:
1
2
|
#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)
#define DEGREES_TO_RADIANS(x) ((x)/180.0*M_PI)
|
混合變換
Core Graphics提供了一系列的函數能夠在一個變換的基礎上作更深層次的變換,若是作一個既要縮放又要旋轉的變換,這就會很是有用了。例以下面幾個函數:
1
2
3
|
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)
|
當操縱一個變換的時候,初始生成一個什麼都不作的變換很重要--也就是建立一個CGAffineTransform類型的空值,矩陣論中稱做單位矩陣,Core Graphics一樣也提供了一個方便的常量:
1
|
CGAffineTransformIdentity
|
最後,若是須要混合兩個已經存在的變換矩陣,就可使用以下方法,在兩個變換的基礎上建立一個新的變換:
1
|
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);
|
咱們來用這些函數組合一個更加複雜的變換,先縮小50%,再旋轉30度,最後向右移動200個像素(清單5.2)。圖5.4顯示了圖層變換最後的結果。
清單5.2 使用若干方法建立一個複合變換
1
2
3
4
5
6
7
8
9
10
|
- (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 順序應用多個仿射變換以後的結果
圖 5.4中有些須要注意的地方:圖片向右邊發生了平移,但並無指定距離那麼遠(200像素),另外它還有點向下發生了平移。緣由在於當你按順序作了變換, 上一個變換的結果將會影響以後的變換,因此200像素的向右平移一樣也被旋轉了30度,縮小了50%,因此它其實是斜向移動了100像素。
這意味着變換的順序會影響最終的結果,也就是說旋轉以後的平移和平移以後的旋轉結果可能不一樣。
剪切變換
Core Graphics爲你提供了計算變換矩陣的一些方法,因此不多須要直接設置CGAffineTransform的值。除非須要建立一個斜切的變換,Core Graphics並無提供直接的函數。
斜切變換是放射變換的第四種類型,較於平移,旋轉和縮放並不經常使用(這也是Core Graphics沒有提供相應函數的緣由),但有些時候也會頗有用。咱們用一張圖片能夠很直接的說明效果(圖5.5)。也許用「傾斜」描述更加恰當,具體作變換的代碼見清單5.3。
圖5.5 水平方向的斜切變換
清單5.3 實現一個斜切變換
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@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
|
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三個參數,分別決定了每一個座標軸方 向上的旋轉:
1
2
3
|
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軸旋轉圖層
1
2
3
4
5
6
7
8
9
|
@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軸旋轉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 對變換應用透視效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@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 應用透視效果以後再次對圖層作旋轉
消亡點
當在透視角度繪圖的時候,遠離相機視角的物體將會變小變遠,當遠離到一個極限距離,它們可能就縮成了一個點,因而全部的物體最後都匯聚消失在同一個點。
在現實中,這個點一般是視圖的中心(圖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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@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場景下旋轉圖層,那麼也能夠從背面去觀察它。若是咱們在清單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軸作相反的旋轉變換
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@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 旋轉後的視圖
運 行結果和咱們預期的一致。如今在3D狀況下再試一次。修改代碼,讓內外兩個視圖繞Y軸旋轉而不是Z軸,再加上透視效果,以便咱們觀察。注意不能用 sublayerTransform屬性,由於內部的圖層並不直接是容器圖層的子圖層,因此這裏分別對圖層設置透視變換(清單5.8)。
清單5.8 繞Y軸相反的旋轉變換
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
- (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;
}
|
預期的效果應該如圖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 建立一個立方體
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
@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去旋轉照相機。
添加以下幾行去旋轉containerView圖層的perspective變換矩陣:
1
2
|
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 對立方體的表面應用動態的光線效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
#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
{
[
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說明了觸摸事件 的處理,視圖中圖層添加的層級順序會比屏幕上顯示的順序更有意義。
第六章咱們會研究一些Core Animation提供不一樣功能的具體的CALayer子類。