** 圖層樹惦辛、寄宿圖以及圖層幾何學(xué)(一)圖層的樹狀結(jié)構(gòu)**
巨妖有圖層,洋蔥也有圖層,你有嗎吸申?我們都有圖層 -- 史萊克
Core Animation其實(shí)是一個(gè)令人誤解的命名。你可能認(rèn)為它只是用來(lái)做動(dòng)畫的享甸,但實(shí)際上它是從一個(gè)叫做Layer Kit這么一個(gè)不怎么和動(dòng)畫有關(guān)的名字演變而來(lái)截碴,所以做動(dòng)畫這只是Core Animation特性的冰山一角。
Core Animation是一個(gè)復(fù)合引擎蛉威,它的職責(zé)就是盡可能快地組合屏幕上不同的可視內(nèi)容日丹,這個(gè)內(nèi)容是被分解成獨(dú)立的圖層,存儲(chǔ)在一個(gè)叫做圖層樹的體系之中蚯嫌。于是這個(gè)樹形成了UIKit以及在iOS應(yīng)用程序當(dāng)中你所能在屏幕上看見的一切的基礎(chǔ)哲虾。
在我們討論動(dòng)畫之前,我們將從圖層樹開始择示,涉及一下Core Animation的靜態(tài)組合以及布局特性束凑。
圖層和視圖
如果你曾經(jīng)在iOS或者M(jìn)ac OS平臺(tái)上寫過應(yīng)用程序,你可能會(huì)對(duì)視圖的概念比較熟悉对妄。一個(gè)視圖就是在屏幕上顯示的一個(gè)矩形塊(比如圖片湘今,文字或者視頻),它能夠攔截類似于鼠標(biāo)點(diǎn)擊或者觸摸手勢(shì)等用戶輸入剪菱。視圖在層級(jí)關(guān)系中可以互相嵌套摩瞎,一個(gè)視圖可以管理它的所有子視圖的位置拴签。圖1.1顯示了一種典型的視圖層級(jí)關(guān)系
在iOS當(dāng)中,所有的視圖都從一個(gè)叫做UIVIew的基類派生而來(lái)旗们,UIView可以處理觸摸事件蚓哩,可以支持基于Core Graphics繪圖,可以做仿射變換(例如旋轉(zhuǎn)或者縮放)上渴,或者簡(jiǎn)單的類似于滑動(dòng)或者漸變的動(dòng)畫岸梨。
CALayer
CALayer類在概念上和UIView類似,同樣也是一些被層級(jí)關(guān)系樹管理的矩形塊稠氮,同樣也可以包含一些內(nèi)容(像圖片曹阔,文本或者背景色),管理子圖層的位置隔披。它們有一些方法和屬性用來(lái)做動(dòng)畫和變換赃份。和UIView最大的不同是CALayer不處理用戶的交互。
CALayer并不清楚具體的響應(yīng)鏈(iOS通過視圖層級(jí)關(guān)系用來(lái)傳送觸摸事件的機(jī)制)奢米,于是它并不能夠響應(yīng)事件抓韩,即使它提供了一些方法來(lái)判斷是否一個(gè)觸點(diǎn)在圖層的范圍之內(nèi)(具體見第三章,“圖層的幾何學(xué)”)
平行的層級(jí)關(guān)系
每一個(gè)UIview都有一個(gè)CALayer實(shí)例的圖層屬性鬓长,也就是所謂的backing layer谒拴,視圖的職責(zé)就是創(chuàng)建并管理這個(gè)圖層,以確保當(dāng)子視圖在層級(jí)關(guān)系中添加或者被移除的時(shí)候涉波,他們關(guān)聯(lián)的圖層也同樣對(duì)應(yīng)在層級(jí)關(guān)系樹當(dāng)中有相同的操作(見圖1.2)英上。
實(shí)際上這些背后關(guān)聯(lián)的圖層才是真正用來(lái)在屏幕上顯示和做動(dòng)畫,UIView僅僅是對(duì)它的一個(gè)封裝怠蹂,提供了一些iOS類似于處理觸摸的具體功能善延,以及Core Animation底層方法的高級(jí)接口。
但是為什么iOS要基于UIView和CALayer提供兩個(gè)平行的層級(jí)關(guān)系呢城侧?為什么不用一個(gè)簡(jiǎn)單的層級(jí)來(lái)處理所有事情呢易遣?原因在于要做職責(zé)分離,這樣也能避免很多重復(fù)代碼嫌佑。在iOS和Mac OS兩個(gè)平臺(tái)上豆茫,事件和用戶交互有很多地方的不同,基于多點(diǎn)觸控的用戶界面和基于鼠標(biāo)鍵盤有著本質(zhì)的區(qū)別屋摇,這就是為什么iOS有UIKit和UIView揩魂,但是Mac OS有AppKit和NSView的原因。他們功能上很相似炮温,但是在實(shí)現(xiàn)上有著顯著的區(qū)別火脉。
繪圖,布局和動(dòng)畫,相比之下就是類似Mac筆記本和桌面系列一樣應(yīng)用于iPhone和iPad觸屏的概念倦挂。把這種功能的邏輯分開并應(yīng)用到獨(dú)立的Core Animation框架畸颅,蘋果就能夠在iOS和Mac OS之間共享代碼,使得對(duì)蘋果自己的OS開發(fā)團(tuán)隊(duì)和第三方開發(fā)者去開發(fā)兩個(gè)平臺(tái)的應(yīng)用更加便捷方援。
實(shí)際上没炒,這里并不是兩個(gè)層級(jí)關(guān)系,而是四個(gè)犯戏,每一個(gè)都扮演不同的角色送火,除了視圖層級(jí)和圖層樹之外,還存在呈現(xiàn)樹和渲染樹先匪,將在第七章“隱式動(dòng)畫”和第十二章“性能調(diào)優(yōu)”分別討論种吸。
圖層的能力
如果說CALayer是UIView內(nèi)部實(shí)現(xiàn)細(xì)節(jié),那我們?yōu)槭裁匆娴亓私馑匮椒牵刻O果當(dāng)然為我們提供了優(yōu)美簡(jiǎn)潔的UIView接口骨稿,那么我們是否就沒必要直接去處理Core Animation的細(xì)節(jié)了呢?
某種意義上說的確是這樣姜钳,對(duì)一些簡(jiǎn)單的需求來(lái)說,我們確實(shí)沒必要處理CALayer形耗,因?yàn)樘O果已經(jīng)通過UIView的高級(jí)API間接地使得動(dòng)畫變得很簡(jiǎn)單哥桥。
但是這種簡(jiǎn)單會(huì)不可避免地帶來(lái)一些靈活上的缺陷。如果你略微想在底層做一些改變激涤,或者使用一些蘋果沒有在UIView上實(shí)現(xiàn)的接口功能拟糕,這時(shí)除了介入Core Animation底層之外別無(wú)選擇。
我們已經(jīng)證實(shí)了圖層不能像視圖那樣處理觸摸事件倦踢,那么他能做哪些視圖不能做的呢送滞?這里有一些UIView沒有暴露出來(lái)的CALayer的功能:
陰影,圓角辱挥,帶顏色的邊框
3D變換
非矩形范圍
透明遮罩
多級(jí)非線性動(dòng)畫
我們將會(huì)在后續(xù)章節(jié)中探索這些功能犁嗅,首先我們要關(guān)注一下在應(yīng)用程序當(dāng)中CALayer是怎樣被利用起來(lái)的。
使用圖層
首先我們來(lái)創(chuàng)建一個(gè)簡(jiǎn)單的項(xiàng)目晤碘,來(lái)操縱一些layer的屬性褂微。打開Xcode,使用Single View Application模板創(chuàng)建一個(gè)工程园爷。
在屏幕中央創(chuàng)建一個(gè)小視圖(大約200 X 200的尺寸)宠蚂,當(dāng)然你可以手工編碼,或者使用Interface Builder(隨你方便)童社。確保你的視圖控制器要添加一個(gè)視圖的屬性以便可以直接訪問它求厕。我們把它稱作layerView。
運(yùn)行項(xiàng)目,應(yīng)該能在淺灰色屏幕背景中看見一個(gè)白色方塊(圖1.3)呀癣,如果沒看見美浦,可能需要調(diào)整一下背景window或者view的顏色
這并沒有什么令人激動(dòng)的地方,我們來(lái)添加一個(gè)色塊十艾,在白色方塊中間添加一個(gè)小的藍(lán)色塊幽纷。
我們當(dāng)然可以簡(jiǎn)單地在已經(jīng)存在的UIView上添加一個(gè)子視圖(隨意用代碼或者IB),但這不能真正學(xué)到任何關(guān)于圖層的東西洁桌。
于是我們來(lái)創(chuàng)建一個(gè)CALayer尸折,并且把它作為我們視圖相關(guān)圖層的子圖層。盡管UIView類的接口中暴露了圖層屬性庆冕,但是標(biāo)準(zhǔn)的Xcode項(xiàng)目模板并沒有包含Core Animation相關(guān)頭文件康吵。所以如果我們不給項(xiàng)目添加合適的庫(kù),是不能夠使用任何圖層相關(guān)的方法或者訪問它的屬性访递。所以首先需要添加QuartzCore框架到Build Phases標(biāo)簽(圖1.4)晦嵌,然后在vc的.m文件中引入庫(kù)。
圖1.4 把QuartzCore庫(kù)添加到項(xiàng)目
之后就可以在代碼中直接引用CALayer的屬性和方法拷姿。在清單1.1中惭载,我們用創(chuàng)建了一個(gè)CALayer,設(shè)置了它的backgroundColor屬性响巢,然后添加到layerView背后相關(guān)圖層的子圖層(這段代碼的前提是通過IB創(chuàng)建了layerView并做好了連接)描滔,圖1.5顯示了結(jié)果。
清單1.1 給視圖添加一個(gè)藍(lán)色子圖層
#import "ViewController.h"
#import
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
?
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create sublayer
CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;
//add it to our view
[self.layerView.layer addSublayer:blueLayer];
}
@end
一個(gè)視圖只有一個(gè)相關(guān)聯(lián)的圖層(自動(dòng)創(chuàng)建)踪古,同時(shí)它也可以支持添加無(wú)數(shù)多個(gè)子圖層含长,從清單1.1可以看出,你可以顯示創(chuàng)建一個(gè)單獨(dú)的圖層伏穆,并且把它直接添加到視圖關(guān)聯(lián)圖層的子圖層拘泞。盡管可以這樣添加圖層,但往往我們只是見簡(jiǎn)單地處理視圖枕扫,他們關(guān)聯(lián)的圖層并不需要額外地手動(dòng)添加子圖層陪腌。
在Mac OS平臺(tái),10.8版本之前铡原,一個(gè)顯著的性能缺陷就是由于用了視圖層級(jí)而不是單獨(dú)在一個(gè)視圖內(nèi)使用CALayer樹狀層級(jí)偷厦。但是在iOS平臺(tái),使用輕量級(jí)的UIView類并沒有顯著的性能影響(當(dāng)然在Mac OS 10.8之后燕刻,NSView的性能同樣也得到很大程度的提高)只泼。
使用圖層關(guān)聯(lián)的視圖而不是CALayer的好處在于,你能在使用所有CALayer底層特性的同時(shí)卵洗,也可以使用UIView的高級(jí)API(比如自動(dòng)排版请唱,布局和事件處理)弥咪。
然而,當(dāng)滿足以下條件的時(shí)候十绑,你可能更需要使用CALayer而不是UIView
開發(fā)同時(shí)可以在Mac OS上運(yùn)行的跨平臺(tái)應(yīng)用
使用多種CALayer的子類(見第六章聚至,“特殊的圖層“),并且不想創(chuàng)建額外的UIView去包封裝它們所有
做一些對(duì)性能特別挑剔的工作本橙,比如對(duì)UIView一些可忽略不計(jì)的操作都會(huì)引起顯著的不同(盡管如此扳躬,你可能會(huì)直接想使用OpenGL繪圖)
但是這些例子都很少見,總的來(lái)說甚亭,處理視圖會(huì)比單獨(dú)處理圖層更加方便贷币。
總結(jié)
這一章闡述了圖層的樹狀結(jié)構(gòu),說明了如何在iOS中由UIView的層級(jí)關(guān)系形成的一種平行的CALayer層級(jí)關(guān)系亏狰,在后面的實(shí)驗(yàn)中役纹,我們創(chuàng)建了自己的CALayer,并把它添加到圖層樹中暇唾。
在第二章促脉,“圖層關(guān)聯(lián)的圖片”,我們將要研究一下CALayer關(guān)聯(lián)的圖片策州,以及Core Animation提供的操作顯示的一些特性瘸味。----------------------------------------------------------------------------------------------------
(二)寄宿圖
圖片勝過千言萬(wàn)語(yǔ),界面抵得上千圖片 ——Ben Shneiderman
我們?cè)诘谝徽隆簣D層樹』中介紹了CALayer類并創(chuàng)建了一個(gè)簡(jiǎn)單的有藍(lán)色背景的圖層够挂。背景顏色還好啦硫戈,但是如果它僅僅是展現(xiàn)了一個(gè)單調(diào)的顏色未免也太無(wú)聊了。事實(shí)上CALayer類能夠包含一張你喜歡的圖片下硕,這一章節(jié)我們將來(lái)探索CALayer的寄宿圖(即圖層中包含的圖)。
contents屬性
CALyer 有一個(gè)屬性叫做contents汁胆,這個(gè)屬性的類型被定義為id梭姓,意味著它可以是任何類型的對(duì)象。在這種情況下嫩码,你可以給contents屬性賦任何值誉尖,你的app仍然能夠編譯通過。但是铸题,在實(shí)踐中铡恕,如果你給contents賦的不是CGImage,那么你得到的圖層將是空白的丢间。
contents這個(gè)奇怪的表現(xiàn)是由Mac OS的歷史原因造成的探熔。它之所以被定義為id類型,是因?yàn)樵贛ac OS系統(tǒng)上烘挫,這個(gè)屬性對(duì)CGImage和NSImage類型的值都起作用诀艰。如果你試圖在iOS平臺(tái)上將UIImage的值賦給它,只能得到一個(gè)空白的圖層。一些初識(shí)Core Animation的iOS開發(fā)者可能會(huì)對(duì)這個(gè)感到困惑其垄。
頭疼的不僅僅是我們剛才提到的這個(gè)問題苛蒲。事實(shí)上,你真正要賦值的類型應(yīng)該是CGImageRef绿满,它是一個(gè)指向CGImage結(jié)構(gòu)的指針臂外。UIImage有一個(gè)CGImage屬性,它返回一個(gè)"CGImageRef",如果你想把這個(gè)值直接賦值給CALayer的contents喇颁,那你將會(huì)得到一個(gè)編譯錯(cuò)誤漏健。因?yàn)镃GImageRef并不是一個(gè)真正的Cocoa對(duì)象,而是一個(gè)Core Foundation類型无牵。
盡管Core Foundation類型跟Cocoa對(duì)象在運(yùn)行時(shí)貌似很像(被稱作toll-free bridging)漾肮,他們并不是類型兼容的,不過你可以通過bridged關(guān)鍵字轉(zhuǎn)換茎毁。如果要給圖層的寄宿圖賦值克懊,你可以按照以下這個(gè)方法:
layer.contents = (__bridge id)image.CGImage;
如果你沒有使用ARC(自動(dòng)引用計(jì)數(shù)),你就不需要__bridge這部分七蜘。但是谭溉,你干嘛不用ARC?橡卤!
讓我們來(lái)繼續(xù)修改我們?cè)诘谝徽滦陆ǖ墓こ贪缒睿员隳軌蛘故疽粡垐D片而不僅僅是一個(gè)背景色。我們已經(jīng)用代碼的方式建立一個(gè)圖層碧库,那我們就不需要額外的圖層了柜与。那么我們就直接把layerView的宿主圖層的contents屬性設(shè)置成圖片。
清單2.1 更新后的代碼嵌灰。
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad]; //load an image
UIImage *image = [UIImage imageNamed:@"Snowman.png"];
//add it directly to our view's layer
self.layerView.layer.contents = (__bridge id)image.CGImage;
}
@end
圖表2.1 在UIView的宿主圖層中顯示一張圖片
我們用這些簡(jiǎn)單的代碼做了一件很有趣的事情:我們利用CALayer在一個(gè)普通的UIView中顯示了一張圖片弄匕。這不是一個(gè)UIImageView,它不是我們通常用來(lái)展示圖片的方法沽瞭。通過直接操作圖層迁匠,我們使用了一些新的函數(shù),使得UIView更加有趣了驹溃。
contentGravity
你可能已經(jīng)注意到了我們的雪人看起來(lái)有點(diǎn)城丧。。豌鹤。胖 ==亡哄! 我們加載的圖片并不剛好是一個(gè)方的,為了適應(yīng)這個(gè)視圖布疙,它有一點(diǎn)點(diǎn)被拉伸了磺平。在使用UIImageView的時(shí)候遇到過同樣的問題魂仍,解決方法就是把contentMode屬性設(shè)置成更合適的值,像這樣:
view.contentMode = UIViewContentModeScaleAspectFit;
這個(gè)方法基本和我們遇到的情況的解決方法已經(jīng)接近了(你可以試一下 :) )拣挪,不過UIView大多數(shù)視覺相關(guān)的屬性比如contentMode擦酌,對(duì)這些屬性的操作其實(shí)是對(duì)對(duì)應(yīng)圖層的操作。
CALayer與contentMode對(duì)應(yīng)的屬性叫做contentsGravity菠劝,但是它是一個(gè)NSString類型赊舶,而不是像對(duì)應(yīng)的UIKit部分,那里面的值是枚舉赶诊。contentsGravity可選的常量值有以下一些:
kCAGravityCenter
kCAGravityTop
kCAGravityBottom
kCAGravityLeft
kCAGravityRight
kCAGravityTopLeft
kCAGravityTopRight
kCAGravityBottomLeft
kCAGravityBottomRight
kCAGravityResize
kCAGravityResizeAspect
kCAGravityResizeAspectFill
和cotentMode一樣笼平,contentsGravity的目的是為了決定內(nèi)容在圖層的邊界中怎么對(duì)齊,我們將使用kCAGravityResizeAspect舔痪,它的效果等同于UIViewContentModeScaleAspectFit寓调, 同時(shí)它還能在圖層中等比例拉伸以適應(yīng)圖層的邊界。
self.layerView.layer.contentsGravity = kCAGravityResizeAspect;
圖2.2 可以看到結(jié)果
contentsScale
contentsScale屬性定義了寄宿圖的像素尺寸和視圖大小的比例锄码,默認(rèn)情況下它是一個(gè)值為1.0的浮點(diǎn)數(shù)夺英。
contentsScale的目的并不是那么明顯。它并不是總會(huì)對(duì)屏幕上的寄宿圖有影響滋捶。如果你嘗試對(duì)我們的例子設(shè)置不同的值痛悯,你就會(huì)發(fā)現(xiàn)根本沒任何影響。因?yàn)閏ontents由于設(shè)置了contentsGravity屬性重窟,所以它已經(jīng)被拉伸以適應(yīng)圖層的邊界载萌。
如果你只是單純地想放大圖層的contents圖片,你可以通過使用圖層的transform和affineTransform屬性來(lái)達(dá)到這個(gè)目的(見第五章『Transforms』巡扇,里面對(duì)此有解釋)扭仁,這(指放大)也不是contengsScale的目的所在.
contentsScale屬性其實(shí)屬于支持高分辨率(又稱Hi-DPI或Retina)屏幕機(jī)制的一部分。它用來(lái)判斷在繪制圖層的時(shí)候應(yīng)該為寄宿圖創(chuàng)建的空間大小厅翔,和需要顯示的圖片的拉伸度(假設(shè)并沒有設(shè)置contentsGravity屬性)斋枢。UIView有一個(gè)類似功能但是非常少用到的contentScaleFactor屬性。
如果contentsScale設(shè)置為1.0知给,將會(huì)以每個(gè)點(diǎn)1個(gè)像素繪制圖片,如果設(shè)置為2.0描姚,則會(huì)以每個(gè)點(diǎn)2個(gè)像素繪制圖片涩赢,這就是我們熟知的Retina屏幕。(如果你對(duì)像素和點(diǎn)的概念不是很清楚的話轩勘,這個(gè)章節(jié)的后面部分將會(huì)對(duì)此做出解釋)筒扒。
這并不會(huì)對(duì)我們?cè)谑褂胟CAGravityResizeAspect時(shí)產(chǎn)生任何影響,因?yàn)樗褪抢靾D片以適應(yīng)圖層而已绊寻,根本不會(huì)考慮到分辨率問題花墩。但是如果我們把contentsGravity設(shè)置為kCAGravityCenter(這個(gè)值并不會(huì)拉伸圖片)悬秉,那將會(huì)有很明顯的變化(如圖2.3)
如你所見,我們的雪人不僅有點(diǎn)大還有點(diǎn)像素的顆粒感冰蘑。那是因?yàn)楹蚒IImage不同和泌,CGImage沒有拉伸的概念。當(dāng)我們使用UIImage類去讀取我們的雪人圖片的時(shí)候祠肥,他讀取了高質(zhì)量的Retina版本的圖片武氓。但是當(dāng)我們用CGImage來(lái)設(shè)置我們的圖層的內(nèi)容時(shí),拉伸這個(gè)因素在轉(zhuǎn)換的時(shí)候就丟失了仇箱。不過我們可以通過手動(dòng)設(shè)置contentsScale來(lái)修復(fù)這個(gè)問題(如2.2清單)县恕,圖2.4是結(jié)果
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad]; //load an image
UIImage *image = [UIImage imageNamed:@"Snowman.png"]; //add it directly to our view's layer
self.layerView.layer.contents = (__bridge id)image.CGImage; //center the image
self.layerView.layer.contentsGravity = kCAGravityCenter;
//set the contentsScale to match image
self.layerView.layer.contentsScale = image.scale;
}
@end
當(dāng)用代碼的方式來(lái)處理寄宿圖的時(shí)候,一定要記住要手動(dòng)的設(shè)置圖層的contentsScale屬性剂桥,否則忠烛,你的圖片在Retina設(shè)備上就顯示得不正確啦。代碼如下:
layer.contentsScale = [UIScreen mainScreen].scale;
maskToBounds
現(xiàn)在我們的雪人總算是顯示了正確的大小权逗,不過你也許已經(jīng)發(fā)現(xiàn)了另外一些事情:他超出了視圖的邊界美尸。默認(rèn)情況下,UIView仍然會(huì)繪制超過邊界的內(nèi)容或是子視圖旬迹,在CALayer下也是這樣的火惊。
UIView有一個(gè)叫做clipsToBounds的屬性可以用來(lái)決定是否顯示超出邊界的內(nèi)容,CALayer對(duì)應(yīng)的屬性叫做masksToBounds奔垦,把它設(shè)置為YES屹耐,雪人就在邊界里啦~(如圖2.5)
contentsRect
CALayer的contentsRect屬性允許我們?cè)趫D層邊框里顯示寄宿圖的一個(gè)子域。這涉及到圖片是如何顯示和拉伸的椿猎,所以要比contentsGravity靈活多了
和bounds惶岭,frame不同,contentsRect不是按點(diǎn)來(lái)計(jì)算的犯眠,它使用了單位坐標(biāo)按灶,單位坐標(biāo)指定在0到1之間,是一個(gè)相對(duì)值(像素和點(diǎn)就是絕對(duì)值)筐咧。所以他們是相對(duì)與寄宿圖的尺寸的鸯旁。iOS使用了以下的坐標(biāo)系統(tǒng):
點(diǎn) —— 在iOS和Mac OS中最常見的坐標(biāo)體系。點(diǎn)就像是虛擬的像素量蕊,也被稱作邏輯像素铺罢。在標(biāo)準(zhǔn)設(shè)備上,一個(gè)點(diǎn)就是一個(gè)像素残炮,但是在Retina設(shè)備上韭赘,一個(gè)點(diǎn)等于2*2個(gè)像素。iOS用點(diǎn)作為屏幕的坐標(biāo)測(cè)算體系就是為了在Retina設(shè)備和普通設(shè)備上能有一致的視覺效果势就。
像素 —— 物理像素坐標(biāo)并不會(huì)用來(lái)屏幕布局泉瞻,但是仍然與圖片有相對(duì)關(guān)系脉漏。UIImage是一個(gè)屏幕分辨率解決方案,所以指定點(diǎn)來(lái)度量大小袖牙。但是一些底層的圖片表示如CGImage就會(huì)使用像素侧巨,所以你要清楚在Retina設(shè)備和普通設(shè)備上,他們表現(xiàn)出來(lái)了不同的大小贼陶。
單位 —— 對(duì)于與圖片大小或是圖層邊界相關(guān)的顯示刃泡,單位坐標(biāo)是一個(gè)方便的度量方式, 當(dāng)大小改變的時(shí)候碉怔,也不需要再次調(diào)整烘贴。單位坐標(biāo)在OpenGL這種紋理坐標(biāo)系統(tǒng)中用得很多,Core Animation中也用到了單位坐標(biāo)撮胧。
默認(rèn)的contentsRect是{0, 0, 1, 1}桨踪,這意味著整個(gè)寄宿圖默認(rèn)都是可見的,如果我們指定一個(gè)小一點(diǎn)的矩形芹啥,圖片就會(huì)被裁剪(如圖2.6)
事實(shí)上給contentsRect設(shè)置一個(gè)負(fù)數(shù)的原點(diǎn)或是大于{1, 1}的尺寸也是可以的锻离。這種情況下,最外面的像素會(huì)被拉伸以填充剩下的區(qū)域墓怀。
contentsRect在app中最有趣的地方在于一個(gè)叫做image sprites(圖片拼合)的用法汽纠。如果你有游戲編程的經(jīng)驗(yàn),那么你一定對(duì)圖片拼合的概念很熟悉傀履,圖片能夠在屏幕上獨(dú)立地變更位置虱朵。拋開游戲編程不談,這個(gè)技術(shù)常用來(lái)指代載入拼合的圖片钓账,跟移動(dòng)圖片一點(diǎn)關(guān)系也沒有碴犬。
典型地,圖片拼合后可以打包整合到一張大圖上一次性載入梆暮。相比多次載入不同的圖片服协,這樣做能夠帶來(lái)很多方面的好處:內(nèi)存使用,載入時(shí)間啦粹,渲染性能等等
2D游戲引擎入Cocos2D使用了拼合技術(shù)偿荷,它使用OpenGL來(lái)顯示圖片。不過我們可以使用拼合在一個(gè)普通的UIKit應(yīng)用中唠椭,對(duì)跳纳!就是使用contentsRect
首先,我們需要一個(gè)拼合后的圖表 —— 一個(gè)包含小一些的拼合圖的大圖片泪蔫。如圖2.7所示:
接下來(lái),我們要在app中載入并顯示這些拼合圖喘批。規(guī)則很簡(jiǎn)單:像平常一樣載入我們的大圖撩荣,然后把它賦值給四個(gè)獨(dú)立的圖層的contents铣揉,然后設(shè)置每個(gè)圖層的contentsRect來(lái)去掉我們不想顯示的部分。
我們的工程中需要一些額外的視圖餐曹。(為了避免太多代碼逛拱。我們將使用Interface Builder來(lái)拜訪他們的位置,如果你愿意還是可以用代碼的方式來(lái)實(shí)現(xiàn)的)台猴。清單2.3有需要的代碼朽合,圖2.8展示了結(jié)果
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *coneView;
@property (nonatomic, weak) IBOutlet UIView *shipView;
@property (nonatomic, weak) IBOutlet UIView *iglooView;
@property (nonatomic, weak) IBOutlet UIView *anchorView;
@end
@implementation ViewController
- (void)addSpriteImage:(UIImage *)image withContentRect:(CGRect)rect ?toLayer:(CALayer *)layer //set image
{
layer.contents = (__bridge id)image.CGImage;
//scale contents to fit
layer.contentsGravity = kCAGravityResizeAspect;
//set contentsRect
layer.contentsRect = rect;
}
- (void)viewDidLoad
{
[super viewDidLoad]; //load sprite sheet
UIImage *image = [UIImage imageNamed:@"Sprites.png"];
//set igloo sprite
[self addSpriteImage:image withContentRect:CGRectMake(0, 0, 0.5, 0.5) toLayer:self.iglooView.layer];
//set cone sprite
[self addSpriteImage:image withContentRect:CGRectMake(0.5, 0, 0.5, 0.5) toLayer:self.coneView.layer];
//set anchor sprite
[self addSpriteImage:image withContentRect:CGRectMake(0, 0.5, 0.5, 0.5) toLayer:self.anchorView.layer];
//set spaceship sprite
[self addSpriteImage:image withContentRect:CGRectMake(0.5, 0.5, 0.5, 0.5) toLayer:self.shipView.layer];
}
@end
拼合不僅給app提供了一個(gè)整潔的載入方式,還有效地提高了載入性能(單張大圖比多張小圖載入地更快)饱狂,但是如果有手動(dòng)安排的話曹步,他們還是有一些不方便的,如果你需要在一個(gè)已經(jīng)創(chuàng)建好的品和圖上做一些尺寸上的修改或者其他變動(dòng)休讳,無(wú)疑是比較麻煩的讲婚。
Mac上有一些商業(yè)軟件可以為你自動(dòng)拼合圖片,這些工具自動(dòng)生成一個(gè)包含拼合后的坐標(biāo)的XML或者plist文件俊柔,拼合圖片的使用大大簡(jiǎn)化筹麸。這個(gè)文件可以和圖片一同載入,并給每個(gè)拼合的圖層設(shè)置contentsRect雏婶,這樣開發(fā)者就不用手動(dòng)寫代碼來(lái)擺放位置了物赶。
這些文件通常在OpenGL游戲中使用,不過呢留晚,你要是有興趣在一些常見的app中使用拼合技術(shù)酵紫,那么一個(gè)叫做LayerSprites的開源庫(kù)(https://github.com/nicklockwood/LayerSprites),它能夠讀取Cocos2D格式中的拼合圖并在普通的Core Animation層中顯示出來(lái)倔丈。
contentsCenter
本章我們介紹的最后一個(gè)和內(nèi)容有關(guān)的屬性是contentsCenter憨闰,看名字你可能會(huì)以為它可能跟圖片的位置有關(guān),不過這名字著實(shí)誤導(dǎo)了你需五。contentsCenter其實(shí)是一個(gè)CGRect鹉动,它定義了一個(gè)固定的邊框和一個(gè)在圖層上可拉伸的區(qū)域。 改變contentsCenter的值并不會(huì)影響到寄宿圖的顯示宏邮,除非這個(gè)圖層的大小改變了泽示,你才看得到效果。
默認(rèn)情況下蜜氨,contentsCenter是{0, 0, 1, 1}械筛,這意味著如果大小(由conttensGravity決定)改變了,那么寄宿圖將會(huì)均勻地拉伸開飒炎。但是如果我們?cè)黾釉c(diǎn)的值并減小尺寸埋哟。我們會(huì)在圖片的周圍創(chuàng)造一個(gè)邊框。圖2.9展示了contentsCenter設(shè)置為{0.25, 0.25, 0.5, 0.5}的效果。
這意味著我們可以隨意重設(shè)尺寸赤赊,邊框仍然會(huì)是連續(xù)的闯狱。他工作起來(lái)的效果和UIImage里的-resizableImageWithCapInsets: 方法效果非常類似,只是它可以運(yùn)用到任何寄宿圖抛计,甚至包括在Core Graphics運(yùn)行時(shí)繪制的圖形(本章稍后會(huì)講到)哄孤。
清單2.4 演示了如何編寫這些可拉伸視圖。不過吹截,contentsCenter的另一個(gè)很酷的特性就是瘦陈,它可以在Interface Builder里面配置,根本不用寫代碼波俄。如圖2.11
清單2.4 用contentsCenter設(shè)置可拉伸視圖
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *button1;
@property (nonatomic, weak) IBOutlet UIView *button2;
@end
@implementation ViewController
- (void)addStretchableImage:(UIImage *)image withContentCenter:(CGRect)rect toLayer:(CALayer *)layer
{
//set image
layer.contents = (__bridge id)image.CGImage;
//set contentsCenter
layer.contentsCenter = rect;
}
- (void)viewDidLoad
{
[super viewDidLoad]; //load button image
UIImage *image = [UIImage imageNamed:@"Button.png"];
//set button 1
[self addStretchableImage:image withContentCenter:CGRectMake(0.25, 0.25, 0.5, 0.5) toLayer:self.button1.layer];
//set button 2
[self addStretchableImage:image withContentCenter:CGRectMake(0.25, 0.25, 0.5, 0.5) toLayer:self.button2.layer];
}
@end
Custome Drawing
給contents賦CGImage的值不是唯一的設(shè)置寄宿圖的方法晨逝。我們也可以直接用Core Graphics直接繪制寄宿圖。能夠通過繼承UIView并實(shí)現(xiàn)-drawRect:方法來(lái)自定義繪制弟断。
-drawRect: 方法沒有默認(rèn)的實(shí)現(xiàn)咏花,因?yàn)閷?duì)UIView來(lái)說,寄宿圖并不是必須的阀趴,它不在意那到底是單調(diào)的顏色還是有一個(gè)圖片的實(shí)例昏翰。如果UIView檢測(cè)到-drawRect: 方法被調(diào)用了,它就會(huì)為視圖分配一個(gè)寄宿圖刘急,這個(gè)寄宿圖的像素尺寸等于視圖大小乘以 contentsScale的值棚菊。
如果你不需要寄宿圖,那就不要?jiǎng)?chuàng)建這個(gè)方法了叔汁,這會(huì)造成CPU資源和內(nèi)存的浪費(fèi)统求,這也是為什么蘋果建議:如果沒有自定義繪制的任務(wù)就不要在子類中寫一個(gè)空的-drawRect:方法。
當(dāng)視圖在屏幕上出現(xiàn)的時(shí)候 -drawRect:方法就會(huì)被自動(dòng)調(diào)用据块。-drawRect:方法里面的代碼利用Core Graphics去繪制一個(gè)寄宿圖码邻,然后內(nèi)容就會(huì)被緩存起來(lái)直到它需要被更新(通常是因?yàn)殚_發(fā)者調(diào)用了-setNeedsDisplay方法,盡管影響到表現(xiàn)效果的屬性值被更改時(shí)另假,一些視圖類型會(huì)被自動(dòng)重繪像屋,如bounds屬性)。雖然-drawRect:方法是一個(gè)UIView方法边篮,事實(shí)上都是底層的CALayer安排了重繪工作和保存了因此產(chǎn)生的圖片己莺。
CALayer有一個(gè)可選的delegate屬性,實(shí)現(xiàn)了CALayerDelegate協(xié)議戈轿,當(dāng)CALayer需要一個(gè)內(nèi)容特定的信息時(shí)凌受,就會(huì)從協(xié)議中請(qǐng)求。CALayerDelegate是一個(gè)非正式協(xié)議思杯,其實(shí)就是說沒有CALayerDelegate @protocol可以讓你在類里面引用啦胜蛉。你只需要調(diào)用你想調(diào)用的方法,CALayer會(huì)幫你做剩下的。(delegate屬性被聲明為id類型誊册,所有的代理方法都是可選的)奈梳。
當(dāng)需要被重繪時(shí),CALayer會(huì)請(qǐng)求它的代理給他一個(gè)寄宿圖來(lái)顯示解虱。它通過調(diào)用下面這個(gè)方法做到的:
-(void)displayLayer:(CALayerCALayer *)layer;
趁著這個(gè)機(jī)會(huì),如果代理想直接設(shè)置contents屬性的話漆撞,它就可以這么做殴泰,不然沒有別的方法可以調(diào)用了。如果代理不實(shí)現(xiàn)-displayLayer:方法浮驳,CALayer就會(huì)轉(zhuǎn)而嘗試調(diào)用下面這個(gè)方法:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
在調(diào)用這個(gè)方法之前悍汛,CALayer創(chuàng)建了一個(gè)合適尺寸的空寄宿圖(尺寸由bounds和contentsScale決定)和一個(gè)Core Graphics的繪制上下文環(huán)境,為繪制寄宿圖做準(zhǔn)備至会,他作為ctx參數(shù)傳入离咐。
讓我們來(lái)繼續(xù)第一章的項(xiàng)目讓它實(shí)現(xiàn)CALayerDelegate并做一些繪圖工作吧(見清單2.5).圖2.12是他的結(jié)果
清單2.5 實(shí)現(xiàn)CALayerDelegate
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
?
//create sublayer
CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;
//set controller as layer delegate
blueLayer.delegate = self;
//ensure that layer backing image uses correct scale
blueLayer.contentsScale = [UIScreen mainScreen].scale; //add layer to our view
[self.layerView.layer addSublayer:blueLayer];
//force layer to redraw
[blueLayer display];
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
//draw a thick red circle
CGContextSetLineWidth(ctx, 10.0f);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextStrokeEllipseInRect(ctx, layer.bounds);
}
@end
注意一下一些有趣的事情:
我們?cè)赽lueLayer上顯式地調(diào)用了-display。不同于UIView奉件,當(dāng)圖層顯示在屏幕上時(shí)宵蛀,CALayer不會(huì)自動(dòng)重繪它的內(nèi)容。它把重繪的決定權(quán)交給了開發(fā)者县貌。
盡管我們沒有用masksToBounds屬性术陶,繪制的那個(gè)圓仍然沿邊界被裁剪了。這是因?yàn)楫?dāng)你使用CALayerDelegate繪制寄宿圖的時(shí)候煤痕,并沒有對(duì)超出邊界外的內(nèi)容提供繪制支持梧宫。
現(xiàn)在你理解了CALayerDelegate,并知道怎么使用它摆碉。但是除非你創(chuàng)建了一個(gè)單獨(dú)的圖層塘匣,你幾乎沒有機(jī)會(huì)用到CALayerDelegate協(xié)議。因?yàn)楫?dāng)UIView創(chuàng)建了它的宿主圖層時(shí)巷帝,它就會(huì)自動(dòng)地把圖層的delegate設(shè)置為它自己忌卤,并提供了一個(gè)-displayLayer:的實(shí)現(xiàn),那所有的問題就都沒了锅睛。
當(dāng)使用寄宿了視圖的圖層的時(shí)候埠巨,你也不必實(shí)現(xiàn)-displayLayer:和-drawLayer:inContext:方法咦繪制你的寄宿圖。通常做法是實(shí)現(xiàn)UIView的-drawRect:方法现拒,UIView就會(huì)幫你做完剩下的工作辣垒,包括在需要重繪的時(shí)候調(diào)用-display方法。
總結(jié)
本章介紹了寄宿圖和一些相關(guān)的屬性印蔬。你學(xué)到了如何顯示和放置圖片勋桶, 使用拼合技術(shù)來(lái)顯示, 以及用CALayerDelegate和Core Graphics來(lái)繪制圖層內(nèi)容。
在第三章例驹,"圖層幾何學(xué)"中捐韩,我們將會(huì)探討一下圖層的幾何,觀察他們是如何放置和改變相互的尺寸的鹃锈。-------------------------------------------------------------------------------------------------------------------------------------
(三)圖層幾何學(xué)
不熟悉幾何學(xué)的人就不要來(lái)這里了 --柏拉圖學(xué)院入口的簽名
在第二章里面荤胁,我們介紹了圖層背后的圖片,和一些控制圖層坐標(biāo)和旋轉(zhuǎn)的屬性屎债。在這一章中仅政,我們將要看一看圖層內(nèi)部是如何根據(jù)父圖層和兄弟圖層來(lái)控制位置和尺寸的。另外我們也會(huì)涉及如何管理圖層的幾何結(jié)構(gòu)盆驹,以及它是如何被自動(dòng)調(diào)整和自動(dòng)布局影響的圆丹。
布局
UIView有三個(gè)比較重要的布局屬性:frame,bounds和center躯喇,CALayer對(duì)應(yīng)地叫做frame辫封,bounds和position。為了能清楚區(qū)分廉丽,圖層用了“position”倦微,視圖用了“center”,但是他們都代表同樣的值正压。
frame代表了圖層的外部坐標(biāo)(也就是在父圖層上占據(jù)的空間)璃诀,bounds是內(nèi)部坐標(biāo)({0, 0}通常是圖層的左上角),center和position都代表了相對(duì)于父圖層anchorPoint所在的位置蔑匣。anchorPoint的屬性將會(huì)在后續(xù)介紹到劣欢,現(xiàn)在把它想成圖層的中心點(diǎn)就好了。圖3.1顯示了這些屬性是如何相互依賴的裁良。
視圖的frame凿将,bounds和center屬性僅僅是存取方法,當(dāng)操縱視圖的frame价脾,實(shí)際上是在改變位于視圖下方CALayer的frame牧抵,不能夠獨(dú)立于圖層之外改變視圖的frame。
對(duì)于視圖或者圖層來(lái)說侨把,frame并不是一個(gè)非常清晰的屬性犀变,它其實(shí)是一個(gè)虛擬屬性,是根據(jù)bounds秋柄,position和transform計(jì)算而來(lái)获枝,所以當(dāng)其中任何一個(gè)值發(fā)生改變,frame都會(huì)變化骇笔。相反省店,改變frame的值同樣會(huì)影響到他們當(dāng)中的值
記住當(dāng)對(duì)圖層做變換的時(shí)候嚣崭,比如旋轉(zhuǎn)或者縮放,frame實(shí)際上代表了覆蓋在圖層旋轉(zhuǎn)之后的整個(gè)軸對(duì)齊的矩形區(qū)域懦傍,也就是說frame的寬高可能和bounds的寬高不再一致了(圖3.2)
錨點(diǎn)
之前提到過雹舀,視圖的center屬性和圖層的position屬性都指定了相對(duì)于父圖層anchorPoint的位置。圖層的anchorPoint通過position來(lái)控制它的frame的位置粗俱,你可以認(rèn)為anchorPoint是用來(lái)移動(dòng)圖層的把柄说榆。
默認(rèn)來(lái)說,anchorPoint位于圖層的中點(diǎn)寸认,所以圖層的將會(huì)以這個(gè)點(diǎn)為中心放置娱俺。anchorPoint屬性并沒有被UIView接口暴露出來(lái),這也是視圖的position屬性被叫做“center”的原因废麻。但是圖層的anchorPoint可以被移動(dòng),比如你可以把它置于圖層frame的左上角模庐,于是圖層的內(nèi)容將會(huì)向右下角的position方向移動(dòng)(圖3.3)烛愧,而不是居中了。
和第二章提到的contentsRect和contentsCenter屬性類似掂碱,anchorPoint用單位坐標(biāo)來(lái)描述怜姿,也就是圖層的相對(duì)坐標(biāo),圖層左上角是{0, 0}疼燥,右下角是{1, 1}沧卢,因此默認(rèn)坐標(biāo)是{0.5, 0.5}。anchorPoint可以通過指定x和y值小于0或者大于1醉者,使它放置在圖層范圍之外绵咱。
注意在圖3.3中奢浑,當(dāng)改變了anchorPoint,position屬性保持固定的值并沒有發(fā)生改變,但是frame卻移動(dòng)了柳爽。
那在什么場(chǎng)合需要改變anchorPoint呢?既然我們可以隨意改變圖層位置损谦,那改變anchorPoint不會(huì)造成困惑么朦乏?為了舉例說明,我們來(lái)舉一個(gè)實(shí)用的例子粒竖,創(chuàng)建一個(gè)模擬鬧鐘的項(xiàng)目颅崩。
鐘面和鐘表由四張圖片組成(圖3.4),為了簡(jiǎn)單說明蕊苗,我們還是用傳統(tǒng)的方式來(lái)裝載和加載圖片沿后,使用四個(gè)UIImageView實(shí)例(當(dāng)然你也可以用正常的視圖,設(shè)置他們圖層的contents圖片)朽砰。
鬧鐘的組件通過IB來(lái)排列(圖3.5)得运,這些圖片視圖嵌套在一個(gè)容器視圖之內(nèi)膝蜈,并且自動(dòng)調(diào)整和自動(dòng)布局都被禁用了。這是因?yàn)樽詣?dòng)調(diào)整會(huì)影響到視圖的frame熔掺,而根據(jù)圖3.2的演示饱搏,當(dāng)視圖旋轉(zhuǎn)的時(shí)候,frame是會(huì)發(fā)生改變的置逻,這將會(huì)導(dǎo)致一些布局上的失靈推沸。
我們用NSTimer來(lái)更新鬧鐘,使用視圖的transform屬性來(lái)旋轉(zhuǎn)鐘表(如果你對(duì)這個(gè)屬性不太熟悉券坞,不要著急鬓催,我們將會(huì)在第5章“變換”當(dāng)中詳細(xì)說明),具體代碼見清單3.1
清單3.1 Clock
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIImageView *hourHand;
@property (nonatomic, weak) IBOutlet UIImageView *minuteHand;
@property (nonatomic, weak) IBOutlet UIImageView *secondHand;
@property (nonatomic, weak) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//start timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
?
//set initial hand positions
[self tick];
}
- (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]];
CGFloat hoursAngle = (components.hour / 12.0) * M_PI * 2.0;
//calculate hour hand angle //calculate minute hand angle
CGFloat minsAngle = (components.minute / 60.0) * M_PI * 2.0;
//calculate second hand angle
CGFloat secsAngle = (components.second / 60.0) * M_PI * 2.0;
//rotate hands
self.hourHand.transform = CGAffineTransformMakeRotation(hoursAngle);
self.minuteHand.transform = CGAffineTransformMakeRotation(minsAngle);
self.secondHand.transform = CGAffineTransformMakeRotation(secsAngle);
}
@end
運(yùn)行項(xiàng)目恨锚,看起來(lái)有點(diǎn)奇怪(圖3.6)宇驾,因?yàn)殓姳淼膱D片在圍繞著中心旋轉(zhuǎn),這并不是我們期待的一個(gè)支點(diǎn)猴伶。
你也許會(huì)認(rèn)為可以在Interface Builder當(dāng)中調(diào)整指針圖片的位置來(lái)解決,但其實(shí)并不能達(dá)到目的他挎,因?yàn)槿绻环旁阽娒嬷虚g的話筝尾,同樣不能正確的旋轉(zhuǎn)。
也許在圖片末尾添加一個(gè)透明空間也是個(gè)解決方案办桨,但這樣會(huì)讓圖片變大筹淫,也會(huì)消耗更多的內(nèi)存,這樣并不優(yōu)雅呢撞。
更好的方案是使用anchorPoint屬性损姜,我們來(lái)在-viewDidLoad方法中添加幾行代碼來(lái)給每個(gè)鐘指針的anchorPoint做一些平移(清單3.2),圖3.7顯示了正確的結(jié)果殊霞。
清單3.2
- (void)viewDidLoad
{
[super viewDidLoad];
// adjust anchor points
self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
// start timer
}
坐標(biāo)系
和視圖一樣,圖層在圖層樹當(dāng)中也是相對(duì)于父圖層按層級(jí)關(guān)系放置脓鹃,一個(gè)圖層的position依賴于它父圖層的bounds逸尖,如果父圖層發(fā)生了移動(dòng),它的所有子圖層也會(huì)跟著移動(dòng)瘸右。
這樣對(duì)于放置圖層會(huì)更加方便娇跟,因?yàn)槟憧梢酝ㄟ^移動(dòng)根圖層來(lái)將它的子圖層作為一個(gè)整體來(lái)移動(dòng),但是有時(shí)候你需要知道一個(gè)圖層的絕對(duì)位置太颤,或者是相對(duì)于另一個(gè)圖層的位置苞俘,而不是它當(dāng)前父圖層的位置。
CALayer給不同坐標(biāo)系之間的圖層轉(zhuǎn)換提供了一些工具類方法:
- (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer;
- (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer *)layer;
- (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer *)layer;
- (CGRect)convertRect:(CGRect)rect toLayer:(CALayer *)layer;
這些方法可以把定義在一個(gè)圖層坐標(biāo)系下的點(diǎn)或者矩形轉(zhuǎn)換成另一個(gè)圖層坐標(biāo)系下的點(diǎn)或者矩形
翻轉(zhuǎn)的幾何結(jié)構(gòu)
常規(guī)說來(lái)龄章,在iOS上吃谣,一個(gè)圖層的position位于父圖層的左上角乞封,但是在Mac OS上,通常是位于左下角岗憋。Core Animation可以通過geometryFlipped屬性來(lái)適配這兩種情況肃晚,它決定了一個(gè)圖層的坐標(biāo)是否相對(duì)于父圖層垂直翻轉(zhuǎn),是一個(gè)BOOL類型仔戈。在iOS上通過設(shè)置它為YES意味著它的子圖層將會(huì)被垂直翻轉(zhuǎn)关串,也就是將會(huì)沿著底部排版而不是通常的頂部(它的所有子圖層也同理,除非把它們的geometryFlipped屬性也設(shè)為YES)监徘。
Z坐標(biāo)軸
和UIView嚴(yán)格的二維坐標(biāo)系不同晋修,CALayer存在于一個(gè)三維空間當(dāng)中。除了我們已經(jīng)討論過的position和anchorPoint屬性之外凰盔,CALayer還有另外兩個(gè)屬性墓卦,zPosition和anchorPointZ,二者都是在Z軸上描述圖層位置的浮點(diǎn)類型户敬。
注意這里并沒有更深的屬性來(lái)描述由寬和高做成的bounds了落剪,圖層是一個(gè)完全扁平的對(duì)象,你可以把它們想象成類似于一頁(yè)二維的堅(jiān)硬的紙片山叮,用膠水粘成一個(gè)空洞,就像三維結(jié)構(gòu)的折紙一樣添履。
zPosition屬性在大多數(shù)情況下其實(shí)并不常用屁倔。在第五章,我們將會(huì)涉及CATransform3D暮胧,你會(huì)知道如何在三維空間移動(dòng)和旋轉(zhuǎn)圖層锐借,除了做變換之外,zPosition最實(shí)用的功能就是改變圖層的顯示順序了往衷。
通常钞翔,圖層是根據(jù)它們子圖層的sublayers出現(xiàn)的順序來(lái)類繪制的,這就是所謂的畫家的算法--就像一個(gè)畫家在墻上作畫--后被繪制上的圖層將會(huì)遮蓋住之前的圖層席舍,但是通過增加圖層的zPosition布轿,就可以把圖層向相機(jī)方向前置,于是它就在所有其他圖層的前面了(或者至少是小于它的zPosition值的圖層的前面)来颤。
這里所謂的“相機(jī)”實(shí)際上是相對(duì)于用戶是視角汰扭,這里和iPhone背后的內(nèi)置相機(jī)沒任何關(guān)系。
圖3.8顯示了在Interface Builder內(nèi)的一對(duì)視圖福铅,正如你所見萝毛,首先出現(xiàn)在視圖層級(jí)綠色的視圖被繪制在紅色視圖的后面。
我們希望在真實(shí)的應(yīng)用中也能顯示出繪圖的順序滑黔,同樣地笆包,如果我們提高綠色視圖的zPosition(清單3.3)环揽,我們會(huì)發(fā)現(xiàn)順序就反了(圖3.9)。其實(shí)并不需要增加太多庵佣,視圖都非常地薄歉胶,所以給zPosition提高一個(gè)像素就可以讓綠色視圖前置,當(dāng)然0.1或者0.0001也能夠做到秧了,但是最好不要這樣跨扮,因?yàn)楦↑c(diǎn)類型四舍五入的計(jì)算可能會(huì)造成一些不便的麻煩。
清單3.3
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *greenView;
@property (nonatomic, weak) IBOutlet UIView *redView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
?
//move the green view zPosition nearer to the camera
self.greenView.layer.zPosition = 1.0f;
}
@end
Hit Testing
第一章“圖層樹”證實(shí)了最好使用圖層相關(guān)視圖验毡,而不是創(chuàng)建獨(dú)立的圖層關(guān)系衡创。其中一個(gè)原因就是要處理額外復(fù)雜的觸摸事件。
CALayer并不關(guān)心任何響應(yīng)鏈?zhǔn)录ǎ圆荒苤苯犹幚碛|摸事件或者手勢(shì)璃氢。但是它有一系列的方法幫你處理事件:-containsPoint:和-hitTest:。
-containsPoint:接受一個(gè)在本圖層坐標(biāo)系下的CGPoint狮辽,如果這個(gè)點(diǎn)在圖層frame范圍內(nèi)就返回YES一也。如清單3.4所示第一章的項(xiàng)目的另一個(gè)合適的版本,也就是使用-containsPoint:方法來(lái)判斷到底是白色還是藍(lán)色的圖層被觸摸了 (圖3.10)喉脖。這需要把觸摸坐標(biāo)轉(zhuǎn)換成每個(gè)圖層坐標(biāo)系下的坐標(biāo)椰苟,結(jié)果很不方便。
清單3.4 使用containsPoint判斷被點(diǎn)擊的圖層
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@property (nonatomic, weak) CALayer *blueLayer;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create sublayer
self.blueLayer = [CALayer layer];
self.blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.blueLayer.backgroundColor = [UIColor blueColor].CGColor;
//add it to our view
[self.layerView.layer addSublayer:self.blueLayer];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get touch position relative to main view
CGPoint point = [[touches anyObject] locationInView:self.view];
//convert point to the white layer's coordinates
point = [self.layerView.layer convertPoint:point fromLayer:self.view.layer];
//get layer using containsPoint:
if ([self.layerView.layer containsPoint:point]) {
//convert point to blueLayer’s coordinates
point = [self.blueLayer convertPoint:point fromLayer:self.layerView.layer];
if ([self.blueLayer containsPoint:point]) {
[[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer"
message:nil
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
} else {
[[[UIAlertView alloc] initWithTitle:@"Inside White Layer"
message:nil
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
}
}
}
@end
-hitTest:方法同樣接受一個(gè)CGPoint類型參數(shù)树叽,而不是BOOL類型舆蝴,它返回圖層本身,或者包含這個(gè)坐標(biāo)點(diǎn)的葉子節(jié)點(diǎn)圖層题诵。這意味著不再需要像使用-containsPoint:那樣洁仗,人工地在每個(gè)子圖層變換或者測(cè)試點(diǎn)擊的坐標(biāo)。如果這個(gè)點(diǎn)在最外面圖層的范圍之外性锭,則返回nil赠潦。具體使用-hitTest:方法被點(diǎn)擊圖層的代碼如清單3.5所示。
清單3.5 使用hitTest判斷被點(diǎn)擊的圖層
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get touch position
CGPoint point = [[touches anyObject] locationInView:self.view];
//get touched layer
CALayer *layer = [self.layerView.layer hitTest:point];
//get layer using hitTest
if (layer == self.blueLayer) {
[[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer"
message:nil
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
} else if (layer == self.layerView.layer) {
[[[UIAlertView alloc] initWithTitle:@"Inside White Layer"
message:nil
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
}
}
注意當(dāng)調(diào)用圖層的-hitTest:方法時(shí)草冈,測(cè)算的順序嚴(yán)格依賴于圖層樹當(dāng)中的圖層順序(和UIView處理事件類似)她奥。之前提到的zPosition屬性可以明顯改變屏幕上圖層的順序,但不能改變事件傳遞的順序怎棱。
這意味著如果改變了圖層的z軸順序方淤,你會(huì)發(fā)現(xiàn)將不能夠檢測(cè)到最前方的視圖點(diǎn)擊事件,這是因?yàn)楸涣硪粋€(gè)圖層遮蓋住了蹄殃,雖然它的zPosition值較小携茂,但是在圖層樹中的順序靠前。我們將在第五章詳細(xì)討論這個(gè)問題诅岩。
自動(dòng)布局
你可能用過UIViewAutoresizingMask類型的一些常量讳苦,應(yīng)用于當(dāng)父視圖改變尺寸的時(shí)候带膜,相應(yīng)UIView的frame也跟著更新的場(chǎng)景(通常用于橫豎屏切換)。
在iOS6中鸳谜,蘋果介紹了自動(dòng)排版機(jī)制膝藕,它和自動(dòng)調(diào)整不同,并且更加復(fù)雜咐扭。
在Mac OS平臺(tái)芭挽,CALayer有一個(gè)叫做layoutManager的屬性可以通過CALayoutManager協(xié)議和CAConstraintLayoutManager類來(lái)實(shí)現(xiàn)自動(dòng)排版的機(jī)制。但由于某些原因蝗肪,這在iOS上并不適用袜爪。
當(dāng)使用視圖的時(shí)候,可以充分利用UIView類接口暴露出來(lái)的UIViewAutoresizingMask和NSLayoutConstraintAPI薛闪,但如果想隨意控制CALayer的布局辛馆,就需要手工操作。最簡(jiǎn)單的方法就是使用CALayerDelegate如下函數(shù):
- (void)layoutSublayersOfLayer:(CALayer *)layer;
當(dāng)圖層的bounds發(fā)生改變豁延,或者圖層的-setNeedsLayout方法被調(diào)用的時(shí)候昙篙,這個(gè)函數(shù)將會(huì)被執(zhí)行。這使得你可以手動(dòng)地重新擺放或者重新調(diào)整子圖層的大小诱咏,但是不能像UIView的autoresizingMask和constraints屬性做到自適應(yīng)屏幕旋轉(zhuǎn)苔可。
這也是為什么最好使用視圖而不是單獨(dú)的圖層來(lái)構(gòu)建應(yīng)用程序的另一個(gè)重要原因之一。
總結(jié)
本章涉及了CALayer的集合結(jié)構(gòu)袋狞,包括它的frame焚辅,position和bounds,介紹了三維空間內(nèi)圖層的概念硕并,以及如何在獨(dú)立的圖層內(nèi)響應(yīng)事件法焰,最后簡(jiǎn)單說明了在iOS平臺(tái)秧荆,Core Animation對(duì)自動(dòng)調(diào)整和自動(dòng)布局支持的缺乏倔毙。
在第四章“視覺效果”當(dāng)中,我們接著介紹一些圖層外表的特性乙濒。
如果這篇文章對(duì)您有些許幫助 請(qǐng)給我點(diǎn)個(gè)心哦陕赃。