CoreAnimation
一、圖層的樹(shù)狀結(jié)構(gòu)
本節(jié)轉(zhuǎn)載自ios核心動(dòng)畫高級(jí)技巧
巨妖有圖層,洋蔥也有圖層,你有嗎?我們都有圖層 – 史萊克
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è)叫做圖層樹(shù)的體系之中。于是這個(gè)樹(shù)形成了UIKit以及在iOS應(yīng)用程序當(dāng)中你所能在屏幕上看見(jiàn)的一切的基礎(chǔ)。
在我們討論動(dòng)畫之前,我們將從圖層樹(shù)開(kāi)始,涉及一下Core Animation的靜態(tài)組合以及布局特性。
1.1 圖層與視圖
如果你曾經(jīng)在iOS或者M(jìn)ac OS平臺(tái)上寫過(guò)應(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)系樹(shù)管理的矩形塊,同樣也可以包含一些內(nèi)容(像圖片,文本或者背景色),管理子圖層的位置。它們有一些方法和屬性用來(lái)做動(dòng)畫和變換。和UIView最大的不同是CALayer不處理用戶的交互。
CALayer并不清楚具體的響應(yīng)鏈(iOS通過(guò)視圖層級(jí)關(guān)系用來(lái)傳送觸摸事件的機(jī)制),于是它并不能夠響應(yīng)事件,即使它提供了一些方法來(lái)判斷是否一個(gè)觸點(diǎn)在圖層的范圍之內(nèi)(具體見(jiàn)第三章,“圖層的幾何學(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)系樹(shù)當(dāng)中有相同的操作(見(jiàn)圖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觸屏的概念。把這種功能的邏輯分開(kāi)并應(yīng)用到獨(dú)立的Core Animation框架,蘋果就能夠在iOS和Mac OS之間共享代碼,使得對(duì)蘋果自己的OS開(kāi)發(fā)團(tuán)隊(duì)和第三方開(kāi)發(fā)者去開(kāi)發(fā)兩個(gè)平臺(tái)的應(yīng)用更加便捷。
實(shí)際上,這里并不是兩個(gè)層級(jí)關(guān)系,而是四個(gè),每一個(gè)都扮演不同的角色,除了視圖層級(jí)和圖層樹(shù)之外,還存在呈現(xiàn)樹(shù)和渲染樹(shù),將在第七章“隱式動(dòng)畫”和第十二章“性能調(diào)優(yōu)”分別討論。
1.2 圖層的能力
如果說(shuō)CALayer是UIView內(nèi)部實(shí)現(xiàn)細(xì)節(jié),那我們?yōu)槭裁匆娴亓私馑?#xff1f;蘋果當(dāng)然為我們提供了優(yōu)美簡(jiǎn)潔的UIView接口,那么我們是否就沒(méi)必要直接去處理Core Animation的細(xì)節(jié)了呢?
某種意義上說(shuō)的確是這樣,對(duì)一些簡(jiǎn)單的需求來(lái)說(shuō),我們確實(shí)沒(méi)必要處理CALayer,因?yàn)樘O果已經(jīng)通過(guò)UIView的高級(jí)API間接地使得動(dòng)畫變得很簡(jiǎn)單。
但是這種簡(jiǎn)單會(huì)不可避免地帶來(lái)一些靈活上的缺陷。如果你略微想在底層做一些改變,或者使用一些蘋果沒(méi)有在UIView上實(shí)現(xiàn)的接口功能,這時(shí)除了介入Core Animation底層之外別無(wú)選擇。
我們已經(jīng)證實(shí)了圖層不能像視圖那樣處理觸摸事件,那么他能做哪些視圖不能做的呢?這里有一些UIView沒(méi)有暴露出來(lái)的CALayer的功能:
- 陰影,圓角,帶顏色的邊框
- 3D變換
- 非矩形范圍
- 透明遮罩
- 多級(jí)非線性動(dòng)畫
我們將會(huì)在后續(xù)章節(jié)中探索這些功能,首先我們要關(guān)注一下在應(yīng)用程序當(dāng)中CALayer是怎樣被利用起來(lái)的。
1.3 使用圖層
首先我們來(lái)創(chuàng)建一個(gè)簡(jiǎn)單的項(xiàng)目,來(lái)操縱一些layer的屬性。打開(kāi)Xcode,使用Single View Application模板創(chuàng)建一個(gè)工程。
在屏幕中央創(chuàng)建一個(gè)小視圖(大約200 X 200的尺寸),當(dāng)然你可以手工編碼,或者使用Interface Builder(隨你方便)。確保你的視圖控制器要添加一個(gè)視圖的屬性以便可以直接訪問(wèn)它。我們把它稱作layerView。
運(yùn)行項(xiàng)目,應(yīng)該能在淺灰色屏幕背景中看見(jiàn)一個(gè)白色方塊(圖1.3),如果沒(méi)看見(jiàn),可能需要調(diào)整一下背景window或者view的顏色
這并沒(méi)有什么令人激動(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)目模板并沒(méi)有包含Core Animation相關(guān)頭文件。所以如果我們不給項(xiàng)目添加合適的庫(kù),是不能夠使用任何圖層相關(guān)的方法或者訪問(wèn)它的屬性。所以首先需要添加QuartzCore框架到Build Phases標(biāo)簽(圖1.4),然后在vc的.m文件中引入庫(kù)。
之后就可以在代碼中直接引用CALayer的屬性和方法。在清單1.1中,我們用創(chuàng)建了一個(gè)CALayer,設(shè)置了它的backgroundColor屬性,然后添加到layerView背后相關(guān)圖層的子圖層(這段代碼的前提是通過(guò)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 sublayerCALayer *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)簡(jiǎn)單地處理視圖,他們關(guān)聯(lián)的圖層并不需要額外地手動(dòng)添加子圖層。
在Mac OS平臺(tái),10.8版本之前,一個(gè)顯著的性能缺陷就是由于用了視圖層級(jí)而不是單獨(dú)在一個(gè)視圖內(nèi)使用CALayer樹(shù)狀層級(jí)。但是在iOS平臺(tái),使用輕量級(jí)的UIView類并沒(méi)有顯著的性能影響(當(dāng)然在Mac OS 10.8之后,NSView的性能同樣也得到很大程度的提高)。
使用圖層關(guān)聯(lián)的視圖而不是CALayer的好處在于,你能在使用所有CALayer底層特性的同時(shí),也可以使用UIView的高級(jí)API(比如自動(dòng)排版,布局和事件處理)。
然而,當(dāng)滿足以下條件的時(shí)候,你可能更需要使用CALayer而不是UIView:
- 開(kāi)發(fā)同時(shí)可以在Mac OS上運(yùn)行的跨平臺(tái)應(yīng)用
- 使用多種CALayer的子類(見(jiàn)第六章,“特殊的圖層“),并且不想創(chuàng)建額外的UIView去包封裝它們所有
- 做一些對(duì)性能特別挑剔的工作,比如對(duì)UIView一些可忽略不計(jì)的操作都會(huì)引起顯著的不同(盡管如此,你可能會(huì)直接想使用OpenGL繪圖)
但是這些例子都很少見(jiàn),總的來(lái)說(shuō),處理視圖會(huì)比單獨(dú)處理圖層更加方便。
1.4 總結(jié)
這一章闡述了圖層的樹(shù)狀結(jié)構(gòu),說(shuō)明了如何在iOS中由UIView的層級(jí)關(guān)系形成的一種平行的CALayer層級(jí)關(guān)系,在后面的實(shí)驗(yàn)中,我們創(chuàng)建了自己的CALayer,并把它添加到圖層樹(shù)中。
在第二章,“圖層關(guān)聯(lián)的圖片”,我們將要研究一下CALayer關(guān)聯(lián)的圖片,以及Core Animation提供的操作顯示的一些特性。
二、寄宿圖
本節(jié)轉(zhuǎn)載自ios核心動(dòng)畫高級(jí)技巧
圖片勝過(guò)千言萬(wàn)語(yǔ),界面抵得上千圖片 ——Ben Shneiderman
我們?cè)诘谝徽隆簣D層樹(shù)』中介紹了CALayer類并創(chuàng)建了一個(gè)簡(jiǎn)單的有藍(lán)色背景的圖層。背景顏色還好啦,但是如果它僅僅是展現(xiàn)了一個(gè)單調(diào)的顏色未免也太無(wú)聊了。事實(shí)上CALayer類能夠包含一張你喜歡的圖片,這一章節(jié)我們將來(lái)探索CALayer的寄宿圖(即圖層中包含的圖)。
2.1 contents屬性
CALayer 有一個(gè)屬性叫做contents,這個(gè)屬性的類型被定義為id,意味著它可以是任何類型的對(duì)象。在這種情況下,你可以給contents屬性賦任何值,你的app仍然能夠編譯通過(guò)。但是,在實(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開(kāi)發(fā)者可能會(huì)對(duì)這個(gè)感到困惑。
頭疼的不僅僅是我們剛才提到的這個(gè)問(wèn)題。事實(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),他們并不是類型兼容的,不過(guò)你可以通過(guò)bridged關(guān)鍵字轉(zhuǎn)換。如果要給圖層的寄宿圖賦值,你可以按照以下這個(gè)方法:
layer.contents = (__bridge id)image.CGImage;
如果你沒(méi)有使用ARC(自動(dòng)引用計(jì)數(shù)),你就不需要__bridge這部分。但是,你干嘛不用ARC?!
讓我們來(lái)繼續(xù)修改我們?cè)诘谝徽滦陆ǖ墓こ?#xff0c;以便能夠展示一張圖片而不僅僅是一個(gè)背景色。我們已經(jīng)用代碼的方式建立一個(gè)圖層,那我們就不需要額外的圖層了。那么我們就直接把layerView的宿主圖層的contents屬性設(shè)置成圖片。
清單2.1 更新后的代碼。 @implementation ViewController- (void)viewDidLoad {[super viewDidLoad]; //load an imageUIImage *image = [UIImage imageNamed:@"Snowman.png"];//add it directly to our view's layerself.layerView.layer.contents = (__bridge id)image.CGImage; } @end我們用這些簡(jiǎn)單的代碼做了一件很有趣的事情:我們利用CALayer在一個(gè)普通的UIView中顯示了一張圖片。這不是一個(gè)UIImageView,它不是我們通常用來(lái)展示圖片的方法。通過(guò)直接操作圖層,我們使用了一些新的函數(shù),使得UIView更加有趣了。
contentGravity
你可能已經(jīng)注意到了我們的雪人看起來(lái)有點(diǎn)。。。胖 ==! 我們加載的圖片并不剛好是一個(gè)方的,為了適應(yīng)這個(gè)視圖,它有一點(diǎn)點(diǎn)被拉伸了。在使用UIImageView的時(shí)候遇到過(guò)同樣的問(wèn)題,解決方法就是把contentMode屬性設(shè)置成更合適的值,像這樣:
view.contentMode = UIViewContentModeScaleAspectFit;這個(gè)方法基本和我們遇到的情況的解決方法已經(jīng)接近了(你可以試一下 :) ),不過(guò)UIView大多數(shù)視覺(jué)相關(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)圖層的邊界。圖2.2 可以看到結(jié)果:
self.layerView.layer.contentsGravity = kCAGravityResizeAspect;contentsScale
contentsScale屬性定義了寄宿圖的像素尺寸和視圖大小的比例,默認(rèn)情況下它是一個(gè)值為1.0的浮點(diǎn)數(shù)。
contentsScale的目的并不是那么明顯。它并不是總會(huì)對(duì)屏幕上的寄宿圖有影響。如果你嘗試對(duì)我們的例子設(shè)置不同的值,你就會(huì)發(fā)現(xiàn)根本沒(méi)任何影響。因?yàn)閏ontents由于設(shè)置了contentsGravity屬性,所以它已經(jīng)被拉伸以適應(yīng)圖層的邊界。
如果你只是單純地想放大圖層的contents圖片,你可以通過(guò)使用圖層的transform和affineTransform屬性來(lái)達(dá)到這個(gè)目的(見(jiàn)第五章『Transforms』,里面對(duì)此有解釋),這(指放大)也不是contengsScale的目的所在.
contentsScale屬性其實(shí)屬于支持高分辨率(又稱Hi-DPI或Retina)屏幕機(jī)制的一部分。它用來(lái)判斷在繪制圖層的時(shí)候應(yīng)該為寄宿圖創(chuàng)建的空間大小,和需要顯示的圖片的拉伸度(假設(shè)并沒(méi)有設(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ì)考慮到分辨率問(wèn)題。但是如果我們把contentsGravity設(shè)置為kCAGravityCenter(這個(gè)值并不會(huì)拉伸圖片),那將會(huì)有很明顯的變化(如圖2.3)
如你所見(jiàn),我們的雪人不僅有點(diǎn)大還有點(diǎn)像素的顆粒感。那是因?yàn)楹蚒IImage不同,CGImage沒(méi)有拉伸的概念。當(dāng)我們使用UIImage類去讀取我們的雪人圖片的時(shí)候,他讀取了高質(zhì)量的Retina版本的圖片。但是當(dāng)我們用CGImage來(lái)設(shè)置我們的圖層的內(nèi)容時(shí),拉伸這個(gè)因素在轉(zhuǎn)換的時(shí)候就丟失了。不過(guò)我們可以通過(guò)手動(dòng)設(shè)置contentsScale來(lái)修復(fù)這個(gè)問(wèn)題(如2.2清單),圖2.4是結(jié)果
@implementation ViewController - (void)viewDidLoad {[super viewDidLoad]; //load an imageUIImage *image = [UIImage imageNamed:@"Snowman.png"]; //add it directly to our view's layerself.layerView.layer.contents = (__bridge id)image.CGImage; //center the imageself.layerView.layer.contentsGravity = kCAGravityCenter;//set the contentsScale to match imageself.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)在我們的雪人總算是顯示了正確的大小,不過(guò)你也許已經(jīng)發(fā)現(xiàn)了另外一些事情:他超出了視圖的邊界。默認(rèn)情況下,UIView仍然會(huì)繪制超過(guò)邊界的內(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中最常見(jiàn)的坐標(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è)備上能有一致的視覺(jué)效果。
- 像素 —— 物理像素坐標(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)都是可見(jià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ú)立地變更位置。拋開(kāi)游戲編程不談,這個(gè)技術(shù)常用來(lái)指代載入拼合的圖片,跟移動(dòng)圖片一點(diǎn)關(guān)系也沒(méi)有。
典型地,圖片拼合后可以打包整合到一張大圖上一次性載入。相比多次載入不同的圖片,這樣做能夠帶來(lái)很多方面的好處:內(nèi)存使用,載入時(shí)間,渲染性能等等。
2D游戲引擎入Cocos2D使用了拼合技術(shù),它使用OpenGL來(lái)顯示圖片。不過(guò)我們可以使用拼合在一個(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 fitlayer.contentsGravity = kCAGravityResizeAspect;//set contentsRectlayer.contentsRect = rect; }- (void)viewDidLoad {[super viewDidLoad]; //load sprite sheetUIImage *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,這樣開(kāi)發(fā)者就不用手動(dòng)寫代碼來(lái)擺放位置了。
這些文件通常在OpenGL游戲中使用,不過(guò)呢,你要是有興趣在一些常見(jiàn)的app中使用拼合技術(shù),那么一個(gè)叫做LayerSprites的開(kāi)源庫(kù),它能夠讀取Cocos2D格式中的拼合圖并在普通的Core Animation層中顯示出來(lái)。
contentsCenter
本章我們介紹的最后一個(gè)和內(nèi)容有關(guān)的屬性是contentsCenter,看名字你可能會(huì)以為它可能跟圖片的位置有關(guān),不過(guò)這名字著實(shí)誤導(dǎo)了你。contentsCenter其實(shí)是一個(gè)CGRect,它定義了一個(gè)固定的邊框和一個(gè)在圖層上可拉伸的區(qū)域。 改變contentsCenter的值并不會(huì)影響到寄宿圖的顯示,除非這個(gè)圖層的大小改變了,你才看得到效果。
默認(rèn)情況下,contentsCenter是{0, 0, 1, 1},這意味著如果大小(由conttensGravity決定)改變了,那么寄宿圖將會(huì)均勻地拉伸開(kāi)。但是如果我們?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 演示了如何編寫這些可拉伸視圖。不過(guò),contentsCenter的另一個(gè)很酷的特性就是,它可以在Interface Builder里面配置,根本不用寫代碼。如圖2.11
清單2.4 用contentsCenter設(shè)置可拉伸視圖
2.2 Custom Drawing
給contents賦CGImage的值不是唯一的設(shè)置寄宿圖的方法。我們也可以直接用Core Graphics直接繪制寄宿圖。能夠通過(guò)繼承UIView并實(shí)現(xiàn)-drawRect:方法來(lái)自定義繪制。
-drawRect:方法沒(méi)有默認(rèn)的實(shí)現(xiàn),因?yàn)閷?duì)UIView來(lái)說(shuō),寄宿圖并不是必須的,它不在意那到底是單調(diào)的顏色還是有一個(gè)圖片的實(shí)例。如果UIView檢測(cè)到-drawRect:方法被調(diào)用了,它就會(huì)為視圖分配一個(gè)寄宿圖,這個(gè)寄宿圖的像素尺寸等于視圖大小乘以 contentsScale的值。
如果你不需要寄宿圖,那就不要?jiǎng)?chuàng)建這個(gè)方法了,這會(huì)造成CPU資源和內(nèi)存的浪費(fèi),這也是為什么蘋果建議:如果沒(mé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)殚_(kāi)發(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í)就是說(shuō)沒(méi)有CALayerDelegate @protocol可以讓你在類里面引用啦。你只需要調(diào)用你想調(diào)用的方法,CALayer會(huì)幫你做剩下的。(delegate屬性被聲明為id類型,所有的代理方法都是可選的)。
當(dāng)需要被重繪時(shí),CALayer會(huì)請(qǐng)求它的代理給他一個(gè)寄宿圖來(lái)顯示。它通過(guò)調(diào)用下面這個(gè)方法做到的:
(void)displayLayer:(CALayerCALayer *)layer;趁著這個(gè)機(jī)會(huì),如果代理想直接設(shè)置contents屬性的話,它就可以這么做,不然沒(méi)有別的方法可以調(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并做一些繪圖工作吧(見(jiàn)清單2.5).圖2.12是他的結(jié)果
清單2.5 實(shí)現(xiàn)CALayerDelegate
@implementation ViewController - (void)viewDidLoad {[super viewDidLoad];//create sublayerCALayer *blueLayer = [CALayer layer];blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);blueLayer.backgroundColor = [UIColor blueColor].CGColor;//set controller as layer delegateblueLayer.delegate = self;//ensure that layer backing image uses correct scaleblueLayer.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 circleCGContextSetLineWidth(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)交給了開(kāi)發(fā)者。
盡管我們沒(méi)有用masksToBounds屬性,繪制的那個(gè)圓仍然沿邊界被裁剪了。這是因?yàn)楫?dāng)你使用CALayerDelegate繪制寄宿圖的時(shí)候,并沒(méi)有對(duì)超出邊界外的內(nèi)容提供繪制支持。
現(xiàn)在你理解了CALayerDelegate,并知道怎么使用它。但是除非你創(chuàng)建了一個(gè)單獨(dú)的圖層,你幾乎沒(méi)有機(jī)會(huì)用到CALayerDelegate協(xié)議。因?yàn)楫?dāng)UIView創(chuàng)建了它的宿主圖層時(shí),它就會(huì)自動(dòng)地把圖層的delegate設(shè)置為它自己,并提供了一個(gè)-displayLayer:的實(shí)現(xiàn),那所有的問(wèn)題就都沒(méi)了。
當(dāng)使用寄宿了視圖的圖層的時(shí)候,你也不必實(shí)現(xiàn)-displayLayer:和-drawLayer:inContext:方法來(lái)繪制你的寄宿圖。通常做法是實(shí)現(xiàn)UIView的-drawRect:方法,UIView就會(huì)幫你做完剩下的工作,包括在需要重繪的時(shí)候調(diào)用-display方法。
2.3 總結(jié)
本章介紹了寄宿圖和一些相關(guān)的屬性。你學(xué)到了如何顯示和放置圖片, 使用拼合技術(shù)來(lái)顯示, 以及用CALayerDelegate和Core Graphics來(lái)繪制圖層內(nèi)容。
在第三章,”圖層幾何學(xué)”中,我們將會(huì)探討一下圖層的幾何,觀察他們是如何放置和改變相互的尺寸的。
三、圖層幾何學(xué)
本節(jié)轉(zhuǎn)載自ios核心動(dòng)畫高級(jí)技巧
不熟悉幾何學(xué)的人就不要來(lái)這里了 –柏拉圖學(xué)院入口的簽名
在第二章里面,我們介紹了圖層背后的圖片,和一些控制圖層坐標(biāo)和旋轉(zhuǎn)的屬性。在這一章中,我們將要看一看圖層內(nèi)部是如何根據(jù)父圖層和兄弟圖層來(lái)控制位置和尺寸的。另外我們也會(huì)涉及如何管理圖層的幾何結(jié)構(gòu),以及它是如何被自動(dòng)調(diào)整和自動(dòng)布局影響的。
3.1 布局
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)說(shuō),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ū)域,也就是說(shuō)frame的寬高可能和bounds的寬高不再一致了(圖3.2)
3.2錨點(diǎn)
之前提到過(guò),視圖的center屬性和圖層的position屬性都指定了anchorPoint相對(duì)于父圖層的位置。圖層的anchorPoint通過(guò)position來(lái)控制它的frame的位置,你可以認(rèn)為anchorPoint是用來(lái)移動(dòng)圖層的把柄。
默認(rèn)來(lái)說(shuō),anchorPoint位于圖層的中點(diǎn),所以圖層的將會(huì)以這個(gè)點(diǎn)為中心放置。anchorPoint屬性并沒(méi)有被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可以通過(guò)指定x和y值小于0或者大于1,使它放置在圖層范圍之外。
注意在圖3.3中,當(dāng)改變了anchorPoint,position屬性保持固定的值并沒(méi)有發(fā)生改變,但是frame卻移動(dòng)了。
那在什么場(chǎng)合需要改變anchorPoint呢?既然我們可以隨意改變圖層位置,那改變anchorPoint不會(huì)造成困惑么?為了舉例說(shuō)明,我們來(lái)舉一個(gè)實(shí)用的例子,創(chuàng)建一個(gè)模擬鬧鐘的項(xiàng)目。
鐘面和鐘表由四張圖片組成(圖3.4),為了簡(jiǎn)單說(shuō)明,我們還是用傳統(tǒng)的方式來(lái)裝載和加載圖片,使用四個(gè)UIImageView實(shí)例(當(dāng)然你也可以用正常的視圖,設(shè)置他們圖層的contents圖片)。
鬧鐘的組件通過(guò)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ì)說(shuō)明),具體代碼見(jiàn)清單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 timerself.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 secondsNSCalendar *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 angleCGFloat minsAngle = (components.minute / 60.0) * M_PI * 2.0;//calculate second hand angleCGFloat secsAngle = (components.second / 60.0) * M_PI * 2.0;//rotate handsself.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 pointsself.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 }3.3 坐標(biāo)系
和視圖一樣,圖層在圖層樹(shù)當(dāng)中也是相對(duì)于父圖層按層級(jí)關(guān)系放置,一個(gè)圖層的position依賴于它父圖層的bounds,如果父圖層發(fā)生了移動(dòng),它的所有子圖層也會(huì)跟著移動(dòng)。
這樣對(duì)于放置圖層會(huì)更加方便,因?yàn)槟憧梢酝ㄟ^(guò)移動(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ī)說(shuō)來(lái),在iOS上,一個(gè)圖層的position位于父圖層的左上角,但是在Mac OS上,通常是位于左下角。Core Animation可以通過(guò)geometryFlipped屬性來(lái)適配這兩種情況,它決定了一個(gè)圖層的坐標(biāo)是否相對(duì)于父圖層垂直翻轉(zhuǎn),是一個(gè)BOOL類型。在iOS上通過(guò)設(shè)置它為YES意味著它的子圖層將會(huì)被垂直翻轉(zhuǎn),也就是將會(huì)沿著底部排版而不是通常的頂部(它的所有子圖層也同理,除非把它們的geometryFlipped屬性也設(shè)為YES)。
Z坐標(biāo)軸
和UIView嚴(yán)格的二維坐標(biāo)系不同,CALayer存在于一個(gè)三維空間當(dāng)中。除了我們已經(jīng)討論過(guò)的position和anchorPoint屬性之外,CALayer還有另外兩個(gè)屬性,zPosition和anchorPointZ,二者都是在Z軸上描述圖層位置的浮點(diǎn)類型。
注意這里并沒(méi)有更深的屬性來(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ì)遮蓋住之前的圖層,但是通過(guò)增加圖層的zPosition,就可以把圖層向相機(jī)方向前置,于是它就在所有其他圖層的前面了(或者至少是小于它的zPosition值的圖層的前面)。
這里所謂的“相機(jī)”實(shí)際上是相對(duì)于用戶是視角,這里和iPhone背后的內(nèi)置相機(jī)沒(méi)任何關(guān)系。
圖3.8顯示了在Interface Builder內(nèi)的一對(duì)視圖,正如你所見(jiàn),首先出現(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 cameraself.greenView.layer.zPosition = 1.0f; } @end3.4 Hit Testing
第一章“圖層樹(shù)”證實(shí)了最好使用圖層相關(guān)視圖,而不是創(chuàng)建獨(dú)立的圖層關(guān)系。其中一個(gè)原因就是要處理額外復(fù)雜的觸摸事件。
CALayer并不關(guān)心任何響應(yīng)鏈?zhǔn)录?#xff0c;所以不能直接處理觸摸事件或者手勢(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 sublayerself.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 viewCGPoint point = [[touches anyObject] locationInView:self.view];//convert point to the white layer's coordinatespoint = [self.layerView.layer convertPoint:point fromLayer:self.view.layer];//get layer using containsPoint:if ([self.layerView.layer containsPoint:point]) {//convert point to blueLayer’s coordinatespoint = [self.blueLayer convertPoint:point fromLayer:self.layerView.layer];if ([self.blueLayer containsPoint:point]) {[[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer"message:nildelegate:nilcancelButtonTitle:@"OK"otherButtonTitles:nil] show];} else {[[[UIAlertView alloc] initWithTitle:@"Inside White Layer"message:nildelegate:nilcancelButtonTitle:@"OK"otherButtonTitles:nil] show];}} } @end注意當(dāng)調(diào)用圖層的-hitTest:方法時(shí),測(cè)算的順序嚴(yán)格依賴于圖層樹(shù)當(dāng)中的圖層順序(和UIView處理事件類似)。之前提到的zPosition屬性可以明顯改變屏幕上圖層的順序,但不能改變事件傳遞的順序。
這意味著如果改變了圖層的z軸順序,你會(huì)發(fā)現(xiàn)將不能夠檢測(cè)到最前方的視圖點(diǎn)擊事件,這是因?yàn)楸涣硪粋€(gè)圖層遮蓋住了,雖然它的zPosition值較小,但是在圖層樹(shù)中的順序靠前。我們將在第五章詳細(xì)討論這個(gè)問(wèn)題。
3.5 自動(dòng)布局
你可能用過(guò)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的屬性可以通過(guò)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è)重要原因之一。
3.6 總結(jié)
本章涉及了CALayer的集合結(jié)構(gòu),包括它的frame,position和bounds,介紹了三維空間內(nèi)圖層的概念,以及如何在獨(dú)立的圖層內(nèi)響應(yīng)事件,最后簡(jiǎn)單說(shuō)明了在iOS平臺(tái),Core Animation對(duì)自動(dòng)調(diào)整和自動(dòng)布局支持的缺乏。
在第四章“視覺(jué)效果”當(dāng)中,我們接著介紹一些圖層外表的特性。
四、視覺(jué)效果
本節(jié)轉(zhuǎn)載自ios核心動(dòng)畫高級(jí)技巧
嗯,圓和橢圓還不錯(cuò),但如果是帶圓角的矩形呢?
我們現(xiàn)在能做到那樣了么?
史蒂芬·喬布斯
我們?cè)诘谌隆簣D層幾何學(xué)』中討論了圖層的frame,第二章『寄宿圖』則討論了圖層的寄宿圖。但是圖層不僅僅可以是圖片或是顏色的容器;還有一系列內(nèi)建的特性使得創(chuàng)造美麗優(yōu)雅的令人深刻的界面元素成為可能。在這一章,我們將會(huì)探索一些能夠通過(guò)使用CALayer屬性實(shí)現(xiàn)的視覺(jué)效果。
4.1 圓角
圓角矩形是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è)曲率值只影響背景顏色而不影響背景圖片或是子圖層。不過(guò),如果把masksToBounds設(shè)置成YES的話,圖層里面的所有東西都會(huì)被截取。
我們可以通過(guò)一個(gè)簡(jiǎn)單的項(xiàng)目來(lái)演示這個(gè)效果。在Interface Builder中,我們放置一些視圖,他們有一些子視圖。而且這些子視圖有一些超出了邊界(如圖4.1)。你可能無(wú)法看到他們超出了邊界,因?yàn)樵诰庉嫿缑娴臅r(shí)候,超出的部分總是被Interface Builder裁切掉了。不過(guò),你相信我就好了 :)
然后在代碼中,我們?cè)O(shè)置角的半徑為20個(gè)點(diǎn),并裁剪掉第一個(gè)視圖的超出部分(見(jiàn)清單4.1)。技術(shù)上來(lái)說(shuō),這些屬性都可以在Interface Builder的探測(cè)板中分別通過(guò)『用戶定義運(yùn)行時(shí)屬性』和勾選『裁剪子視圖』(Clip Subviews)選擇框來(lái)直接設(shè)置屬性的值。不過(guò),在這個(gè)示例中,代碼能夠表示得更清楚。圖4.2是運(yùn)行代碼的結(jié)果
清單4.1 設(shè)置cornerRadius和masksToBounds
@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 layersself.layerView1.layer.cornerRadius = 20.0f;self.layerView2.layer.cornerRadius = 20.0f;//enable clipping on the second layerself.layerView2.layer.masksToBounds = YES; } @end右圖中,紅色的子視圖沿角半徑被裁剪了
如你所見(jiàn),右邊的子視圖沿邊界被裁剪了。
單獨(dú)控制每個(gè)層的圓角曲率也不是不可能的。如果想創(chuàng)建有些圓角有些直角的圖層或視圖時(shí),你可能需要一些不同的方法。比如使用一個(gè)圖層蒙板(本章稍后會(huì)講到)或者是CAShapeLayer(見(jiàn)第六章『專用圖層』)。
4.2 圖層邊框
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ì)象。不過(guò)呢,你肯定也清楚圖層引用了borderColor,雖然屬性聲明并不能證明這一點(diǎn)。CGColorRef在引用/釋放時(shí)候的行為表現(xiàn)得與NSObject極其相似。但是Objective-C語(yǔ)法并不支持這一做法,所以CGColorRef屬性即便是強(qiáng)引用也只能通過(guò)assign關(guān)鍵字來(lái)聲明。
邊框是繪制在圖層邊界里面的,而且在所有子內(nèi)容之前,也在子圖層之前。如果我們?cè)谥暗氖纠?#xff08;清單4.2)加入圖層的邊框,你就能看到到底是怎么一回事了(如圖4.3).
清單4.2 加上邊框
@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];//set the corner radius on our layersself.layerView1.layer.cornerRadius = 20.0f;self.layerView2.layer.cornerRadius = 20.0f;//add a border to our layersself.layerView1.layer.borderWidth = 5.0f;self.layerView2.layer.borderWidth = 5.0f;//enable clipping on the second layerself.layerView2.layer.masksToBounds = YES; } @end仔細(xì)觀察會(huì)發(fā)現(xiàn)邊框并不會(huì)把寄宿圖或子圖層的形狀計(jì)算進(jìn)來(lái),如果圖層的子圖層超過(guò)了邊界,或者是寄宿圖在透明區(qū)域有一個(gè)透明蒙板,邊框仍然會(huì)沿著圖層的邊界繪制出來(lái)(如圖4.4).
4.3 陰影
iOS的另一個(gè)常見(jiàn)特性呢,就是陰影。陰影往往可以達(dá)到圖層深度暗示的效果。也能夠用來(lái)強(qiáng)調(diào)正在顯示的圖層和優(yōu)先級(jí)(比如說(shuō)一個(gè)在其他視圖之前的彈出框),不過(guò)有時(shí)候他們只是單純的裝飾目的。
給shadowOpacity屬性一個(gè)大于默認(rèn)值(也就是0)的值,陰影就可以顯示在任意圖層之下。shadowOpacity是一個(gè)必須在0.0(不可見(jiàn))和1.0(完全不透明)之間的浮點(diǎn)數(shù)。如果設(shè)置為1.0,將會(huì)顯示一個(gè)有輕微模糊的黑色陰影稍微在圖層之上。若要改動(dòng)陰影的表現(xiàn),你可以使用CALayer的另外三個(gè)屬性:shadowColor,shadowOffset和shadowRadius。
顯而易見(jiàn),shadowColor屬性控制著陰影的顏色,和borderColor和backgroundColor一樣,它的類型也是CGColorRef。陰影默認(rèn)是黑色,大多數(shù)時(shí)候你需要的陰影也是黑色的(其他顏色的陰影看起來(lái)是不是有一點(diǎn)點(diǎn)奇怪。。)。
shadowOffset屬性控制著陰影的方向和距離。它是一個(gè)CGSize的值,寬度控制這陰影橫向的位移,高度控制著縱向的位移。shadowOffset的默認(rèn)值是 {0, -3},意即陰影相對(duì)于Y軸有3個(gè)點(diǎn)的向上位移。
為什么要默認(rèn)向上的陰影呢?盡管Core Animation是從圖層套裝演變而來(lái)(可以認(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).
蘋果更傾向于用戶界面的陰影應(yīng)該是垂直向下的,所以在iOS把陰影寬度設(shè)為0,然后高度設(shè)為一個(gè)正值不失為一個(gè)做法。
shadowRadius屬性控制著陰影的模糊度,當(dāng)它的值是0的時(shí)候,陰影就和視圖一樣有一個(gè)非常確定的邊界線。當(dāng)值越來(lái)越大的時(shí)候,邊界線看上去就會(huì)越來(lái)越模糊和自然。蘋果自家的應(yīng)用設(shè)計(jì)更偏向于自然的陰影,所以一個(gè)非零值再合適不過(guò)了。
通常來(lái)講,如果你想讓視圖或控件非常醒目獨(dú)立于背景之外(比如彈出框遮罩層),你就應(yīng)該給shadowRadius設(shè)置一個(gè)稍大的值。陰影越模糊,圖層的深度看上去就會(huì)更明顯(如圖4.6).
陰影裁剪
和圖層邊框不同,圖層的陰影繼承自內(nèi)容的外形,而不是根據(jù)邊界和角半徑來(lái)確定。為了計(jì)算出陰影的形狀,Core Animation會(huì)將寄宿圖(包括子視圖,如果有的話)考慮在內(nèi),然后通過(guò)這些來(lái)完美搭配圖層形狀從而創(chuàng)建一個(gè)陰影(見(jiàn)圖4.7)。
當(dāng)陰影和裁剪扯上關(guān)系的時(shí)候就有一個(gè)頭疼的限制:陰影通常就是在Layer的邊界之外,如果你開(kāi)啟了masksToBounds屬性,所有從圖層中突出來(lái)的內(nèi)容都會(huì)被才剪掉。如果我們?cè)谖覀冎暗倪吙蚴纠?xiàng)目中增加圖層的陰影屬性時(shí),你就會(huì)發(fā)現(xiàn)問(wèn)題所在(見(jiàn)圖4.8).
從技術(shù)角度來(lái)說(shuō),這個(gè)結(jié)果是可以是可以理解的,但確實(shí)又不是我們想要的效果。如果你想沿著內(nèi)容裁切,你需要用到兩個(gè)圖層:一個(gè)只畫陰影的空的外圖層,和一個(gè)用masksToBounds裁剪內(nèi)容的內(nèi)圖層。
如果我們把之前項(xiàng)目的右邊用單獨(dú)的視圖把裁剪的視圖包起來(lái),我們就可以解決這個(gè)問(wèn)題(如圖4.9).
我們只把陰影用在最外層的視圖上,內(nèi)層視圖進(jìn)行裁剪。清單4.3是代碼實(shí)現(xiàn),圖4.10是運(yùn)行結(jié)果。
清單4.3 用一個(gè)額外的視圖來(lái)解決陰影裁切的問(wèn)題
@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 layersself.layerView1.layer.cornerRadius = 20.0f;self.layerView2.layer.cornerRadius = 20.0f;//add a border to our layersself.layerView1.layer.borderWidth = 5.0f;self.layerView2.layer.borderWidth = 5.0f;//add a shadow to layerView1self.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 layerself.layerView2.layer.masksToBounds = YES; } @endshadowPath屬性
我們已經(jīng)知道圖層陰影并不總是方的,而是從圖層內(nèi)容的形狀繼承而來(lái)。這看上去不錯(cuò),但是實(shí)時(shí)計(jì)算陰影也是一個(gè)非常消耗資源的,尤其是圖層有多個(gè)子圖層,每個(gè)圖層還有一個(gè)有透明效果的寄宿圖的時(shí)候。
如果你事先知道你的陰影形狀會(huì)是什么樣子的,你可以通過(guò)指定一個(gè)shadowPath來(lái)提高性能。shadowPath是一個(gè)CGPathRef類型(一個(gè)指向CGPath的指針)。CGPath是一個(gè)Core Graphics對(duì)象,用來(lái)指定任意的一個(gè)矢量圖形。我們可以通過(guò)這個(gè)屬性單獨(dú)于圖層形狀之外指定陰影的形狀。
圖4.11 展示了同一寄宿圖的不同陰影設(shè)定。如你所見(jiàn),我們使用的圖形很簡(jiǎn)單,但是它的陰影可以是你想要的任何形狀。清單4.4是代碼實(shí)現(xiàn)。
清單4.4 創(chuàng)建簡(jiǎn)單的陰影形狀
@interface ViewController ()@property (nonatomic, weak) IBOutlet UIView *layerView1; @property (nonatomic, weak) IBOutlet UIView *layerView2; @end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];//enable layer shadowsself.layerView1.layer.shadowOpacity = 0.5f;self.layerView2.layer.shadowOpacity = 0.5f;//create a square shadowCGMutablePathRef squarePath = CGPathCreateMutable();CGPathAddRect(squarePath, NULL, self.layerView1.bounds);self.layerView1.layer.shadowPath = squarePath; CGPathRelease(squarePath);//create a circular shadowCGMutablePathRef 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包裝類。
圖4.6 大一些的陰影位移和角半徑會(huì)增加圖層的深度即視感
4.4 圖層蒙板
通過(guò)masksToBounds屬性,我們可以沿邊界裁剪圖形;通過(guò)cornerRadius屬性,我們還可以設(shè)定一個(gè)圓角。但是有時(shí)候你希望展現(xiàn)的內(nèi)容不是在一個(gè)矩形或圓角矩形。比如,你想展示一個(gè)有星形框架的圖片,又或者想讓一些古卷文字慢慢漸變成背景色,而不是一個(gè)突兀的邊界。
使用一個(gè)32位有alpha通道的png圖片通常是創(chuàng)建一個(gè)無(wú)矩形視圖最方便的方法,你可以給它指定一個(gè)透明蒙板來(lái)實(shí)現(xiàn)。但是這個(gè)方法不能讓你以編碼的方式動(dòng)態(tài)地生成蒙板,也不能讓子圖層或子視圖裁剪成同樣的形狀。
CALayer有一個(gè)屬性叫做mask可以解決這個(gè)問(wèn)題。這個(gè)屬性本身就是個(gè)CALayer類型,有和其他圖層一樣的繪制和布局屬性。它類似于一個(gè)子圖層,相對(duì)于父圖層(即擁有該屬性的圖層)布局,但是它卻不是一個(gè)普通的子圖層。不同于那些繪制在父圖層中的子圖層,mask圖層定義了父圖層的部分可見(jiàn)區(qū)域。
mask圖層的Color屬性是無(wú)關(guān)緊要的,真正重要的是圖層的輪廓。mask屬性就像是一個(gè)餅干切割機(jī),mask圖層實(shí)心的部分會(huì)被保留下來(lái),其他的則會(huì)被拋棄。(如圖4.12)
如果mask圖層比父圖層要小,只有在mask圖層里面的內(nèi)容才是它關(guān)心的,除此以外的一切都會(huì)被隱藏起來(lái)。
我們將代碼演示一下這個(gè)過(guò)程,創(chuàng)建一個(gè)簡(jiǎn)單的項(xiàng)目,通過(guò)圖層的mask屬性來(lái)作用于圖片之上。為了簡(jiǎn)便一些,我們用Interface Builder來(lái)創(chuàng)建一個(gè)包含UIImageView的圖片圖層。這樣我們就只要代碼實(shí)現(xiàn)蒙板圖層了。清單4.5是最終的代碼,圖4.13是運(yùn)行后的結(jié)果。
清單4.5 應(yīng)用蒙板圖層
@interface ViewController ()@property (nonatomic, weak) IBOutlet UIImageView *imageView; @end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];//create mask layerCALayer *maskLayer = [CALayer layer];maskLayer.frame = self.layerView.bounds;UIImage *maskImage = [UIImage imageNamed:@"Cone.png"];maskLayer.contents = (__bridge id)maskImage.CGImage;//apply mask to image layerself.imageView.layer.mask = maskLayer; } @endCALayer蒙板圖層真正厲害的地方在于蒙板圖不局限于靜態(tài)圖。任何有圖層構(gòu)成的都可以作為mask屬性,這意味著你的蒙板可以通過(guò)代碼甚至是動(dòng)畫實(shí)時(shí)生成。
4.5 拉伸過(guò)濾
最后我們?cè)賮?lái)談?wù)刴inificationFilter和magnificationFilter屬性。總得來(lái)講,當(dāng)我們視圖顯示一個(gè)圖片的時(shí)候,都應(yīng)該正確地顯示這個(gè)圖片(意即:以正確的比例和正確的1:1像素顯示在屏幕上)。原因如下:
- 能夠顯示最好的畫質(zhì),像素既沒(méi)有被壓縮也沒(méi)有被拉伸。
- 能更好的使用內(nèi)存,因?yàn)檫@就是所有你要存儲(chǔ)的東西。
- 最好的性能表現(xiàn),CPU不需要為此額外的計(jì)算。
不過(guò)有時(shí)候,顯示一個(gè)非真實(shí)大小的圖片確實(shí)是我們需要的效果。比如說(shuō)一個(gè)頭像或是圖片的縮略圖,再比如說(shuō)一個(gè)可以被拖拽和伸縮的大圖。這些情況下,為同一圖片的不同大小存儲(chǔ)不同的圖片顯得又不切實(shí)際。
當(dāng)圖片需要顯示不同的大小的時(shí)候,有一種叫做拉伸過(guò)濾的算法就起到作用了。它作用于原圖的像素上并根據(jù)需要生成新的像素顯示在屏幕上。
事實(shí)上,重繪圖片大小也沒(méi)有一個(gè)統(tǒng)一的通用算法。這取決于需要拉伸的內(nèi)容,放大或是縮小的需求等這些因素。CALayer為此提供了三種拉伸過(guò)濾方法,他們是:
- kCAFilterLinear
- kCAFilterNearest
- kCAFilterTrilinear
minification(縮小圖片)和magnification(放大圖片)默認(rèn)的過(guò)濾器都是kCAFilterLinear,這個(gè)過(guò)濾器采用雙線性濾波算法,它在大多數(shù)情況下都表現(xiàn)良好。雙線性濾波算法通過(guò)對(duì)多個(gè)像素取樣最終生成新的值,得到一個(gè)平滑的表現(xiàn)不錯(cuò)的拉伸。但是當(dāng)放大倍數(shù)比較大的時(shí)候圖片就模糊不清了。
kCAFilterTrilinear和kCAFilterLinear非常相似,大部分情況下二者都看不出來(lái)有什么差別。但是,較雙線性濾波算法而言,三線性濾波算法存儲(chǔ)了多個(gè)大小情況下的圖片(也叫多重貼圖),并三維取樣,同時(shí)結(jié)合大圖和小圖的存儲(chǔ)進(jìn)而得到最后的結(jié)果。
這個(gè)方法的好處在于算法能夠從一系列已經(jīng)接近于最終大小的圖片中得到想要的結(jié)果,也就是說(shuō)不要對(duì)很多像素同步取樣。這不僅提高了性能,也避免了小概率因舍入錯(cuò)誤引起的取樣失靈的問(wèn)題
kCAFilterNearest是一種比較武斷的方法。從名字不難看出,這個(gè)算法(也叫最近過(guò)濾)就是取樣最近的單像素點(diǎn)而不管其他的顏色。這樣做非???#xff0c;也不會(huì)使圖片模糊。但是,最明顯的效果就是,會(huì)使得壓縮圖片更糟,圖片放大之后也顯得塊狀或是馬賽克嚴(yán)重。
總的來(lái)說(shuō),對(duì)于比較小的圖或者是差異特別明顯,極少斜線的大圖,最近過(guò)濾算法會(huì)保留這種差異明顯的特質(zhì)以呈現(xiàn)更好的結(jié)果。但是對(duì)于大多數(shù)的圖尤其是有很多斜線或是曲線輪廓的圖片來(lái)說(shuō),最近過(guò)濾算法會(huì)導(dǎo)致更差的結(jié)果。換句話說(shuō),線性過(guò)濾保留了形狀,最近過(guò)濾則保留了像素的差異。
讓我們來(lái)實(shí)驗(yàn)一下。我們對(duì)第三章的時(shí)鐘項(xiàng)目改動(dòng)一下,用LCD風(fēng)格的數(shù)字方式顯示。我們用簡(jiǎn)單的像素字體(一種用像素構(gòu)成字符的字體,而非矢量圖形)創(chuàng)造數(shù)字顯示方式,用圖片存儲(chǔ)起來(lái),而且用第二章介紹過(guò)的拼合技術(shù)來(lái)顯示(如圖4.16)。
我們?cè)贗nterface Builder中放置了六個(gè)視圖,小時(shí)、分鐘、秒鐘各兩個(gè),圖4.17顯示了這六個(gè)視圖是如何在Interface Builder中放置的。如果每個(gè)都用一個(gè)淡出的outlets對(duì)象就會(huì)顯得太多了,所以我們就用了一個(gè)IBOutletCollection對(duì)象把他們和控制器聯(lián)系起來(lái),這樣我們就可以以數(shù)組的方式訪問(wèn)視圖了。清單4.6是代碼實(shí)現(xiàn)。
清單4.6 顯示一個(gè)LCD風(fēng)格的時(shí)鐘
@interface ViewController ()@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *digitViews; @property (nonatomic, weak) NSTimer *timer;  @end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad]; //get spritesheet imageUIImage *digits = [UIImage imageNamed:@"Digits.png"];//set up digit viewsfor (UIView *view in self.digitViews) {//set contentsview.layer.contents = (__bridge id)digits.CGImage;view.layer.contentsRect = CGRectMake(0, 0, 0.1, 1.0);view.layer.contentsGravity = kCAGravityResizeAspect;}//start timerself.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 digitview.layer.contentsRect = CGRectMake(digit * 0.1, 0, 0.1, 1.0); }- (void)tick {//convert time to hours, minutes and secondsNSCalendar *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,這樣做的確起了效果,但是圖片看起來(lái)模糊了??雌饋?lái)默認(rèn)的kCAFilterLinear選項(xiàng)讓我們失望了。
為了能像圖4.19中那樣,我們需要在for循環(huán)中加入如下代碼:
view.layer.magnificationFilter = kCAFilterNearest;
4.6 組透明
UIView有一個(gè)叫做alpha的屬性來(lái)確定視圖的透明度。CALayer有一個(gè)等同的屬性叫做opacity,這兩個(gè)屬性都是影響子層級(jí)的。也就是說(shuō),如果你給一個(gè)圖層設(shè)置了opacity屬性,那它的子圖層都會(huì)受此影響。
iOS常見(jiàn)的做法是把一個(gè)控件的alpha值設(shè)置為0.5(50%)以使其看上去呈現(xiàn)為不可用狀態(tài)。對(duì)于獨(dú)立的視圖來(lái)說(shuō)還不錯(cuò),但是當(dāng)一個(gè)控件有子視圖的時(shí)候就有點(diǎn)奇怪了,圖4.20展示了一個(gè)內(nèi)嵌了UILabel的自定義UIButton;左邊是一個(gè)不透明的按鈕,右邊是50%透明度的相同按鈕。我們可以注意到,里面的標(biāo)簽的輪廓跟按鈕的背景很不搭調(diào)。
這是由透明度的混合疊加造成的,當(dāng)你顯示一個(gè)50%透明度的圖層時(shí),圖層的每個(gè)像素都會(huì)一半顯示自己的顏色,另一半顯示圖層下面的顏色。這是正常的透明度的表現(xiàn)。但是如果圖層包含一個(gè)同樣顯示50%透明的子圖層時(shí),你所看到的視圖,50%來(lái)自子視圖,25%來(lái)了圖層本身的顏色,另外的25%則來(lái)自背景色。
在我們的示例中,按鈕和表情都是白色背景。雖然他們都是50%的可見(jiàn)度,但是合起來(lái)的可見(jiàn)度是75%,所以標(biāo)簽所在的區(qū)域看上去就沒(méi)有周圍的部分那么透明。所以看上去子視圖就高亮了,使得這個(gè)顯示效果都糟透了。
理想狀況下,當(dāng)你設(shè)置了一個(gè)圖層的透明度,你希望它包含的整個(gè)圖層樹(shù)像一個(gè)整體一樣的透明效果。你可以通過(guò)設(shè)置Info.plist文件中的UIViewGroupOpacity為YES來(lái)達(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屬性(見(jiàn)清單4.7)來(lái)實(shí)現(xiàn)組透明的效果,如果它被設(shè)置為YES,在應(yīng)用透明度之前,圖層及其子圖層都會(huì)被整合成一個(gè)整體的圖片,這樣就沒(méi)有透明度混合的問(wèn)題了(如圖4.21)。
為了啟用shouldRasterize屬性,我們?cè)O(shè)置了圖層的rasterizationScale屬性。默認(rèn)情況下,所有圖層拉伸都是1.0, 所以如果你使用了shouldRasterize屬性,你就要確保你設(shè)置了rasterizationScale屬性去匹配屏幕,以防止出現(xiàn)Retina屏幕像素化的問(wèn)題。
當(dāng)shouldRasterize和UIViewGroupOpacity一起的時(shí)候,性能問(wèn)題就出現(xiàn)了(我們?cè)诘?2章『速度』和第15章『圖層性能』將做出介紹),但是性能碰撞都本地化了(譯者注:這句話需要再翻譯)。
清單4.7 使用shouldRasterize屬性解決組透明問(wèn)題
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @end@implementation ViewController- (UIButton *)customButton {//create buttonCGRect frame = CGRectMake(0, 0, 150, 50);UIButton *button = [[UIButton alloc] initWithFrame:frame];button.backgroundColor = [UIColor whiteColor];button.layer.cornerRadius = 10;//add labelframe = 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 buttonUIButton *button1 = [self customButton];button1.center = CGPointMake(50, 150);[self.containerView addSubview:button1];//create translucent buttonUIButton *button2 = [self customButton];button2.center = CGPointMake(250, 150);button2.alpha = 0.5;[self.containerView addSubview:button2];//enable rasterization for the translucent buttonbutton2.layer.shouldRasterize = YES;button2.layer.rasterizationScale = [UIScreen mainScreen].scale; } @end4.7 總結(jié)
這一章介紹了一些可以通過(guò)代碼應(yīng)用到圖層上的視覺(jué)效果,比如圓角,陰影和蒙板。我們也了解了拉伸過(guò)濾器和組透明。
在第五章,『變換』中,我們將會(huì)研究圖層變化和3D轉(zhuǎn)換
五、變換
本節(jié)轉(zhuǎn)載自ios核心動(dòng)畫高級(jí)技巧
很不幸,沒(méi)人能告訴你母體是什么,你只能自己體會(huì) – 駭客帝國(guó)
在第四章“可視效果”中,我們研究了一些增強(qiáng)圖層和它的內(nèi)容顯示效果的一些技術(shù),在這一章中,我們將要研究可以用來(lái)對(duì)圖層旋轉(zhuǎn),擺放或者扭曲的CGAffineTransform,以及可以將扁平物體轉(zhuǎn)換成三維空間對(duì)象的CATransform3D(而不是僅僅對(duì)圓角矩形添加下沉陰影)。
5.1 仿射變換
在第三章“圖層幾何學(xué)”中,我們使用了UIView的transform屬性旋轉(zhuǎn)了鐘的指針,但并沒(méi)有解釋背后運(yùn)作的原理,實(shí)際上UIView的transform屬性是一個(gè)CGAffineTransform類型,用于在二維空間做旋轉(zhuǎn),縮放和平移。CGAffineTransform是一個(gè)可以和二維空間向量(例如CGPoint)做乘法的3X2的矩陣(見(jiàn)圖5.1)。
用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顯示了一些仿射的和非仿射的變換:
創(chuàng)建一個(gè)CGAffineTransform
對(duì)矩陣數(shù)學(xué)做一個(gè)全面的闡述就超出本書的討論范圍了,不過(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度角度(圖5.3)
UIView可以通過(guò)設(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度
注意我們使用的旋轉(zhuǎn)常量是M_PI_4,而不是你想象的45,因?yàn)閕OS的變換函數(shù)使用弧度而不是角度作為單位。弧度用數(shù)學(xué)常量pi的倍數(shù)表示,一個(gè)pi代表180度,所以四分之一的pi就是45度。
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)
混合變換
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è)像素(清單5.2)。圖5.4顯示了圖層變換最后的結(jié)果。
清單5.2 使用若干方法創(chuàng)建一個(gè)復(fù)合變換
- (void)viewDidLoad {[super viewDidLoad];//create a new transformCGAffineTransform transform = CGAffineTransformIdentity; //scale by 50%transform = CGAffineTransformScale(transform, 0.5, 0.5);//rotate by 30 degreestransform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0);//translate by 200 pointstransform = CGAffineTransformTranslate(transform, 200, 0);//apply transform to layerself.layerView.layer.affineTransform = transform; }圖5.4中有些需要注意的地方:圖片向右邊發(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é)果可能不同。
#define DEGREES_TO_RADIANS(x) ((x)/180.0*M_PI)5.2 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的矩陣(圖5.6)。
和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軸比較熟悉了,分別以右和下為正方向(回憶第三章,這是iOS上的標(biāo)準(zhǔn)結(jié)構(gòu),在Mac OS,Y軸朝上為正方向),Z軸和這兩個(gè)軸分別垂直,指向視角外為正方向(圖5.7)。
由圖所見(jiàn),繞Z軸的旋轉(zhuǎn)等同于之前二維空間的仿射旋轉(zhuǎn),但是繞X軸和Y軸的旋轉(zhuǎn)就突破了屏幕的二維空間,并且在用戶視角看來(lái)發(fā)生了傾斜。
舉個(gè)例子:清單5.4的代碼使用了CATransform3DMakeRotation對(duì)視圖內(nèi)的圖層繞Y軸做了45度角的旋轉(zhuǎn),我們可以把視圖向右傾斜,這樣會(huì)看得更清晰。
結(jié)果見(jiàn)圖5.8,但并不像我們期待的那樣。
清單5.4 繞Y軸旋轉(zhuǎn)圖層
看起來(lái)圖層并沒(méi)有被旋轉(zhuǎn),而是僅僅在水平方向上的一個(gè)壓縮,是哪里出了問(wèn)題呢?
其實(shí)完全沒(méi)錯(cuò),視圖看起來(lái)更窄實(shí)際上是因?yàn)槲覀冊(cè)谟靡粋€(gè)斜向的視角看它,而不是透視。
透視投影
在真實(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(圖5.9)用于按比例縮放X和Y的值來(lái)計(jì)算到底要離視角多遠(yuǎn)。
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)用透視的代碼見(jiàn)清單5.5,結(jié)果見(jiàn)圖5.10。
清單5.5 對(duì)變換應(yīng)用透視效果
@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];//create a new transformCATransform3D transform = CATransform3DIdentity;//apply perspectivetransform.m34 = - 1.0 / 500.0;//rotate by 45 degrees along the Y axistransform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);//apply to layerself.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è)點(diǎn)應(yīng)該聚在屏幕中點(diǎn),或者至少是包含所有3D對(duì)象的視圖中點(diǎn)。
Core Animation定義了這個(gè)點(diǎn)位于變換圖層的anchorPoint(通常位于圖層中心,但也有例外,見(jiàn)第三章)。這就是說(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)。
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)做平移。
我們來(lái)用一個(gè)demo舉例說(shuō)明。這里用Interface Builder并排放置兩個(gè)視圖(圖5.12),然后通過(guò)設(shè)置它們?nèi)萜饕晥D的透視變換,我們可以保證它們有相同的透視和滅點(diǎn),代碼見(jiàn)清單5.6,結(jié)果見(jiàn)圖5.13。
清單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 containerCATransform3D perspective = CATransform3DIdentity;perspective.m34 = - 1.0 / 500.0;self.containerView.layer.sublayerTransform = perspective;//rotate layerView1 by 45 degrees along the Y axisCATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);self.layerView1.layer.transform = transform1;//rotate layerView2 by 45 degrees along the Y axisCATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);self.layerView2.layer.transform = transform2; } @end背面
我們既然可以在3D場(chǎng)景下旋轉(zhuǎn)圖層,那么也可以從背面去觀察它。如果我們?cè)谇鍐?.4中把角度修改為M_PI(180度)而不是當(dāng)前的M_PI_4(45度),那么將會(huì)把圖層完全旋轉(zhuǎn)一個(gè)半圈,于是完全背對(duì)了相機(jī)視角。
那么從背部看圖層是什么樣的呢,見(jiàn)圖5.14
如你所見(jiàn),圖層是雙面繪制的,反面顯示的是正面的一個(gè)鏡像圖片。
但這并不是一個(gè)很好的特性,因?yàn)槿绻麍D層包含文本或者其他控件,那用戶看到這些內(nèi)容的鏡像圖片當(dāng)然會(huì)感到困惑。另外也有可能造成資源的浪費(fèi):想象用這些圖層形成一個(gè)不透明的固態(tài)立方體,既然永遠(yuǎn)都看不見(jiàn)這些圖層的背面,那為什么浪費(fèi)GPU來(lái)繪制它們呢?
CALayer有一個(gè)叫做doubleSided的屬性來(lái)控制圖層的背面是否要被繪制。這是一個(gè)BOOL類型,默認(rèn)為YES,如果設(shè)置為NO,那么當(dāng)圖層正面從相機(jī)視角消失的時(shí)候,它將不會(huì)被繪制。
扁平化圖層
如果對(duì)包含已經(jīng)做過(guò)變換的圖層的圖層做反方向的變換將會(huì)發(fā)什么什么呢?是不是有點(diǎn)困惑?見(jiàn)圖5.15
注意做了-45度旋轉(zhuǎn)的內(nèi)部圖層是怎樣抵消旋轉(zhuǎn)45度的圖層,從而恢復(fù)正常狀態(tài)的。
如果內(nèi)部圖層相對(duì)外部圖層做了相反的變換(這里是繞Z軸的旋轉(zhuǎn)),那么按照邏輯這兩個(gè)變換將被相互抵消。
驗(yàn)證一下,相應(yīng)代碼見(jiàn)清單5.7,結(jié)果見(jiàn)5.16
清單5.7 繞Z軸做相反的旋轉(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 degreesCATransform3D 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 degreesCATransform3D 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所示。
但其實(shí)這并不是我們所看到的,相反,我們看到的結(jié)果如圖5.18所示。發(fā)什么了什么呢?內(nèi)部的圖層仍然向左側(cè)旋轉(zhuǎn),并且發(fā)生了扭曲,但按道理說(shuō)它應(yīng)該保持正面朝上,并且顯示正常的方塊。+
這是由于盡管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ì)具體討論。
5.3 固體對(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è)面。
在這個(gè)例子中,我們用Interface Builder來(lái)構(gòu)建立方體的面(圖5.19),我們當(dāng)然可以用代碼來(lái)寫,但是用Interface Builder的好處是可以方便的在每一個(gè)面上添加子視圖。記住這些面僅僅是包含視圖和控件的普通的用戶界面元素,它們完全是我們界面交互的部分,并且當(dāng)把它折成一個(gè)立方體之后也不會(huì)改變這個(gè)性質(zhì)。
這些面視圖并沒(méi)有放置在主視圖當(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)單的解釋。
具體把視圖組織成立方體的代碼見(jiàn)清單5.9,結(jié)果見(jiàn)圖5.20
清單5.9 創(chuàng)建一個(gè)立方體
從這個(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變換矩陣:
這就對(duì)相機(jī)(或者相對(duì)相機(jī)的整個(gè)場(chǎng)景,你也可以這么認(rèn)為)繞Y軸旋轉(zhuǎn)45度,并且繞X軸旋轉(zhuǎn)45度?,F(xiàn)在從另一個(gè)角度去觀察立方體,就能看出它的真實(shí)面貌(圖5.21)。
光亮和陰影
現(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é)果。叉乘代表了光源和圖層之間的角度,從而決定了它有多大程度上的光亮。
清單5.10實(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é)果如圖5.22所示,試著調(diào)整LIGHT_DIRECTION和AMBIENT_LIGHT的值來(lái)切換光線效果
清單5.10 對(duì)立方體的表面應(yīng)用動(dòng)態(tài)的光線效果
點(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)擊按鈕了(圖5.23)。
5.4 總結(jié)
這一章涉及了一些2D和3D的變換。你學(xué)習(xí)了一些矩陣計(jì)算的基礎(chǔ),以及如何用Core Animation創(chuàng)建3D場(chǎng)景。你看到了圖層背后到底是如何呈現(xiàn)的,并且知道了不能把扁平的圖片做成真實(shí)的立體效果,最后我們用demo說(shuō)明了觸摸事件的處理,視圖中圖層添加的層級(jí)順序會(huì)比屏幕上顯示的順序更有意義。
第六章我們會(huì)研究一些Core Animation提供不同功能的具體的CALayer子類。
六、專用圖層
本節(jié)轉(zhuǎn)載自ios核心動(dòng)畫高級(jí)技巧
復(fù)雜的組織都是專門化的
Catharine R. Stimpson
到目前為止,我們已經(jīng)探討過(guò)CALayer類了,同時(shí)我們也了解到了一些非常有用的繪圖和動(dòng)畫功能。但是Core Animation圖層不僅僅能作用于圖片和顏色而已。本章就會(huì)學(xué)習(xí)其他的一些圖層類,進(jìn)一步擴(kuò)展使用Core Animation繪圖的能力。
6.1 CAShapeLayer
在第四章『視覺(jué)效果』我們學(xué)習(xí)到了不使用圖片的情況下用CGPath去構(gòu)造任意形狀的陰影。如果我們能用同樣的方式創(chuàng)建相同形狀的圖層就好了。
CAShapeLayer是一個(gè)通過(guò)矢量圖形而不是bitmap來(lái)繪制的圖層子類。你指定諸如顏色和線寬等屬性,用CGPath來(lái)定義想要繪制的圖形,最后CAShapeLayer就自動(dòng)渲染出來(lái)了。當(dāng)然,你也可以用Core Graphics直接向原始的CALyer的內(nèi)容中繪制一個(gè)路徑,相比直下,使用CAShapeLayer有以下一些優(yōu)點(diǎn):
- 渲染快速。CAShapeLayer使用了硬件加速,繪制同一圖形會(huì)比用Core Graphics快很多。
- 高效使用內(nèi)存。一個(gè)CAShapeLayer不需要像普通CALayer一樣創(chuàng)建一個(gè)寄宿圖形,所以無(wú)論有多大,都不會(huì)占用太多的內(nèi)存。
- 不會(huì)被圖層邊界剪裁掉。一個(gè)CAShapeLayer可以在邊界之外繪制。你的圖層路徑不會(huì)像在使用Core Graphics的普通CALayer一樣被剪裁掉(如我們?cè)诘诙滤?jiàn))。
- 不會(huì)出現(xiàn)像素化。當(dāng)你給CAShapeLayer做3D變換時(shí),它不像一個(gè)有寄宿圖的普通圖層一樣變得像素化。
創(chuàng)建一個(gè)CGPath
CAShapeLayer可以用來(lái)繪制所有能夠通過(guò)CGPath來(lái)表示的形狀。這個(gè)形狀不一定要閉合,圖層路徑也不一定要不可破,事實(shí)上你可以在一個(gè)圖層上繪制好幾個(gè)不同的形狀。你可以控制一些屬性比如lineWith(線寬,用點(diǎn)表示單位),lineCap(線條結(jié)尾的樣子),和lineJoin(線條之間的結(jié)合點(diǎn)的樣子);但是在圖層層面你只有一次機(jī)會(huì)設(shè)置這些屬性。如果你想用不同顏色或風(fēng)格來(lái)繪制多個(gè)形狀,就不得不為每個(gè)形狀準(zhǔn)備一個(gè)圖層了。
清單6.1 的代碼用一個(gè)CAShapeLayer渲染一個(gè)簡(jiǎn)單的火柴人。CAShapeLayer屬性是CGPathRef類型,但是我們用UIBezierPath幫助類創(chuàng)建了圖層路徑,這樣我們就不用考慮人工釋放CGPath了。圖6.1是代碼運(yùn)行的結(jié)果。雖然還不是很完美,但是總算知道了大意對(duì)吧!
清單6.1 用CAShapeLayer繪制一個(gè)火柴人
#import "DrawingView.h" #import @interface ViewController ()@property (nonatomic, weak) IBOutlet UIView *containerView;@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];//create pathUIBezierPath *path = [[UIBezierPath alloc] init];[path moveToPoint:CGPointMake(175, 100)];[path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:2*M_PI clockwise:YES];[path moveToPoint:CGPointMake(150, 125)];[path addLineToPoint:CGPointMake(150, 175)];[path addLineToPoint:CGPointMake(125, 225)];[path moveToPoint:CGPointMake(150, 175)];[path addLineToPoint:CGPointMake(175, 225)];[path moveToPoint:CGPointMake(100, 150)];[path addLineToPoint:CGPointMake(200, 150)];//create shape layerCAShapeLayer *shapeLayer = [CAShapeLayer layer];shapeLayer.strokeColor = [UIColor redColor].CGColor;shapeLayer.fillColor = [UIColor clearColor].CGColor;shapeLayer.lineWidth = 5;shapeLayer.lineJoin = kCALineJoinRound;shapeLayer.lineCap = kCALineCapRound;shapeLayer.path = path.CGPath;//add it to our view[self.containerView.layer addSublayer:shapeLayer]; } @end圓角
第二章里面提到了CAShapeLayer為創(chuàng)建圓角視圖提供了一個(gè)方法,就是CALayer的cornerRadius屬性(譯者注:其實(shí)是在第四章提到的)。雖然使用CAShapeLayer類需要更多的工作,但是它有一個(gè)優(yōu)勢(shì)就是可以單獨(dú)指定每個(gè)角。
我們創(chuàng)建圓角矩形其實(shí)就是人工繪制單獨(dú)的直線和弧度,但是事實(shí)上UIBezierPath有自動(dòng)繪制圓角矩形的構(gòu)造方法,下面這段代碼繪制了一個(gè)有三個(gè)圓角一個(gè)直角的矩形:
//define path parameters CGRect rect = CGRectMake(50, 50, 100, 100); CGSize radii = CGSizeMake(20, 20); UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomRight | UIRectCornerBottomLeft; //create path UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii];我們可以通過(guò)這個(gè)圖層路徑繪制一個(gè)既有直角又有圓角的視圖。如果我們想依照此圖形來(lái)剪裁視圖內(nèi)容,我們可以把CAShapeLayer作為視圖的宿主圖層,而不是添加一個(gè)子視圖(圖層蒙板的詳細(xì)解釋見(jiàn)第四章『視覺(jué)效果』)。
6.2 CATextLayer
用戶界面是無(wú)法從一個(gè)單獨(dú)的圖片里面構(gòu)建的。一個(gè)設(shè)計(jì)良好的圖標(biāo)能夠很好地表現(xiàn)一個(gè)按鈕或控件的意圖,不過(guò)你遲早都要需要一個(gè)不錯(cuò)的老式風(fēng)格的文本標(biāo)簽。
如果你想在一個(gè)圖層里面顯示文字,完全可以借助圖層代理直接將字符串使用Core Graphics寫入圖層的內(nèi)容(這就是UILabel的精髓)。如果越過(guò)寄宿于圖層的視圖,直接在圖層上操作,那其實(shí)相當(dāng)繁瑣。你要為每一個(gè)顯示文字的圖層創(chuàng)建一個(gè)能像圖層代理一樣工作的類,還要邏輯上判斷哪個(gè)圖層需要顯示哪個(gè)字符串,更別提還要記錄不同的字體,顏色等一系列亂七八糟的東西。
萬(wàn)幸的是這些都是不必要的,Core Animation提供了一個(gè)CALayer的子類CATextLayer,它以圖層的形式包含了UILabel幾乎所有的繪制特性,并且額外提供了一些新的特性。
同樣,CATextLayer也要比UILabel渲染得快得多。很少有人知道在iOS 6及之前的版本,UILabel其實(shí)是通過(guò)WebKit來(lái)實(shí)現(xiàn)繪制的,這樣就造成了當(dāng)有很多文字的時(shí)候就會(huì)有極大的性能壓力。而CATextLayer使用了Core text,并且渲染得非???。
讓我們來(lái)嘗試用CATextLayer來(lái)顯示一些文字。清單6.2的代碼實(shí)現(xiàn)了這一功能,結(jié)果如圖6.2所示。
清單6.2 用CATextLayer來(lái)實(shí)現(xiàn)一個(gè)UILabel
如果你仔細(xì)看這個(gè)文本,你會(huì)發(fā)現(xiàn)一個(gè)奇怪的地方:這些文本有一些像素化了。這是因?yàn)椴](méi)有以Retina的方式渲染,第二章提到了這個(gè)contentScale屬性,用來(lái)決定圖層內(nèi)容應(yīng)該以怎樣的分辨率來(lái)渲染。contentsScale并不關(guān)心屏幕的拉伸因素而總是默認(rèn)為1.0。如果我們想以Retina的質(zhì)量來(lái)顯示文字,我們就得手動(dòng)地設(shè)置CATextLayer的contentsScale屬性,如下:
textLayer.contentsScale = [UIScreen mainScreen].scale;
這樣就解決了這個(gè)問(wèn)題(如圖6.3)
CATextLayer的font屬性不是一個(gè)UIFont類型,而是一個(gè)CFTypeRef類型。這樣可以根據(jù)你的具體需要來(lái)決定字體屬性應(yīng)該是用CGFontRef類型還是CTFontRef類型(Core Text字體)。同時(shí)字體大小也是用fontSize屬性單獨(dú)設(shè)置的,因?yàn)镃TFontRef和CGFontRef并不像UIFont一樣包含點(diǎn)大小。這個(gè)例子會(huì)告訴你如何將UIFont轉(zhuǎn)換成CGFontRef。
另外,CATextLayer的string屬性并不是你想象的NSString類型,而是id類型。這樣你既可以用NSString也可以用NSAttributedString來(lái)指定文本了(注意,NSAttributedString并不是NSString的子類)。屬性化字符串是iOS用來(lái)渲染字體風(fēng)格的機(jī)制,它以特定的方式來(lái)決定指定范圍內(nèi)的字符串的原始信息,比如字體,顏色,字重,斜體等。
富文本
iOS 6中,Apple給UILabel和其他UIKit文本視圖添加了直接的屬性化字符串的支持,應(yīng)該說(shuō)這是一個(gè)很方便的特性。不過(guò)事實(shí)上從iOS3.2開(kāi)始CATextLayer就已經(jīng)支持屬性化字符串了。這樣的話,如果你想要支持更低版本的iOS系統(tǒng),CATextLayer無(wú)疑是你向界面中增加富文本的好辦法,而且也不用去跟復(fù)雜的Core Text打交道,也省了用UIWebView的麻煩。
讓我們編輯一下示例使用到NSAttributedString(見(jiàn)清單6.3).iOS 6及以上我們可以用新的NSTextAttributeName實(shí)例來(lái)設(shè)置我們的字符串屬性,但是練習(xí)的目的是為了演示在iOS 5及以下,所以我們用了Core Text,也就是說(shuō)你需要把Core Text framework添加到你的項(xiàng)目中。否則,編譯器是無(wú)法識(shí)別屬性常量的。
圖6.4是代碼運(yùn)行結(jié)果(注意那個(gè)紅色的下劃線文本)
清單6.3 用NSAttributedString實(shí)現(xiàn)一個(gè)富文本標(biāo)簽。
行距和字距
有必要提一下的是,由于繪制的實(shí)現(xiàn)機(jī)制不同(Core Text和WebKit),用CATextLayer渲染和用UILabel渲染出的文本行距和字距也不是不盡相同的。
有二者的差異程度(由使用的字體和字符決定)總的來(lái)說(shuō)挺小,但是如果你想正確的顯示普通便簽和CATextLayer就一定要記住這一點(diǎn)。
UILabel的替代品
有我們已經(jīng)證實(shí)了CATextLayer比UILabel有著更好的性能表現(xiàn),同時(shí)還有額外的布局選項(xiàng)并且在iOS 5上支持富文本。但是與一般的標(biāo)簽比較而言會(huì)更加繁瑣一些。如果我們真的在需求一個(gè)UILabel的可用替代品,最好是能夠在Interface Builder上創(chuàng)建我們的標(biāo)簽,而且盡可能地像一般的視圖一樣正常工作。
有我們應(yīng)該繼承UILabel,然后添加一個(gè)子圖層CATextLayer并重寫顯示文本的方法。但是仍然會(huì)有由UILabel的-drawRect:方法創(chuàng)建的空寄宿圖。而且由于CALayer不支持自動(dòng)縮放和自動(dòng)布局,子視圖并不是主動(dòng)跟蹤視圖邊界的大小,所以每次視圖大小被更改,我們不得不手動(dòng)更新子圖層的邊界。
有我們真正想要的是一個(gè)用CATextLayer作為宿主圖層的UILabel子類,這樣就可以隨著視圖自動(dòng)調(diào)整大小而且也沒(méi)有冗余的寄宿圖啦。
有就像我們?cè)诘谝徽隆簣D層樹(shù)』討論的一樣,每一個(gè)UIView都是寄宿在一個(gè)CALayer的示例上。這個(gè)圖層是由視圖自動(dòng)創(chuàng)建和管理的,那我們可以用別的圖層類型替代它么?一旦被創(chuàng)建,我們就無(wú)法代替這個(gè)圖層了。但是如果我們繼承了UIView,那我們就可以重寫+layerClass方法使得在創(chuàng)建的時(shí)候能返回一個(gè)不同的圖層子類。UIView會(huì)在初始化的時(shí)候調(diào)用+layerClass方法,然后用它的返回類型來(lái)創(chuàng)建宿主圖層。
有清單6.4 演示了一個(gè)UILabel子類LayerLabel用CATextLayer繪制它的問(wèn)題,而不是調(diào)用一般的UILabel使用的較慢的-drawRect:方法。LayerLabel示例既可以用代碼實(shí)現(xiàn),也可以在Interface Builder實(shí)現(xiàn),只要把普通的標(biāo)簽拖入視圖之中,然后設(shè)置它的類是LayerLabel就可以了。
清單6.4 使用CATextLayer的UILabel子類:LayerLabel
#import "LayerLabel.h" #import @implementation LayerLabel + (Class)layerClass {//this makes our label create a CATextLayer //instead of a regular CALayer for its backing layerreturn [CATextLayer class]; }- (CATextLayer *)textLayer {return (CATextLayer *)self.layer; }- (void)setUp {//set defaults from UILabel settingsself.text = self.text;self.textColor = self.textColor;self.font = self.font;//we should really derive these from the UILabel settings too//but that's complicated, so for now we'll just hard-code them[self textLayer].alignmentMode = kCAAlignmentJustified;[self textLayer].wrapped = YES;[self.layer display]; }- (id)initWithFrame:(CGRect)frame {//called when creating label programmaticallyif (self = [super initWithFrame:frame]) {[self setUp];}return self; }- (void)awakeFromNib {//called when creating label using Interface Builder[self setUp]; }- (void)setText:(NSString *)text {super.text = text;//set layer text[self textLayer].string = text; }- (void)setTextColor:(UIColor *)textColor {super.textColor = textColor;//set layer text color[self textLayer].foregroundColor = textColor.CGColor; }- (void)setFont:(UIFont *)font {super.font = font;//set layer fontCFStringRef fontName = (__bridge CFStringRef)font.fontName;CGFontRef fontRef = CGFontCreateWithFontName(fontName);[self textLayer].font = fontRef;[self textLayer].fontSize = font.pointSize;CGFontRelease(fontRef); } @end如果你運(yùn)行代碼,你會(huì)發(fā)現(xiàn)文本并沒(méi)有像素化,而我們也沒(méi)有設(shè)置contentsScale屬性。把CATextLayer作為宿主圖層的另一好處就是視圖自動(dòng)設(shè)置了contentsScale屬性。
在這個(gè)簡(jiǎn)單的例子中,我們只是實(shí)現(xiàn)了UILabel的一部分風(fēng)格和布局屬性,不過(guò)稍微再改進(jìn)一下我們就可以創(chuàng)建一個(gè)支持UILabel所有功能甚至更多功能的LayerLabel類(你可以在一些線上的開(kāi)源項(xiàng)目中找到)。
如果你打算支持iOS 6及以上,基于CATextLayer的標(biāo)簽可能就有有些局限性。但是總得來(lái)說(shuō),如果想在app里面充分利用CALayer子類,用+layerClass來(lái)創(chuàng)建基于不同圖層的視圖是一個(gè)簡(jiǎn)單可復(fù)用的方法。
6.3 CATransformLayer
當(dāng)我們?cè)跇?gòu)造復(fù)雜的3D事物的時(shí)候,如果能夠組織獨(dú)立元素就太方便了。比如說(shuō),你想創(chuàng)造一個(gè)孩子的手臂:你就需要確定哪一部分是孩子的手腕,哪一部分是孩子的前臂,哪一部分是孩子的肘,哪一部分是孩子的上臂,哪一部分是孩子的肩膀等等。
當(dāng)然是允許獨(dú)立地移動(dòng)每個(gè)區(qū)域的啦。以肘為指點(diǎn)會(huì)移動(dòng)前臂和手,而不是肩膀。Core Animation圖層很容易就可以讓你在2D環(huán)境下做出這樣的層級(jí)體系下的變換,但是3D情況下就不太可能,因?yàn)樗械膱D層都把他的孩子都平面化到一個(gè)場(chǎng)景中(第五章『變換』有提到)。
CATransformLayer解決了這個(gè)問(wèn)題,CATransformLayer不同于普通的CALayer,因?yàn)樗荒茱@示它自己的內(nèi)容。只有當(dāng)存在了一個(gè)能作用域子圖層的變換它才真正存在。CATransformLayer并不平面化它的子圖層,所以它能夠用于構(gòu)造一個(gè)層級(jí)的3D結(jié)構(gòu),比如我的手臂示例。
用代碼創(chuàng)建一個(gè)手臂需要相當(dāng)多的代碼,所以我就演示得更簡(jiǎn)單一些吧:在第五章的立方體示例,我們將通過(guò)旋轉(zhuǎn)camara來(lái)解決圖層平面化問(wèn)題而不是像立方體示例代碼中用的sublayerTransform。這是一個(gè)非常不錯(cuò)的技巧,但是只能作用域單個(gè)對(duì)象上,如果你的場(chǎng)景包含兩個(gè)立方體,那我們就不能用這個(gè)技巧單獨(dú)旋轉(zhuǎn)他們了。
那么,就讓我們來(lái)試一試CATransformLayer吧,第一個(gè)問(wèn)題就來(lái)了:在第五章,我們是用多個(gè)視圖來(lái)構(gòu)造了我們的立方體,而不是單獨(dú)的圖層。我們不能在不打亂已有的視圖層次的前提下在一個(gè)本身不是有寄宿圖的圖層中放置一個(gè)寄宿圖圖層。我們可以創(chuàng)建一個(gè)新的UIView子類寄宿在CATransformLayer(用+layerClass方法)之上。但是,為了簡(jiǎn)化案例,我們僅僅重建了一個(gè)單獨(dú)的圖層,而不是使用視圖。這意味著我們不能像第五章一樣在立方體表面顯示按鈕和標(biāo)簽,不過(guò)我們現(xiàn)在也用不到這個(gè)特性。
清單6.5就是代碼。我們以我們?cè)诘谖逭率褂眠^(guò)的相同基本邏輯放置立方體。但是并不像以前那樣直接將立方面添加到容器視圖的宿主圖層,我們將他們放置到一個(gè)CATransformLayer中創(chuàng)建一個(gè)獨(dú)立的立方體對(duì)象,然后將兩個(gè)這樣的立方體放進(jìn)容器中。我們隨機(jī)地給立方面染色以將他們區(qū)分開(kāi)來(lái),這樣就不用靠標(biāo)簽或是光亮來(lái)區(qū)分他們。圖6.5是運(yùn)行結(jié)果。
清單6.5 用CATransformLayer裝配一個(gè)3D圖層體系
@interface ViewController ()@property (nonatomic, weak) IBOutlet UIView *containerView;@end@implementation ViewController- (CALayer *)faceWithTransform:(CATransform3D)transform {//create cube face layerCALayer *face = [CALayer layer];face.frame = CGRectMake(-50, -50, 100, 100);//apply a random colorCGFloat red = (rand() / (double)INT_MAX);CGFloat green = (rand() / (double)INT_MAX);CGFloat blue = (rand() / (double)INT_MAX);face.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;//apply the transform and returnface.transform = transform;return face; }- (CALayer *)cubeWithTransform:(CATransform3D)transform {//create cube layerCATransformLayer *cube = [CATransformLayer layer];//add cube face 1CATransform3D ct = CATransform3DMakeTranslation(0, 0, 50);[cube addSublayer:[self faceWithTransform:ct]];//add cube face 2ct = CATransform3DMakeTranslation(50, 0, 0);ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);[cube addSublayer:[self faceWithTransform:ct]];//add cube face 3ct = CATransform3DMakeTranslation(0, -50, 0);ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);[cube addSublayer:[self faceWithTransform:ct]];//add cube face 4ct = CATransform3DMakeTranslation(0, 50, 0);ct = CATransform3DRotate(ct, -M_PI_2, 1, 0, 0);[cube addSublayer:[self faceWithTransform:ct]];//add cube face 5ct = CATransform3DMakeTranslation(-50, 0, 0);ct = CATransform3DRotate(ct, -M_PI_2, 0, 1, 0);[cube addSublayer:[self faceWithTransform:ct]];//add cube face 6ct = CATransform3DMakeTranslation(0, 0, -50);ct = CATransform3DRotate(ct, M_PI, 0, 1, 0);[cube addSublayer:[self faceWithTransform:ct]];//center the cube layer within the containerCGSize containerSize = self.containerView.bounds.size;cube.position = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);//apply the transform and returncube.transform = transform;return cube; }- (void)viewDidLoad {[super viewDidLoad];//set up the perspective transformCATransform3D pt = CATransform3DIdentity;pt.m34 = -1.0 / 500.0;self.containerView.layer.sublayerTransform = pt;//set up the transform for cube 1 and add itCATransform3D c1t = CATransform3DIdentity;c1t = CATransform3DTranslate(c1t, -100, 0, 0);CALayer *cube1 = [self cubeWithTransform:c1t];[self.containerView.layer addSublayer:cube1];//set up the transform for cube 2 and add itCATransform3D c2t = CATransform3DIdentity;c2t = CATransform3DTranslate(c2t, 100, 0, 0);c2t = CATransform3DRotate(c2t, -M_PI_4, 1, 0, 0);c2t = CATransform3DRotate(c2t, -M_PI_4, 0, 1, 0);CALayer *cube2 = [self cubeWithTransform:c2t];[self.containerView.layer addSublayer:cube2]; } @end6.4 CAGradientLayer
CAGradientLayer是用來(lái)生成兩種或更多顏色平滑漸變的。用Core Graphics復(fù)制一個(gè)CAGradientLayer并將內(nèi)容繪制到一個(gè)普通圖層的寄宿圖也是有可能的,但是CAGradientLayer的真正好處在于繪制使用了硬件加速。
基礎(chǔ)漸變
我們將從一個(gè)簡(jiǎn)單的紅變藍(lán)的對(duì)角線漸變開(kāi)始(見(jiàn)清單6.6).這些漸變色彩放在一個(gè)數(shù)組中,并賦給colors屬性。這個(gè)數(shù)組成員接受CGColorRef類型的值(并不是從NSObject派生而來(lái)),所以我們要用通過(guò)bridge轉(zhuǎn)換以確保編譯正常。
CAGradientLayer也有startPoint和endPoint屬性,他們決定了漸變的方向。這兩個(gè)參數(shù)是以單位坐標(biāo)系進(jìn)行的定義,所以左上角坐標(biāo)是{0, 0},右下角坐標(biāo)是{1, 1}。代碼運(yùn)行結(jié)果如圖6.6
清單6.6 簡(jiǎn)單的兩種顏色的對(duì)角線漸變
多重漸變
如果你愿意,colors屬性可以包含很多顏色,所以創(chuàng)建一個(gè)彩虹一樣的多重漸變也是很簡(jiǎn)單的。默認(rèn)情況下,這些顏色在空間上均勻地被渲染,但是我們可以用locations屬性來(lái)調(diào)整空間。locations屬性是一個(gè)浮點(diǎn)數(shù)值的數(shù)組(以NSNumber包裝)。這些浮點(diǎn)數(shù)定義了colors屬性中每個(gè)不同顏色的位置,同樣的,也是以單位坐標(biāo)系進(jìn)行標(biāo)定。0.0代表著漸變的開(kāi)始,1.0代表著結(jié)束。
locations數(shù)組并不是強(qiáng)制要求的,但是如果你給它賦值了就一定要確保locations的數(shù)組大小和colors數(shù)組大小一定要相同,否則你將會(huì)得到一個(gè)空白的漸變。
清單6.7展示了一個(gè)基于清單6.6的對(duì)角線漸變的代碼改造。現(xiàn)在變成了從紅到黃最后到綠色的漸變。locations數(shù)組指定了0.0,0.25和0.5三個(gè)數(shù)值,這樣這三個(gè)漸變就有點(diǎn)像擠在了左上角。(如圖6.7).
清單6.7 在漸變上使用locations
6.5 CAReplicatorLayer
CAReplicatorLayer的目的是為了高效生成許多相似的圖層。它會(huì)繪制一個(gè)或多個(gè)圖層的子圖層,并在每個(gè)復(fù)制體上應(yīng)用不同的變換??瓷先パ菔灸軌蚋咏忉屵@些,我們來(lái)寫個(gè)例子吧。
重復(fù)圖層(Repeating Layers)
清單6.8中,我們?cè)谄聊坏闹虚g創(chuàng)建了一個(gè)小白色方塊圖層,然后用CAReplicatorLayer生成十個(gè)圖層組成一個(gè)圓圈。instanceCount屬性指定了圖層需要重復(fù)多少次。instanceTransform指定了一個(gè)CATransform3D3D變換(這種情況下,下一圖層的位移和旋轉(zhuǎn)將會(huì)移動(dòng)到圓圈的下一個(gè)點(diǎn))。
變換是逐步增加的,每個(gè)實(shí)例都是相對(duì)于前一實(shí)例布局。這就是為什么這些復(fù)制體最終不會(huì)出現(xiàn)在同意位置上,圖6.8是代碼運(yùn)行結(jié)果。
清單6.8 用CAReplicatorLayer重復(fù)圖層
注意到當(dāng)圖層在重復(fù)的時(shí)候,他們的顏色也在變化:這是用instanceBlueOffset和instanceGreenOffset屬性實(shí)現(xiàn)的。通過(guò)逐步減少藍(lán)色和綠色通道,我們逐漸將圖層顏色轉(zhuǎn)換成了紅色。這個(gè)復(fù)制效果看起來(lái)很酷,但是CAReplicatorLayer真正應(yīng)用到實(shí)際程序上的場(chǎng)景比如:一個(gè)游戲中導(dǎo)彈的軌跡云,或者粒子爆炸(盡管iOS 5已經(jīng)引入了CAEmitterLayer,它更適合創(chuàng)建任意的粒子效果)。除此之外,還有一個(gè)實(shí)際應(yīng)用是:反射。
反射
使用CAReplicatorLayer并應(yīng)用一個(gè)負(fù)比例變換于一個(gè)復(fù)制圖層,你就可以創(chuàng)建指定視圖(或整個(gè)視圖層次)內(nèi)容的鏡像圖片,這樣就創(chuàng)建了一個(gè)實(shí)時(shí)的『反射』效果。讓我們來(lái)嘗試實(shí)現(xiàn)這個(gè)創(chuàng)意:指定一個(gè)繼承于UIView的ReflectionView,它會(huì)自動(dòng)產(chǎn)生內(nèi)容的反射效果。實(shí)現(xiàn)這個(gè)效果的代碼很簡(jiǎn)單(見(jiàn)清單6.9),實(shí)際上用ReflectionView實(shí)現(xiàn)這個(gè)效果會(huì)更簡(jiǎn)單,我們只需要把ReflectionView的實(shí)例放置于Interface Builder(見(jiàn)圖6.9),它就會(huì)實(shí)時(shí)生成子視圖的反射,而不需要?jiǎng)e的代碼(見(jiàn)圖6.10).
清單6.9 用CAReplicatorLayer自動(dòng)繪制反射
開(kāi)源代碼ReflectionView完成了一個(gè)自適應(yīng)的漸變淡出效果(用CAGradientLayer
和圖層蒙板實(shí)現(xiàn))
6.6 CAScrollLayer
對(duì)于一個(gè)未轉(zhuǎn)換的圖層,它的bounds和它的frame是一樣的,frame屬性是由bounds屬性自動(dòng)計(jì)算而出的,所以更改任意一個(gè)值都會(huì)更新其他值。
但是如果你只想顯示一個(gè)大圖層里面的一小部分呢。比如說(shuō),你可能有一個(gè)很大的圖片,你希望用戶能夠隨意滑動(dòng),或者是一個(gè)數(shù)據(jù)或文本的長(zhǎng)列表。在一個(gè)典型的iOS應(yīng)用中,你可能會(huì)用到UITableView或是UIScrollView,但是對(duì)于獨(dú)立的圖層來(lái)說(shuō),什么會(huì)等價(jià)于剛剛提到的UITableView和UIScrollView呢?
在第二章中,我們探索了圖層的contentsRect屬性的用法,它的確是能夠解決在圖層中小地方顯示大圖片的解決方法。但是如果你的圖層包含子圖層那它就不是一個(gè)非常好的解決方案,因?yàn)?#xff0c;這樣做的話每次你想『滑動(dòng)』可視區(qū)域的時(shí)候,你就需要手工重新計(jì)算并更新所有的子圖層位置。
這個(gè)時(shí)候就需要CAScrollLayer了。CAScrollLayer有一個(gè)-scrollToPoint:方法,它自動(dòng)適應(yīng)bounds的原點(diǎn)以便圖層內(nèi)容出現(xiàn)在滑動(dòng)的地方。注意,這就是它做的所有事情。前面提到過(guò),Core Animation并不處理用戶輸入,所以CAScrollLayer并不負(fù)責(zé)將觸摸事件轉(zhuǎn)換為滑動(dòng)事件,既不渲染滾動(dòng)條,也不實(shí)現(xiàn)任何iOS指定行為例如滑動(dòng)反彈(當(dāng)視圖滑動(dòng)超多了它的邊界的將會(huì)反彈回正確的地方)。
讓我們來(lái)用CAScrollLayer來(lái)常見(jiàn)一個(gè)基本的UIScrollView替代品。我們將會(huì)用CAScrollLayer作為視圖的宿主圖層,并創(chuàng)建一個(gè)自定義的UIView,然后用UIPanGestureRecognizer實(shí)現(xiàn)觸摸事件響應(yīng)。這段代碼見(jiàn)清單6.10. 圖6.11是運(yùn)行效果:ScrollView顯示了一個(gè)大于它的frame的UIImageView。
清單6.10 用CAScrollLayer實(shí)現(xiàn)滑動(dòng)視圖
圖6.11 用UIScrollView創(chuàng)建一個(gè)湊合的滑動(dòng)視圖
不同于UIScrollView,我們定制的滑動(dòng)視圖類并沒(méi)有實(shí)現(xiàn)任何形式的邊界檢查(bounds checking)。圖層內(nèi)容極有可能滑出視圖的邊界并無(wú)限滑下去。CAScrollLayer并沒(méi)有等同于UIScrollView中contentSize的屬性,所以當(dāng)CAScrollLayer滑動(dòng)的時(shí)候完全沒(méi)有一個(gè)全局的可滑動(dòng)區(qū)域的概念,也無(wú)法自適應(yīng)它的邊界原點(diǎn)至你指定的值。它之所以不能自適應(yīng)邊界大小是因?yàn)樗恍枰?#xff0c;內(nèi)容完全可以超過(guò)邊界。
那你一定會(huì)奇怪用CAScrollLayer的意義到底何在,因?yàn)槟憧梢院?jiǎn)單地用一個(gè)普通的CALayer然后手動(dòng)適應(yīng)邊界原點(diǎn)啊。真相其實(shí)并不復(fù)雜,UIScrollView并沒(méi)有用CAScrollLayer,事實(shí)上,就是簡(jiǎn)單的通過(guò)直接操作圖層邊界來(lái)實(shí)現(xiàn)滑動(dòng)。
CAScrollLayer有一個(gè)潛在的有用特性。如果你查看CAScrollLayer的頭文件,你就會(huì)注意到有一個(gè)擴(kuò)展分類實(shí)現(xiàn)了一些方法和屬性:
- (void)scrollPoint:(CGPoint)p; - (void)scrollRectToVisible:(CGRect)r; @property(readonly) CGRect visibleRect;看到這些方法和屬性名,你也許會(huì)以為這些方法給每個(gè)CALayer實(shí)例增加了滑動(dòng)功能。但是事實(shí)上他們只是放置在CAScrollLayer中的圖層的實(shí)用方法。scrollPoint:方法從圖層樹(shù)中查找并找到第一個(gè)可用的CAScrollLayer,然后滑動(dòng)它使得指定點(diǎn)成為可視的。scrollRectToVisible:方法實(shí)現(xiàn)了同樣的事情只不過(guò)是作用在一個(gè)矩形上的。visibleRect屬性決定圖層(如果存在的話)的哪部分是當(dāng)前的可視區(qū)域。如果你自己實(shí)現(xiàn)這些方法就會(huì)相對(duì)容易明白一點(diǎn),但是CAScrollLayer幫你省了這些麻煩,所以當(dāng)涉及到實(shí)現(xiàn)圖層滑動(dòng)的時(shí)候就可以用上了。
6.7 CATiledLayer
有些時(shí)候你可能需要繪制一個(gè)很大的圖片,常見(jiàn)的例子就是一個(gè)高像素的照片或者是地球表面的詳細(xì)地圖。iOS應(yīng)用通暢運(yùn)行在內(nèi)存受限的設(shè)備上,所以讀取整個(gè)圖片到內(nèi)存中是不明智的。載入大圖可能會(huì)相當(dāng)?shù)芈?#xff0c;那些對(duì)你看上去比較方便的做法(在主線程調(diào)用UIImage的-imageNamed:方法或者-imageWithContentsOfFile:方法)將會(huì)阻塞你的用戶界面,至少會(huì)引起動(dòng)畫卡頓現(xiàn)象。
能高效繪制在iOS上的圖片也有一個(gè)大小限制。所有顯示在屏幕上的圖片最終都會(huì)被轉(zhuǎn)化為OpenGL紋理,同時(shí)OpenGL有一個(gè)最大的紋理尺寸(通常是2048 * 2048,或4096 * 4096,這個(gè)取決于設(shè)備型號(hào))。如果你想在單個(gè)紋理中顯示一個(gè)比這大的圖,即便圖片已經(jīng)存在于內(nèi)存中了,你仍然會(huì)遇到很大的性能問(wèn)題,因?yàn)镃ore Animation強(qiáng)制用CPU處理圖片而不是更快的GPU(見(jiàn)第12章『速度的曲調(diào)』,和第13章『高效繪圖』,它更加詳細(xì)地解釋了軟件繪制和硬件繪制)。
CATiledLayer為載入大圖造成的性能問(wèn)題提供了一個(gè)解決方案:將大圖分解成小片然后將他們單獨(dú)按需載入。讓我們用實(shí)驗(yàn)來(lái)證明一下。
小片裁剪
這個(gè)示例中,我們將會(huì)從一個(gè)2048 * 2048分辨率的雪人圖片入手。為了能夠從CATiledLayer中獲益,我們需要把這個(gè)圖片裁切成許多小一些的圖片。你可以通過(guò)代碼來(lái)完成這件事情,但是如果你在運(yùn)行時(shí)讀入整個(gè)圖片并裁切,那CATiledLayer這些所有的性能優(yōu)點(diǎn)就損失殆盡了。理想情況下來(lái)說(shuō),最好能夠逐個(gè)步驟來(lái)實(shí)現(xiàn)。
清單6.11 演示了一個(gè)簡(jiǎn)單的Mac OS命令行程序,它用CATiledLayer將一個(gè)圖片裁剪成小圖并存儲(chǔ)到不同的文件中。
清單6.11 裁剪圖片成小圖的終端程序
這個(gè)程序?qū)?048 * 2048分辨率的雪人圖案裁剪成了64個(gè)不同的256*256的小圖。(256*256是CATiledLayer的默認(rèn)小圖大小,默認(rèn)大小可以通過(guò)tileSize屬性更改)。程序接受一個(gè)圖片路徑作為命令行的第一個(gè)參數(shù)。我們可以在編譯的scheme將路徑參數(shù)硬編碼然后就可以在Xcode中運(yùn)行了,但是以后作用在另一個(gè)圖片上就不方便了。所以,我們編譯了這個(gè)程序并把它保存到敏感的地方,然后從終端調(diào)用,如下面所示:
> path/to/TileCutterApp path/to/Snowman.jpg
這個(gè)程序相當(dāng)基礎(chǔ),但是能夠輕易地?cái)U(kuò)展支持額外的參數(shù)比如小圖大小,或者導(dǎo)出格式等等。運(yùn)行結(jié)果是64個(gè)新圖的序列,如下面命名:
Snowman_00_00.jpg Snowman_00_01.jpg Snowman_00_02.jpg ... Snowman_07_07.jpg既然我們有了裁切后的小圖,我們就要讓iOS程序用到他們。CATiledLayer很好地和UIScrollView集成在一起。除了設(shè)置圖層和滑動(dòng)視圖邊界以適配整個(gè)圖片大小,我們真正要做的就是實(shí)現(xiàn)-drawLayer:inContext:方法,當(dāng)需要載入新的小圖時(shí),CATiledLayer就會(huì)調(diào)用到這個(gè)方法。
清單6.12演示了代碼。圖6.12是代碼運(yùn)行結(jié)果。
清單6.12 一個(gè)簡(jiǎn)單的滾動(dòng)CATiledLayer實(shí)現(xiàn)
當(dāng)你滑動(dòng)這個(gè)圖片,你會(huì)發(fā)現(xiàn)當(dāng)CATiledLayer載入小圖的時(shí)候,他們會(huì)淡入到界面中。這是CATiledLayer的默認(rèn)行為。(你可能已經(jīng)在iOS 6之前的蘋果地圖程序中見(jiàn)過(guò)這個(gè)效果)你可以用fadeDuration屬性改變淡入時(shí)長(zhǎng)或直接禁用掉。CATiledLayer(不同于大部分的UIKit和Core Animation方法)支持多線程繪制,-drawLayer:inContext:方法可以在多個(gè)線程中同時(shí)地并發(fā)調(diào)用,所以請(qǐng)小心謹(jǐn)慎地確保你在這個(gè)方法中實(shí)現(xiàn)的繪制代碼是線程安全的。
Retina小圖
你也許已經(jīng)注意到了這些小圖并不是以Retina的分辨率顯示的。為了以屏幕的原生分辨率來(lái)渲染CATiledLayer,我們需要設(shè)置圖層的contentsScale來(lái)匹配UIScreen的scale屬性:
tileLayer.contentsScale = [UIScreen mainScreen].scale;
有趣的是,tileSize是以像素為單位,而不是點(diǎn),所以增大了contentsScale就自動(dòng)有了默認(rèn)的小圖尺寸(現(xiàn)在它是128*128的點(diǎn)而不是256*256).所以,我們不需要手工更新小圖的尺寸或是在Retina分辨率下指定一個(gè)不同的小圖。我們需要做的是適應(yīng)小圖渲染代碼以對(duì)應(yīng)安排scale的變化,然而:
//determine tile coordinate CGRect bounds = CGContextGetClipBoundingBox(ctx); CGFloat scale = [UIScreen mainScreen].scale; NSInteger x = floor(bounds.origin.x / layer.tileSize.width * scale); NSInteger y = floor(bounds.origin.y / layer.tileSize.height * scale);通過(guò)這個(gè)方法糾正scale
也意味著我們的雪人圖將以一半的大小渲染在Retina設(shè)備上(總尺寸是1024*1024,而不是2048*2048)。這個(gè)通常都不會(huì)影響到用CATiledLayer
正常顯示的圖片類型(比如照片和地圖,他們?cè)谠O(shè)計(jì)上就是要支持放大縮小,能夠在不同的縮放條件下顯示),但是也需要在心里明白。
6.8 CAEmitterLayer
在iOS 5中,蘋果引入了一個(gè)新的CALayer子類叫做CAEmitterLayer。CAEmitterLayer是一個(gè)高性能的粒子引擎,被用來(lái)創(chuàng)建實(shí)時(shí)例子動(dòng)畫如:煙霧,火,雨等等這些效果。
CAEmitterLayer看上去像是許多CAEmitterCell的容器,這些CAEmitierCell定義了一個(gè)例子效果。你將會(huì)為不同的例子效果定義一個(gè)或多個(gè)CAEmitterCell作為模版,同時(shí)CAEmitterLayer負(fù)責(zé)基于這些模版實(shí)例化一個(gè)粒子流。一個(gè)CAEmitterCell類似于一個(gè)CALayer:它有一個(gè)contents屬性可以定義為一個(gè)CGImage,另外還有一些可設(shè)置屬性控制著表現(xiàn)和行為。我們不會(huì)對(duì)這些屬性逐一進(jìn)行詳細(xì)的描述,你們可以在CAEmitterCell類的頭文件中找到。
我們來(lái)舉個(gè)例子。我們將利用在一圓中發(fā)射不同速度和透明度的粒子創(chuàng)建一個(gè)火爆炸的效果。清單6.13包含了生成爆炸的代碼。圖6.13是運(yùn)行結(jié)果
清單6.13 用CAEmitterLayer創(chuàng)建爆炸效果
圖6.13 火焰爆炸效果
CAEMitterCell的屬性基本上可以分為三種:
- 這種粒子的某一屬性的初始值。比如,color屬性指定了一個(gè)可以混合圖片內(nèi)容顏色的混合色。在示例中,我們將它設(shè)置為桔色。
- 例子某一屬性的變化范圍。比如emissionRange屬性的值是2π,這意味著例子可以從360度任意位置反射出來(lái)。如果指定一個(gè)小一些的值,就可以創(chuàng)造出一個(gè)圓錐形
- 指定值在時(shí)間線上的變化。比如,在示例中,我們將alphaSpeed設(shè)置為-0.4,就是說(shuō)例子的透明度每過(guò)一秒就是減少0.4,這樣就有發(fā)射出去之后逐漸小時(shí)的效果。
CAEmitterLayer的屬性它自己控制著整個(gè)例子系統(tǒng)的位置和形狀。一些屬性比如birthRate,lifetime和celocity,這些屬性在CAEmitterCell中也有。這些屬性會(huì)以相乘的方式作用在一起,這樣你就可以用一個(gè)值來(lái)加速或者擴(kuò)大整個(gè)例子系統(tǒng)。其他值得提到的屬性有以下這些:
- preservesDepth,是否將3D例子系統(tǒng)平面化到一個(gè)圖層(默認(rèn)值)或者可以在3D空間中混合其他的圖層
- renderMode,控制著在視覺(jué)上粒子圖片是如何混合的。你可能已經(jīng)注意到了示例中我們把它設(shè)置為kCAEmitterLayerAdditive,它實(shí)現(xiàn)了這樣一個(gè)效果:合并例子重疊部分的亮度使得看上去更亮。如果我們把它設(shè)置為默認(rèn)的kCAEmitterLayerUnordered,效果就沒(méi)那么好看了(見(jiàn)圖6.14).
6.8 CAEAGLLayer
當(dāng)iOS要處理高性能圖形繪制,必要時(shí)就是OpenGL。應(yīng)該說(shuō)它應(yīng)該是最后的殺手锏,至少對(duì)于非游戲的應(yīng)用來(lái)說(shuō)是的。因?yàn)橄啾菴ore Animation和UIkit框架,它不可思議地復(fù)雜。
OpenGL提供了Core Animation的基礎(chǔ),它是底層的C接口,直接和iPhone,iPad的硬件通信,極少地抽象出來(lái)的方法。OpenGL沒(méi)有對(duì)象或是圖層的繼承概念。它只是簡(jiǎn)單地處理三角形。OpenGL中所有東西都是3D空間中有顏色和紋理的三角形。用起來(lái)非常復(fù)雜和強(qiáng)大,但是用OpenGL繪制iOS用戶界面就需要很多很多的工作了。
為了能夠以高性能使用Core Animation,你需要判斷你需要繪制哪種內(nèi)容(矢量圖形,例子,文本,等等),但后選擇合適的圖層去呈現(xiàn)這些內(nèi)容,Core Animation中只有一些類型的內(nèi)容是被高度優(yōu)化的;所以如果你想繪制的東西并不能找到標(biāo)準(zhǔn)的圖層類,想要得到高性能就比較費(fèi)事情了。
因?yàn)镺penGL根本不會(huì)對(duì)你的內(nèi)容進(jìn)行假設(shè),它能夠繪制得相當(dāng)快。利用OpenGL,你可以繪制任何你知道必要的集合信息和形狀邏輯的內(nèi)容。所以很多游戲都喜歡用OpenGL(這些情況下,Core Animation的限制就明顯了:它優(yōu)化過(guò)的內(nèi)容類型并不一定能滿足需求),但是這樣依賴,方便的高度抽象接口就沒(méi)了。
在iOS 5中,蘋果引入了一個(gè)新的框架叫做GLKit,它去掉了一些設(shè)置OpenGL的復(fù)雜性,提供了一個(gè)叫做CLKView的UIView的子類,幫你處理大部分的設(shè)置和繪制工作。前提是各種各樣的OpenGL繪圖緩沖的底層可配置項(xiàng)仍然需要你用CAEAGLLayer完成,它是CALayer的一個(gè)子類,用來(lái)顯示任意的OpenGL圖形。
大部分情況下你都不需要手動(dòng)設(shè)置CAEAGLLayer(假設(shè)用GLKView),過(guò)去的日子就不要再提了。特別的,我們將設(shè)置一個(gè)OpenGL ES 2.0的上下文,它是現(xiàn)代的iOS設(shè)備的標(biāo)準(zhǔn)做法。
盡管不需要GLKit也可以做到這一切,但是GLKit囊括了很多額外的工作,比如設(shè)置頂點(diǎn)和片段著色器,這些都以類C語(yǔ)言叫做GLSL自包含在程序中,同時(shí)在運(yùn)行時(shí)載入到圖形硬件中。編寫GLSL代碼和設(shè)置EAGLayer沒(méi)有什么關(guān)系,所以我們將用GLKBaseEffect類將著色邏輯抽象出來(lái)。其他的事情,我們還是會(huì)有以往的方式。
在開(kāi)始之前,你需要將GLKit和OpenGLES框架加入到你的項(xiàng)目中,然后就可以實(shí)現(xiàn)清單6.14中的代碼,里面是設(shè)置一個(gè)GAEAGLLayer的最少工作,它使用了OpenGL ES 2.0 的繪圖上下文,并渲染了一個(gè)有色三角(見(jiàn)圖6.15).
清單6.14 用CAEAGLLayer繪制一個(gè)三角形
#import "ViewController.h" #import #import @interface ViewController ()@property (nonatomic, weak) IBOutlet UIView *glView; @property (nonatomic, strong) EAGLContext *glContext; @property (nonatomic, strong) CAEAGLLayer *glLayer; @property (nonatomic, assign) GLuint framebuffer; @property (nonatomic, assign) GLuint colorRenderbuffer; @property (nonatomic, assign) GLint framebufferWidth; @property (nonatomic, assign) GLint framebufferHeight; @property (nonatomic, strong) GLKBaseEffect *effect;  @end@implementation ViewController- (void)setUpBuffers {//set up frame bufferglGenFramebuffers(1, &_framebuffer);glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);//set up color render bufferglGenRenderbuffers(1, &_colorRenderbuffer);glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer);[self.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.glLayer];glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_framebufferWidth);glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_framebufferHeight);//check successif (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {NSLog(@"Failed to make complete framebuffer object: %i", glCheckFramebufferStatus(GL_FRAMEBUFFER));} }- (void)tearDownBuffers {if (_framebuffer) {//delete framebufferglDeleteFramebuffers(1, &_framebuffer);_framebuffer = 0;}if (_colorRenderbuffer) {//delete color render bufferglDeleteRenderbuffers(1, &_colorRenderbuffer);_colorRenderbuffer = 0;} }- (void)drawFrame {//bind framebuffer & set viewportglBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);glViewport(0, 0, _framebufferWidth, _framebufferHeight);//bind shader program[self.effect prepareToDraw];//clear the screenglClear(GL_COLOR_BUFFER_BIT); glClearColor(0.0, 0.0, 0.0, 1.0);//set up verticesGLfloat vertices[] = {-0.5f, -0.5f, -1.0f, 0.0f, 0.5f, -1.0f, 0.5f, -0.5f, -1.0f,};//set up colorsGLfloat colors[] = {0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f,};//draw triangleglEnableVertexAttribArray(GLKVertexAttribPosition);glEnableVertexAttribArray(GLKVertexAttribColor);glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, vertices);glVertexAttribPointer(GLKVertexAttribColor,4, GL_FLOAT, GL_FALSE, 0, colors);glDrawArrays(GL_TRIANGLES, 0, 3);//present render bufferglBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);[self.glContext presentRenderbuffer:GL_RENDERBUFFER]; }- (void)viewDidLoad {[super viewDidLoad];//set up contextself.glContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];[EAGLContext setCurrentContext:self.glContext];//set up layerself.glLayer = [CAEAGLLayer layer];self.glLayer.frame = self.glView.bounds;[self.glView.layer addSublayer:self.glLayer];self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@NO, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};//set up base effectself.effect = [[GLKBaseEffect alloc] init];//set up buffers[self setUpBuffers];//draw frame[self drawFrame]; }- (void)viewDidUnload {[self tearDownBuffers];[super viewDidUnload]; }- (void)dealloc {[self tearDownBuffers];[EAGLContext setCurrentContext:nil]; } @end
在一個(gè)真正的OpenGL應(yīng)用中,我們可能會(huì)用NSTimer或CADisplayLink周期性地每秒鐘調(diào)用-drawRrame方法60次,同時(shí)會(huì)將幾何圖形生成和繪制分開(kāi)以便不會(huì)每次都重新生成三角形的頂點(diǎn)(這樣也可以讓我們繪制其他的一些東西而不是一個(gè)三角形而已),不過(guò)上面這個(gè)例子已經(jīng)足夠演示了繪圖原則了。
6.10 AVPlayerLayer
最后一個(gè)圖層類型是AVPlayerLayer。盡管它不是Core Animation框架的一部分(AV前綴看上去像),AVPlayerLayer是有別的框架(AVFoundation)提供的,它和Core Animation緊密地結(jié)合在一起,提供了一個(gè)CALayer子類來(lái)顯示自定義的內(nèi)容類型。
AVPlayerLayer是用來(lái)在iOS上播放視頻的。他是高級(jí)接口例如MPMoivePlayer的底層實(shí)現(xiàn),提供了顯示視頻的底層控制。AVPlayerLayer的使用相當(dāng)簡(jiǎn)單:你可以用+playerLayerWithPlayer:方法創(chuàng)建一個(gè)已經(jīng)綁定了視頻播放器的圖層,或者你可以先創(chuàng)建一個(gè)圖層,然后用player屬性綁定一個(gè)AVPlayer實(shí)例。
在我們開(kāi)始之前,我們需要添加AVFoundation到我們的項(xiàng)目中。然后,清單6.15創(chuàng)建了一個(gè)簡(jiǎn)單的電影播放器,圖6.16是代碼運(yùn)行結(jié)果。
清單6.15 用AVPlayerLayer播放視頻
#import "ViewController.h" #import #import @interface ViewController ()@property (nonatomic, weak) IBOutlet UIView *containerView; @end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];//get video URLNSURL *URL = [[NSBundle mainBundle] URLForResource:@"Ship" withExtension:@"mp4"];//create player and player layerAVPlayer *player = [AVPlayer playerWithURL:URL];AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];//set player layer frame and attach it to our viewplayerLayer.frame = self.containerView.bounds;[self.containerView.layer addSublayer:playerLayer];//play the video[player play]; } @end我們用代碼創(chuàng)建了一個(gè)AVPlayerLayer,但是我們?nèi)匀话阉砑拥搅艘粋€(gè)容器視圖中,而不是直接在controller中的主視圖上添加。這樣其實(shí)是為了可以使用自動(dòng)布局限制使得圖層在最中間;否則,一旦設(shè)備被旋轉(zhuǎn)了我們就要手動(dòng)重新放置位置,因?yàn)镃ore Animation并不支持自動(dòng)大小和自動(dòng)布局(見(jiàn)第三章『圖層幾何學(xué)』)。
當(dāng)然,因?yàn)锳VPlayerLayer是CALayer的子類,它繼承了父類的所有特性。我們并不會(huì)受限于要在一個(gè)矩形中播放視頻;清單6.16演示了在3D,圓角,有色邊框,蒙板,陰影等效果(見(jiàn)圖6.17).
清單6.16 給視頻增加變換,邊框和圓角
- (void)viewDidLoad {...//set player layer frame and attach it to our viewplayerLayer.frame = self.containerView.bounds;[self.containerView.layer addSublayer:playerLayer];//transform layerCATransform3D transform = CATransform3DIdentity;transform.m34 = -1.0 / 500.0;transform = CATransform3DRotate(transform, M_PI_4, 1, 1, 0);playerLayer.transform = transform;//add rounded corners and borderplayerLayer.masksToBounds = YES;playerLayer.cornerRadius = 20.0;playerLayer.borderColor = [UIColor redColor].CGColor;playerLayer.borderWidth = 5.0;//play the video[player play]; }6.11 總結(jié)
這一章我們簡(jiǎn)要概述了一些專用圖層以及用他們實(shí)現(xiàn)的一些效果,我們只是了解到這些圖層的皮毛,像CATiledLayer和CAEMitterLayer這些類可以單獨(dú)寫一章的。但是,重點(diǎn)是記住CALayer是用處很大的,而且它并沒(méi)有為所有可能的場(chǎng)景進(jìn)行優(yōu)化。為了獲得Core Animation最好的性能,你需要為你的工作選對(duì)正確的工具,希望你能夠挖掘這些不同的CALayer子類的功能。 這一章我們通過(guò)CAEmitterLayer和AVPlayerLayer類簡(jiǎn)單地接觸到了一些動(dòng)畫,在第二章,我們將繼續(xù)深入研究動(dòng)畫,就從隱式動(dòng)畫開(kāi)始。
總結(jié)
以上是生活随笔為你收集整理的CoreAnimation的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 51单片机 Proteus仿真 四路倒计
- 下一篇: 验证JDK是否安装成功