CoreAnimation
一、圖層的樹狀結構
本節轉載自ios核心動畫高級技巧
巨妖有圖層,洋蔥也有圖層,你有嗎?我們都有圖層 – 史萊克
Core Animation其實是一個令人誤解的命名。你可能認為它只是用來做動畫的,但實際上它是從一個叫做Layer Kit這么一個不怎么和動畫有關的名字演變而來,所以做動畫這只是Core Animation特性的冰山一角。
Core Animation是一個復合引擎,它的職責就是盡可能快地組合屏幕上不同的可視內容,這個內容是被分解成獨立的圖層,存儲在一個叫做圖層樹的體系之中。于是這個樹形成了UIKit以及在iOS應用程序當中你所能在屏幕上看見的一切的基礎。
在我們討論動畫之前,我們將從圖層樹開始,涉及一下Core Animation的靜態組合以及布局特性。
1.1 圖層與視圖
如果你曾經在iOS或者Mac OS平臺上寫過應用程序,你可能會對視圖的概念比較熟悉。一個視圖就是在屏幕上顯示的一個矩形塊(比如圖片,文字或者視頻),它能夠攔截類似于鼠標點擊或者觸摸手勢等用戶輸入。視圖在層級關系中可以互相嵌套,一個視圖可以管理它的所有子視圖的位置。圖1.1顯示了一種典型的視圖層級關系
在iOS當中,所有的視圖都從一個叫做UIVIew的基類派生而來,UIView可以處理觸摸事件,可以支持基于Core Graphics繪圖,可以做仿射變換(例如旋轉或者縮放),或者簡單的類似于滑動或者漸變的動畫。
CALayer
CALayer類在概念上和UIView類似,同樣也是一些被層級關系樹管理的矩形塊,同樣也可以包含一些內容(像圖片,文本或者背景色),管理子圖層的位置。它們有一些方法和屬性用來做動畫和變換。和UIView最大的不同是CALayer不處理用戶的交互。
CALayer并不清楚具體的響應鏈(iOS通過視圖層級關系用來傳送觸摸事件的機制),于是它并不能夠響應事件,即使它提供了一些方法來判斷是否一個觸點在圖層的范圍之內(具體見第三章,“圖層的幾何學”)
平行的層級關系
每一個UIview都有一個CALayer實例的圖層屬性,也就是所謂的backing layer,視圖的職責就是創建并管理這個圖層,以確保當子視圖在層級關系中添加或者被移除的時候,他們關聯的圖層也同樣對應在層級關系樹當中有相同的操作(見圖1.2)。
實際上這些背后關聯的圖層才是真正用來在屏幕上顯示和做動畫,UIView僅僅是對它的一個封裝,提供了一些iOS類似于處理觸摸的具體功能,以及Core Animation底層方法的高級接口。
但是為什么iOS要基于UIView和CALayer提供兩個平行的層級關系呢?為什么不用一個簡單的層級來處理所有事情呢?原因在于要做職責分離,這樣也能避免很多重復代碼。在iOS和Mac OS兩個平臺上,事件和用戶交互有很多地方的不同,基于多點觸控的用戶界面和基于鼠標鍵盤有著本質的區別,這就是為什么iOS有UIKit和UIView,但是Mac OS有AppKit和NSView的原因。他們功能上很相似,但是在實現上有著顯著的區別。
繪圖,布局和動畫,相比之下就是類似Mac筆記本和桌面系列一樣應用于iPhone和iPad觸屏的概念。把這種功能的邏輯分開并應用到獨立的Core Animation框架,蘋果就能夠在iOS和Mac OS之間共享代碼,使得對蘋果自己的OS開發團隊和第三方開發者去開發兩個平臺的應用更加便捷。
實際上,這里并不是兩個層級關系,而是四個,每一個都扮演不同的角色,除了視圖層級和圖層樹之外,還存在呈現樹和渲染樹,將在第七章“隱式動畫”和第十二章“性能調優”分別討論。
1.2 圖層的能力
如果說CALayer是UIView內部實現細節,那我們為什么要全面地了解它呢?蘋果當然為我們提供了優美簡潔的UIView接口,那么我們是否就沒必要直接去處理Core Animation的細節了呢?
某種意義上說的確是這樣,對一些簡單的需求來說,我們確實沒必要處理CALayer,因為蘋果已經通過UIView的高級API間接地使得動畫變得很簡單。
但是這種簡單會不可避免地帶來一些靈活上的缺陷。如果你略微想在底層做一些改變,或者使用一些蘋果沒有在UIView上實現的接口功能,這時除了介入Core Animation底層之外別無選擇。
我們已經證實了圖層不能像視圖那樣處理觸摸事件,那么他能做哪些視圖不能做的呢?這里有一些UIView沒有暴露出來的CALayer的功能:
- 陰影,圓角,帶顏色的邊框
- 3D變換
- 非矩形范圍
- 透明遮罩
- 多級非線性動畫
我們將會在后續章節中探索這些功能,首先我們要關注一下在應用程序當中CALayer是怎樣被利用起來的。
1.3 使用圖層
首先我們來創建一個簡單的項目,來操縱一些layer的屬性。打開Xcode,使用Single View Application模板創建一個工程。
在屏幕中央創建一個小視圖(大約200 X 200的尺寸),當然你可以手工編碼,或者使用Interface Builder(隨你方便)。確保你的視圖控制器要添加一個視圖的屬性以便可以直接訪問它。我們把它稱作layerView。
運行項目,應該能在淺灰色屏幕背景中看見一個白色方塊(圖1.3),如果沒看見,可能需要調整一下背景window或者view的顏色
這并沒有什么令人激動的地方,我們來添加一個色塊,在白色方塊中間添加一個小的藍色塊。
我們當然可以簡單地在已經存在的UIView上添加一個子視圖(隨意用代碼或者IB),但這不能真正學到任何關于圖層的東西。
于是我們來創建一個CALayer,并且把它作為我們視圖相關圖層的子圖層。盡管UIView類的接口中暴露了圖層屬性,但是標準的Xcode項目模板并沒有包含Core Animation相關頭文件。所以如果我們不給項目添加合適的庫,是不能夠使用任何圖層相關的方法或者訪問它的屬性。所以首先需要添加QuartzCore框架到Build Phases標簽(圖1.4),然后在vc的.m文件中引入庫。
之后就可以在代碼中直接引用CALayer的屬性和方法。在清單1.1中,我們用創建了一個CALayer,設置了它的backgroundColor屬性,然后添加到layerView背后相關圖層的子圖層(這段代碼的前提是通過IB創建了layerView并做好了連接),圖1.5顯示了結果。
清單1.1 給視圖添加一個藍色子圖層
#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一個視圖只有一個相關聯的圖層(自動創建),同時它也可以支持添加無數多個子圖層,從清單1.1可以看出,你可以顯示創建一個單獨的圖層,并且把它直接添加到視圖關聯圖層的子圖層。盡管可以這樣添加圖層,但往往我們只是見簡單地處理視圖,他們關聯的圖層并不需要額外地手動添加子圖層。
在Mac OS平臺,10.8版本之前,一個顯著的性能缺陷就是由于用了視圖層級而不是單獨在一個視圖內使用CALayer樹狀層級。但是在iOS平臺,使用輕量級的UIView類并沒有顯著的性能影響(當然在Mac OS 10.8之后,NSView的性能同樣也得到很大程度的提高)。
使用圖層關聯的視圖而不是CALayer的好處在于,你能在使用所有CALayer底層特性的同時,也可以使用UIView的高級API(比如自動排版,布局和事件處理)。
然而,當滿足以下條件的時候,你可能更需要使用CALayer而不是UIView:
- 開發同時可以在Mac OS上運行的跨平臺應用
- 使用多種CALayer的子類(見第六章,“特殊的圖層“),并且不想創建額外的UIView去包封裝它們所有
- 做一些對性能特別挑剔的工作,比如對UIView一些可忽略不計的操作都會引起顯著的不同(盡管如此,你可能會直接想使用OpenGL繪圖)
但是這些例子都很少見,總的來說,處理視圖會比單獨處理圖層更加方便。
1.4 總結
這一章闡述了圖層的樹狀結構,說明了如何在iOS中由UIView的層級關系形成的一種平行的CALayer層級關系,在后面的實驗中,我們創建了自己的CALayer,并把它添加到圖層樹中。
在第二章,“圖層關聯的圖片”,我們將要研究一下CALayer關聯的圖片,以及Core Animation提供的操作顯示的一些特性。
二、寄宿圖
本節轉載自ios核心動畫高級技巧
圖片勝過千言萬語,界面抵得上千圖片 ——Ben Shneiderman
我們在第一章『圖層樹』中介紹了CALayer類并創建了一個簡單的有藍色背景的圖層。背景顏色還好啦,但是如果它僅僅是展現了一個單調的顏色未免也太無聊了。事實上CALayer類能夠包含一張你喜歡的圖片,這一章節我們將來探索CALayer的寄宿圖(即圖層中包含的圖)。
2.1 contents屬性
CALayer 有一個屬性叫做contents,這個屬性的類型被定義為id,意味著它可以是任何類型的對象。在這種情況下,你可以給contents屬性賦任何值,你的app仍然能夠編譯通過。但是,在實踐中,如果你給contents賦的不是CGImage,那么你得到的圖層將是空白的。
contents這個奇怪的表現是由Mac OS的歷史原因造成的。它之所以被定義為id類型,是因為在Mac OS系統上,這個屬性對CGImage和NSImage類型的值都起作用。如果你試圖在iOS平臺上將UIImage的值賦給它,只能得到一個空白的圖層。一些初識Core Animation的iOS開發者可能會對這個感到困惑。
頭疼的不僅僅是我們剛才提到的這個問題。事實上,你真正要賦值的類型應該是CGImageRef,它是一個指向CGImage結構的指針。UIImage有一個CGImage屬性,它返回一個”CGImageRef”,如果你想把這個值直接賦值給CALayer的contents,那你將會得到一個編譯錯誤。因為CGImageRef并不是一個真正的Cocoa對象,而是一個Core Foundation類型。
盡管Core Foundation類型跟Cocoa對象在運行時貌似很像(被稱作toll-free bridging),他們并不是類型兼容的,不過你可以通過bridged關鍵字轉換。如果要給圖層的寄宿圖賦值,你可以按照以下這個方法:
layer.contents = (__bridge id)image.CGImage;
如果你沒有使用ARC(自動引用計數),你就不需要__bridge這部分。但是,你干嘛不用ARC?!
讓我們來繼續修改我們在第一章新建的工程,以便能夠展示一張圖片而不僅僅是一個背景色。我們已經用代碼的方式建立一個圖層,那我們就不需要額外的圖層了。那么我們就直接把layerView的宿主圖層的contents屬性設置成圖片。
清單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我們用這些簡單的代碼做了一件很有趣的事情:我們利用CALayer在一個普通的UIView中顯示了一張圖片。這不是一個UIImageView,它不是我們通常用來展示圖片的方法。通過直接操作圖層,我們使用了一些新的函數,使得UIView更加有趣了。
contentGravity
你可能已經注意到了我們的雪人看起來有點。。。胖 ==! 我們加載的圖片并不剛好是一個方的,為了適應這個視圖,它有一點點被拉伸了。在使用UIImageView的時候遇到過同樣的問題,解決方法就是把contentMode屬性設置成更合適的值,像這樣:
view.contentMode = UIViewContentModeScaleAspectFit;這個方法基本和我們遇到的情況的解決方法已經接近了(你可以試一下 :) ),不過UIView大多數視覺相關的屬性比如contentMode,對這些屬性的操作其實是對對應圖層的操作。
CALayer與contentMode對應的屬性叫做contentsGravity,但是它是一個NSString類型,而不是像對應的UIKit部分,那里面的值是枚舉。contentsGravity可選的常量值有以下一些:
- kCAGravityCenter
- kCAGravityTop
- kCAGravityBottom
- kCAGravityLeft
- kCAGravityRight
- kCAGravityTopLeft
- kCAGravityTopRight
- kCAGravityBottomLeft
- kCAGravityBottomRight
- kCAGravityResize
- kCAGravityResizeAspect
- kCAGravityResizeAspectFill
和cotentMode一樣,contentsGravity的目的是為了決定內容在圖層的邊界中怎么對齊,我們將使用kCAGravityResizeAspect,它的效果等同于UIViewContentModeScaleAspectFit, 同時它還能在圖層中等比例拉伸以適應圖層的邊界。圖2.2 可以看到結果:
self.layerView.layer.contentsGravity = kCAGravityResizeAspect;contentsScale
contentsScale屬性定義了寄宿圖的像素尺寸和視圖大小的比例,默認情況下它是一個值為1.0的浮點數。
contentsScale的目的并不是那么明顯。它并不是總會對屏幕上的寄宿圖有影響。如果你嘗試對我們的例子設置不同的值,你就會發現根本沒任何影響。因為contents由于設置了contentsGravity屬性,所以它已經被拉伸以適應圖層的邊界。
如果你只是單純地想放大圖層的contents圖片,你可以通過使用圖層的transform和affineTransform屬性來達到這個目的(見第五章『Transforms』,里面對此有解釋),這(指放大)也不是contengsScale的目的所在.
contentsScale屬性其實屬于支持高分辨率(又稱Hi-DPI或Retina)屏幕機制的一部分。它用來判斷在繪制圖層的時候應該為寄宿圖創建的空間大小,和需要顯示的圖片的拉伸度(假設并沒有設置contentsGravity屬性)。UIView有一個類似功能但是非常少用到的contentScaleFactor屬性。
如果contentsScale設置為1.0,將會以每個點1個像素繪制圖片,如果設置為2.0,則會以每個點2個像素繪制圖片,這就是我們熟知的Retina屏幕。(如果你對像素和點的概念不是很清楚的話,這個章節的后面部分將會對此做出解釋)。
這并不會對我們在使用kCAGravityResizeAspect時產生任何影響,因為它就是拉伸圖片以適應圖層而已,根本不會考慮到分辨率問題。但是如果我們把contentsGravity設置為kCAGravityCenter(這個值并不會拉伸圖片),那將會有很明顯的變化(如圖2.3)
如你所見,我們的雪人不僅有點大還有點像素的顆粒感。那是因為和UIImage不同,CGImage沒有拉伸的概念。當我們使用UIImage類去讀取我們的雪人圖片的時候,他讀取了高質量的Retina版本的圖片。但是當我們用CGImage來設置我們的圖層的內容時,拉伸這個因素在轉換的時候就丟失了。不過我們可以通過手動設置contentsScale來修復這個問題(如2.2清單),圖2.4是結果
@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當用代碼的方式來處理寄宿圖的時候,一定要記住要手動的設置圖層的contentsScale屬性,否則,你的圖片在Retina設備上就顯示得不正確啦。代碼如下:
layer.contentsScale = [UIScreen mainScreen].scale;maskToBounds
現在我們的雪人總算是顯示了正確的大小,不過你也許已經發現了另外一些事情:他超出了視圖的邊界。默認情況下,UIView仍然會繪制超過邊界的內容或是子視圖,在CALayer下也是這樣的。
UIView有一個叫做clipsToBounds的屬性可以用來決定是否顯示超出邊界的內容,CALayer對應的屬性叫做masksToBounds,把它設置為YES,雪人就在邊界里啦~(如圖2.5)
contentsRect
CALayer的contentsRect屬性允許我們在圖層邊框里顯示寄宿圖的一個子域。這涉及到圖片是如何顯示和拉伸的,所以要比contentsGravity靈活多了
和bounds,frame不同,contentsRect不是按點來計算的,它使用了單位坐標,單位坐標指定在0到1之間,是一個相對值(像素和點就是絕對值)。所以他們是相對與寄宿圖的尺寸的。iOS使用了以下的坐標系統:
- 點 —— 在iOS和Mac OS中最常見的坐標體系。點就像是虛擬的像素,也被稱作邏輯像素。在標準設備上,一個點就是一個像素,但是在Retina設備上,一個點等于2*2個像素。iOS用點作為屏幕的坐標測算體系就是為了在Retina設備和普通設備上能有一致的視覺效果。
- 像素 —— 物理像素坐標并不會用來屏幕布局,但是仍然與圖片有相對關系。UIImage是一個屏幕分辨率解決方案,所以指定點來度量大小。但是一些底層的圖片表示如CGImage就會使用像素,所以你要清楚在Retina設備和普通設備上,他們表現出來了不同的大小。
- 單位 —— 對于與圖片大小或是圖層邊界相關的顯示,單位坐標是一個方便的度量方式, 當大小改變的時候,也不需要再次調整。單位坐標在OpenGL這種紋理坐標系統中用得很多,Core Animation中也用到了單位坐標。
默認的contentsRect是{0, 0, 1, 1},這意味著整個寄宿圖默認都是可見的,如果我們指定一個小一點的矩形,圖片就會被裁剪(如圖2.6)
事實上給contentsRect設置一個負數的原點或是大于{1, 1}的尺寸也是可以的。這種情況下,最外面的像素會被拉伸以填充剩下的區域。
contentsRect在app中最有趣的地方在于一個叫做image sprites(圖片拼合)的用法。如果你有游戲編程的經驗,那么你一定對圖片拼合的概念很熟悉,圖片能夠在屏幕上獨立地變更位置。拋開游戲編程不談,這個技術常用來指代載入拼合的圖片,跟移動圖片一點關系也沒有。
典型地,圖片拼合后可以打包整合到一張大圖上一次性載入。相比多次載入不同的圖片,這樣做能夠帶來很多方面的好處:內存使用,載入時間,渲染性能等等。
2D游戲引擎入Cocos2D使用了拼合技術,它使用OpenGL來顯示圖片。不過我們可以使用拼合在一個普通的UIKit應用中,對!就是使用contentsRect
首先,我們需要一個拼合后的圖表 —— 一個包含小一些的拼合圖的大圖片。如圖2.7所示:
接下來,我們要在app中載入并顯示這些拼合圖。規則很簡單:像平常一樣載入我們的大圖,然后把它賦值給四個獨立的圖層的contents,然后設置每個圖層的contentsRect來去掉我們不想顯示的部分。
我們的工程中需要一些額外的視圖。(為了避免太多代碼。我們將使用Interface Builder來拜訪他們的位置,如果你愿意還是可以用代碼的方式來實現的)。清單2.3有需要的代碼,圖2.8展示了結果
@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提供了一個整潔的載入方式,還有效地提高了載入性能(單張大圖比多張小圖載入地更快),但是如果有手動安排的話,他們還是有一些不方便的,如果你需要在一個已經創建好的品和圖上做一些尺寸上的修改或者其他變動,無疑是比較麻煩的。
Mac上有一些商業軟件可以為你自動拼合圖片,這些工具自動生成一個包含拼合后的坐標的XML或者plist文件,拼合圖片的使用大大簡化。這個文件可以和圖片一同載入,并給每個拼合的圖層設置contentsRect,這樣開發者就不用手動寫代碼來擺放位置了。
這些文件通常在OpenGL游戲中使用,不過呢,你要是有興趣在一些常見的app中使用拼合技術,那么一個叫做LayerSprites的開源庫,它能夠讀取Cocos2D格式中的拼合圖并在普通的Core Animation層中顯示出來。
contentsCenter
本章我們介紹的最后一個和內容有關的屬性是contentsCenter,看名字你可能會以為它可能跟圖片的位置有關,不過這名字著實誤導了你。contentsCenter其實是一個CGRect,它定義了一個固定的邊框和一個在圖層上可拉伸的區域。 改變contentsCenter的值并不會影響到寄宿圖的顯示,除非這個圖層的大小改變了,你才看得到效果。
默認情況下,contentsCenter是{0, 0, 1, 1},這意味著如果大小(由conttensGravity決定)改變了,那么寄宿圖將會均勻地拉伸開。但是如果我們增加原點的值并減小尺寸。我們會在圖片的周圍創造一個邊框。圖2.9展示了contentsCenter設置為{0.25, 0.25, 0.5, 0.5}的效果。
這意味著我們可以隨意重設尺寸,邊框仍然會是連續的。他工作起來的效果和UIImage里的-resizableImageWithCapInsets: 方法效果非常類似,只是它可以運用到任何寄宿圖,甚至包括在Core Graphics運行時繪制的圖形(本章稍后會講到)。
清單2.4 演示了如何編寫這些可拉伸視圖。不過,contentsCenter的另一個很酷的特性就是,它可以在Interface Builder里面配置,根本不用寫代碼。如圖2.11
清單2.4 用contentsCenter設置可拉伸視圖
2.2 Custom Drawing
給contents賦CGImage的值不是唯一的設置寄宿圖的方法。我們也可以直接用Core Graphics直接繪制寄宿圖。能夠通過繼承UIView并實現-drawRect:方法來自定義繪制。
-drawRect:方法沒有默認的實現,因為對UIView來說,寄宿圖并不是必須的,它不在意那到底是單調的顏色還是有一個圖片的實例。如果UIView檢測到-drawRect:方法被調用了,它就會為視圖分配一個寄宿圖,這個寄宿圖的像素尺寸等于視圖大小乘以 contentsScale的值。
如果你不需要寄宿圖,那就不要創建這個方法了,這會造成CPU資源和內存的浪費,這也是為什么蘋果建議:如果沒有自定義繪制的任務就不要在子類中寫一個空的-drawRect:方法。
當視圖在屏幕上出現的時候-drawRect:方法就會被自動調用。-drawRect:方法里面的代碼利用Core Graphics去繪制一個寄宿圖,然后內容就會被緩存起來直到它需要被更新(通常是因為開發者調用了-setNeedsDisplay方法,盡管影響到表現效果的屬性值被更改時,一些視圖類型會被自動重繪,如bounds屬性)。雖然-drawRect:方法是一個UIView方法,事實上都是底層的CALayer安排了重繪工作和保存了因此產生的圖片。
CALayer有一個可選的delegate屬性,實現了CALayerDelegate協議,當CALayer需要一個內容特定的信息時,就會從協議中請求。CALayerDelegate是一個非正式協議,其實就是說沒有CALayerDelegate @protocol可以讓你在類里面引用啦。你只需要調用你想調用的方法,CALayer會幫你做剩下的。(delegate屬性被聲明為id類型,所有的代理方法都是可選的)。
當需要被重繪時,CALayer會請求它的代理給他一個寄宿圖來顯示。它通過調用下面這個方法做到的:
(void)displayLayer:(CALayerCALayer *)layer;趁著這個機會,如果代理想直接設置contents屬性的話,它就可以這么做,不然沒有別的方法可以調用了。如果代理不實現-displayLayer:方法,CALayer就會轉而嘗試調用下面這個方法:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;在調用這個方法之前,CALayer創建了一個合適尺寸的空寄宿圖(尺寸由bounds和contentsScale決定)和一個Core Graphics的繪制上下文環境,為繪制寄宿圖做準備,他作為ctx參數傳入。
讓我們來繼續第一章的項目讓它實現CALayerDelegate并做一些繪圖工作吧(見清單2.5).圖2.12是他的結果
清單2.5 實現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注意一下一些有趣的事情:
我們在blueLayer上顯式地調用了-display。不同于UIView,當圖層顯示在屏幕上時,CALayer不會自動重繪它的內容。它把重繪的決定權交給了開發者。
盡管我們沒有用masksToBounds屬性,繪制的那個圓仍然沿邊界被裁剪了。這是因為當你使用CALayerDelegate繪制寄宿圖的時候,并沒有對超出邊界外的內容提供繪制支持。
現在你理解了CALayerDelegate,并知道怎么使用它。但是除非你創建了一個單獨的圖層,你幾乎沒有機會用到CALayerDelegate協議。因為當UIView創建了它的宿主圖層時,它就會自動地把圖層的delegate設置為它自己,并提供了一個-displayLayer:的實現,那所有的問題就都沒了。
當使用寄宿了視圖的圖層的時候,你也不必實現-displayLayer:和-drawLayer:inContext:方法來繪制你的寄宿圖。通常做法是實現UIView的-drawRect:方法,UIView就會幫你做完剩下的工作,包括在需要重繪的時候調用-display方法。
2.3 總結
本章介紹了寄宿圖和一些相關的屬性。你學到了如何顯示和放置圖片, 使用拼合技術來顯示, 以及用CALayerDelegate和Core Graphics來繪制圖層內容。
在第三章,”圖層幾何學”中,我們將會探討一下圖層的幾何,觀察他們是如何放置和改變相互的尺寸的。
三、圖層幾何學
本節轉載自ios核心動畫高級技巧
不熟悉幾何學的人就不要來這里了 –柏拉圖學院入口的簽名
在第二章里面,我們介紹了圖層背后的圖片,和一些控制圖層坐標和旋轉的屬性。在這一章中,我們將要看一看圖層內部是如何根據父圖層和兄弟圖層來控制位置和尺寸的。另外我們也會涉及如何管理圖層的幾何結構,以及它是如何被自動調整和自動布局影響的。
3.1 布局
UIView有三個比較重要的布局屬性:frame,bounds和center,CALayer對應地叫做frame,bounds和position。為了能清楚區分,圖層用了“position”,視圖用了“center”,但是他們都代表同樣的值。
frame代表了圖層的外部坐標(也就是在父圖層上占據的空間),bounds是內部坐標({0, 0}通常是圖層的左上角),center和position都代表了相對于父圖層anchorPoint所在的位置。anchorPoint的屬性將會在后續介紹到,現在把它想成圖層的中心點就好了。圖3.1顯示了這些屬性是如何相互依賴的。
視圖的frame,bounds和center屬性僅僅是存取方法,當操縱視圖的frame,實際上是在改變位于視圖下方CALayer的frame,不能夠獨立于圖層之外改變視圖的frame。
對于視圖或者圖層來說,frame并不是一個非常清晰的屬性,它其實是一個虛擬屬性,是根據bounds,position和transform計算而來,所以當其中任何一個值發生改變,frame都會變化。相反,改變frame的值同樣會影響到他們當中的值
記住當對圖層做變換的時候,比如旋轉或者縮放,frame實際上代表了覆蓋在圖層旋轉之后的整個軸對齊的矩形區域,也就是說frame的寬高可能和bounds的寬高不再一致了(圖3.2)
3.2錨點
之前提到過,視圖的center屬性和圖層的position屬性都指定了anchorPoint相對于父圖層的位置。圖層的anchorPoint通過position來控制它的frame的位置,你可以認為anchorPoint是用來移動圖層的把柄。
默認來說,anchorPoint位于圖層的中點,所以圖層的將會以這個點為中心放置。anchorPoint屬性并沒有被UIView接口暴露出來,這也是視圖的position屬性被叫做“center”的原因。但是圖層的anchorPoint可以被移動,比如你可以把它置于圖層frame的左上角,于是圖層的內容將會向右下角的position方向移動(圖3.3),而不是居中了。
和第二章提到的contentsRect和contentsCenter屬性類似,anchorPoint用單位坐標來描述,也就是圖層的相對坐標,圖層左上角是{0, 0},右下角是{1, 1},因此默認坐標是{0.5, 0.5}。anchorPoint可以通過指定x和y值小于0或者大于1,使它放置在圖層范圍之外。
注意在圖3.3中,當改變了anchorPoint,position屬性保持固定的值并沒有發生改變,但是frame卻移動了。
那在什么場合需要改變anchorPoint呢?既然我們可以隨意改變圖層位置,那改變anchorPoint不會造成困惑么?為了舉例說明,我們來舉一個實用的例子,創建一個模擬鬧鐘的項目。
鐘面和鐘表由四張圖片組成(圖3.4),為了簡單說明,我們還是用傳統的方式來裝載和加載圖片,使用四個UIImageView實例(當然你也可以用正常的視圖,設置他們圖層的contents圖片)。
鬧鐘的組件通過IB來排列(圖3.5),這些圖片視圖嵌套在一個容器視圖之內,并且自動調整和自動布局都被禁用了。這是因為自動調整會影響到視圖的frame,而根據圖3.2的演示,當視圖旋轉的時候,frame是會發生改變的,這將會導致一些布局上的失靈。
我們用NSTimer來更新鬧鐘,使用視圖的transform屬性來旋轉鐘表(如果你對這個屬性不太熟悉,不要著急,我們將會在第5章“變換”當中詳細說明),具體代碼見清單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運行項目,看起來有點奇怪(圖3.6),因為鐘表的圖片在圍繞著中心旋轉,這并不是我們期待的一個支點。
你也許會認為可以在Interface Builder當中調整指針圖片的位置來解決,但其實并不能達到目的,因為如果不放在鐘面中間的話,同樣不能正確的旋轉。
也許在圖片末尾添加一個透明空間也是個解決方案,但這樣會讓圖片變大,也會消耗更多的內存,這樣并不優雅。
更好的方案是使用anchorPoint屬性,我們來在-viewDidLoad方法中添加幾行代碼來給每個鐘指針的anchorPoint做一些平移(清單3.2),圖3.7顯示了正確的結果。
清單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 坐標系
和視圖一樣,圖層在圖層樹當中也是相對于父圖層按層級關系放置,一個圖層的position依賴于它父圖層的bounds,如果父圖層發生了移動,它的所有子圖層也會跟著移動。
這樣對于放置圖層會更加方便,因為你可以通過移動根圖層來將它的子圖層作為一個整體來移動,但是有時候你需要知道一個圖層的絕對位置,或者是相對于另一個圖層的位置,而不是它當前父圖層的位置。
CALayer給不同坐標系之間的圖層轉換提供了一些工具類方法:
- (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;這些方法可以把定義在一個圖層坐標系下的點或者矩形轉換成另一個圖層坐標系下的點或者矩形.
翻轉的幾何結構
常規說來,在iOS上,一個圖層的position位于父圖層的左上角,但是在Mac OS上,通常是位于左下角。Core Animation可以通過geometryFlipped屬性來適配這兩種情況,它決定了一個圖層的坐標是否相對于父圖層垂直翻轉,是一個BOOL類型。在iOS上通過設置它為YES意味著它的子圖層將會被垂直翻轉,也就是將會沿著底部排版而不是通常的頂部(它的所有子圖層也同理,除非把它們的geometryFlipped屬性也設為YES)。
Z坐標軸
和UIView嚴格的二維坐標系不同,CALayer存在于一個三維空間當中。除了我們已經討論過的position和anchorPoint屬性之外,CALayer還有另外兩個屬性,zPosition和anchorPointZ,二者都是在Z軸上描述圖層位置的浮點類型。
注意這里并沒有更深的屬性來描述由寬和高做成的bounds了,圖層是一個完全扁平的對象,你可以把它們想象成類似于一頁二維的堅硬的紙片,用膠水粘成一個空洞,就像三維結構的折紙一樣。
zPosition屬性在大多數情況下其實并不常用。在第五章,我們將會涉及CATransform3D,你會知道如何在三維空間移動和旋轉圖層,除了做變換之外,zPosition最實用的功能就是改變圖層的顯示順序了。
通常,圖層是根據它們子圖層的sublayers出現的順序來類繪制的,這就是所謂的畫家的算法–就像一個畫家在墻上作畫–后被繪制上的圖層將會遮蓋住之前的圖層,但是通過增加圖層的zPosition,就可以把圖層向相機方向前置,于是它就在所有其他圖層的前面了(或者至少是小于它的zPosition值的圖層的前面)。
這里所謂的“相機”實際上是相對于用戶是視角,這里和iPhone背后的內置相機沒任何關系。
圖3.8顯示了在Interface Builder內的一對視圖,正如你所見,首先出現在視圖層級綠色的視圖被繪制在紅色視圖的后面。
我們希望在真實的應用中也能顯示出繪圖的順序,同樣地,如果我們提高綠色視圖的zPosition(清單3.3),我們會發現順序就反了(圖3.9)。其實并不需要增加太多,視圖都非常地薄,所以給zPosition提高一個像素就可以讓綠色視圖前置,當然0.1或者0.0001也能夠做到,但是最好不要這樣,因為浮點類型四舍五入的計算可能會造成一些不便的麻煩。
清單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
第一章“圖層樹”證實了最好使用圖層相關視圖,而不是創建獨立的圖層關系。其中一個原因就是要處理額外復雜的觸摸事件。
CALayer并不關心任何響應鏈事件,所以不能直接處理觸摸事件或者手勢。但是它有一系列的方法幫你處理事件:-containsPoint:和-hitTest:。
-containsPoint:接受一個在本圖層坐標系下的CGPoint,如果這個點在圖層frame范圍內就返回YES。如清單3.4所示第一章的項目的另一個合適的版本,也就是使用-containsPoint:方法來判斷到底是白色還是藍色的圖層被觸摸了 (圖3.10)。這需要把觸摸坐標轉換成每個圖層坐標系下的坐標,結果很不方便。
清單3.4 使用containsPoint判斷被點擊的圖層
@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注意當調用圖層的-hitTest:方法時,測算的順序嚴格依賴于圖層樹當中的圖層順序(和UIView處理事件類似)。之前提到的zPosition屬性可以明顯改變屏幕上圖層的順序,但不能改變事件傳遞的順序。
這意味著如果改變了圖層的z軸順序,你會發現將不能夠檢測到最前方的視圖點擊事件,這是因為被另一個圖層遮蓋住了,雖然它的zPosition值較小,但是在圖層樹中的順序靠前。我們將在第五章詳細討論這個問題。
3.5 自動布局
你可能用過UIViewAutoresizingMask類型的一些常量,應用于當父視圖改變尺寸的時候,相應UIView的frame也跟著更新的場景(通常用于橫豎屏切換)。
在iOS6中,蘋果介紹了自動排版機制,它和自動調整不同,并且更加復雜。
在Mac OS平臺,CALayer有一個叫做layoutManager的屬性可以通過CALayoutManager協議和CAConstraintLayoutManager類來實現自動排版的機制。但由于某些原因,這在iOS上并不適用。
當使用視圖的時候,可以充分利用UIView類接口暴露出來的UIViewAutoresizingMask和NSLayoutConstraintAPI,但如果想隨意控制CALayer的布局,就需要手工操作。最簡單的方法就是使用CALayerDelegate如下函數:
- (void)layoutSublayersOfLayer:(CALayer *)layer;當圖層的bounds發生改變,或者圖層的-setNeedsLayout方法被調用的時候,這個函數將會被執行。這使得你可以手動地重新擺放或者重新調整子圖層的大小,但是不能像UIView的autoresizingMask和constraints屬性做到自適應屏幕旋轉。
這也是為什么最好使用視圖而不是單獨的圖層來構建應用程序的另一個重要原因之一。
3.6 總結
本章涉及了CALayer的集合結構,包括它的frame,position和bounds,介紹了三維空間內圖層的概念,以及如何在獨立的圖層內響應事件,最后簡單說明了在iOS平臺,Core Animation對自動調整和自動布局支持的缺乏。
在第四章“視覺效果”當中,我們接著介紹一些圖層外表的特性。
四、視覺效果
本節轉載自ios核心動畫高級技巧
嗯,圓和橢圓還不錯,但如果是帶圓角的矩形呢?
我們現在能做到那樣了么?
史蒂芬·喬布斯
我們在第三章『圖層幾何學』中討論了圖層的frame,第二章『寄宿圖』則討論了圖層的寄宿圖。但是圖層不僅僅可以是圖片或是顏色的容器;還有一系列內建的特性使得創造美麗優雅的令人深刻的界面元素成為可能。在這一章,我們將會探索一些能夠通過使用CALayer屬性實現的視覺效果。
4.1 圓角
圓角矩形是iOS的一個標志性審美特性。這在iOS的每一個地方都得到了體現,不論是主屏幕圖標,還是警告彈框,甚至是文本框。按照這流行程度,你可能會認為一定有不借助Photoshop就能輕易創建圓角舉行的方法。恭喜你,猜對了。
CALayer有一個叫做conrnerRadius的屬性控制著圖層角的曲率。它是一個浮點數,默認為0(為0的時候就是直角),但是你可以把它設置成任意值。默認情況下,這個曲率值只影響背景顏色而不影響背景圖片或是子圖層。不過,如果把masksToBounds設置成YES的話,圖層里面的所有東西都會被截取。
我們可以通過一個簡單的項目來演示這個效果。在Interface Builder中,我們放置一些視圖,他們有一些子視圖。而且這些子視圖有一些超出了邊界(如圖4.1)。你可能無法看到他們超出了邊界,因為在編輯界面的時候,超出的部分總是被Interface Builder裁切掉了。不過,你相信我就好了 :)
然后在代碼中,我們設置角的半徑為20個點,并裁剪掉第一個視圖的超出部分(見清單4.1)。技術上來說,這些屬性都可以在Interface Builder的探測板中分別通過『用戶定義運行時屬性』和勾選『裁剪子視圖』(Clip Subviews)選擇框來直接設置屬性的值。不過,在這個示例中,代碼能夠表示得更清楚。圖4.2是運行代碼的結果
清單4.1 設置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右圖中,紅色的子視圖沿角半徑被裁剪了
如你所見,右邊的子視圖沿邊界被裁剪了。
單獨控制每個層的圓角曲率也不是不可能的。如果想創建有些圓角有些直角的圖層或視圖時,你可能需要一些不同的方法。比如使用一個圖層蒙板(本章稍后會講到)或者是CAShapeLayer(見第六章『專用圖層』)。
4.2 圖層邊框
CALayer另外兩個非常有用屬性就是borderWidth和borderColor。二者共同定義了圖層邊的繪制樣式。這條線(也被稱作stroke)沿著圖層的bounds繪制,同時也包含圖層的角。
borderWidth是以點為單位的定義邊框粗細的浮點數,默認為0.borderColor定義了邊框的顏色,默認為黑色。
borderColor是CGColorRef類型,而不是UIColor,所以它不是Cocoa的內置對象。不過呢,你肯定也清楚圖層引用了borderColor,雖然屬性聲明并不能證明這一點。CGColorRef在引用/釋放時候的行為表現得與NSObject極其相似。但是Objective-C語法并不支持這一做法,所以CGColorRef屬性即便是強引用也只能通過assign關鍵字來聲明。
邊框是繪制在圖層邊界里面的,而且在所有子內容之前,也在子圖層之前。如果我們在之前的示例中(清單4.2)加入圖層的邊框,你就能看到到底是怎么一回事了(如圖4.3).
清單4.2 加上邊框
@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仔細觀察會發現邊框并不會把寄宿圖或子圖層的形狀計算進來,如果圖層的子圖層超過了邊界,或者是寄宿圖在透明區域有一個透明蒙板,邊框仍然會沿著圖層的邊界繪制出來(如圖4.4).
4.3 陰影
iOS的另一個常見特性呢,就是陰影。陰影往往可以達到圖層深度暗示的效果。也能夠用來強調正在顯示的圖層和優先級(比如說一個在其他視圖之前的彈出框),不過有時候他們只是單純的裝飾目的。
給shadowOpacity屬性一個大于默認值(也就是0)的值,陰影就可以顯示在任意圖層之下。shadowOpacity是一個必須在0.0(不可見)和1.0(完全不透明)之間的浮點數。如果設置為1.0,將會顯示一個有輕微模糊的黑色陰影稍微在圖層之上。若要改動陰影的表現,你可以使用CALayer的另外三個屬性:shadowColor,shadowOffset和shadowRadius。
顯而易見,shadowColor屬性控制著陰影的顏色,和borderColor和backgroundColor一樣,它的類型也是CGColorRef。陰影默認是黑色,大多數時候你需要的陰影也是黑色的(其他顏色的陰影看起來是不是有一點點奇怪。。)。
shadowOffset屬性控制著陰影的方向和距離。它是一個CGSize的值,寬度控制這陰影橫向的位移,高度控制著縱向的位移。shadowOffset的默認值是 {0, -3},意即陰影相對于Y軸有3個點的向上位移。
為什么要默認向上的陰影呢?盡管Core Animation是從圖層套裝演變而來(可以認為是為iOS創建的私有動畫框架),但是呢,它卻是在Mac OS上面世的,前面有提到,二者的Y軸是顛倒的。這就導致了默認的3個點位移的陰影是向上的。在Mac上,shadowOffset的默認值是陰影向下的,這樣你就能理解為什么iOS上的陰影方向是向上的了(如圖4.5).
蘋果更傾向于用戶界面的陰影應該是垂直向下的,所以在iOS把陰影寬度設為0,然后高度設為一個正值不失為一個做法。
shadowRadius屬性控制著陰影的模糊度,當它的值是0的時候,陰影就和視圖一樣有一個非常確定的邊界線。當值越來越大的時候,邊界線看上去就會越來越模糊和自然。蘋果自家的應用設計更偏向于自然的陰影,所以一個非零值再合適不過了。
通常來講,如果你想讓視圖或控件非常醒目獨立于背景之外(比如彈出框遮罩層),你就應該給shadowRadius設置一個稍大的值。陰影越模糊,圖層的深度看上去就會更明顯(如圖4.6).
陰影裁剪
和圖層邊框不同,圖層的陰影繼承自內容的外形,而不是根據邊界和角半徑來確定。為了計算出陰影的形狀,Core Animation會將寄宿圖(包括子視圖,如果有的話)考慮在內,然后通過這些來完美搭配圖層形狀從而創建一個陰影(見圖4.7)。
當陰影和裁剪扯上關系的時候就有一個頭疼的限制:陰影通常就是在Layer的邊界之外,如果你開啟了masksToBounds屬性,所有從圖層中突出來的內容都會被才剪掉。如果我們在我們之前的邊框示例項目中增加圖層的陰影屬性時,你就會發現問題所在(見圖4.8).
從技術角度來說,這個結果是可以是可以理解的,但確實又不是我們想要的效果。如果你想沿著內容裁切,你需要用到兩個圖層:一個只畫陰影的空的外圖層,和一個用masksToBounds裁剪內容的內圖層。
如果我們把之前項目的右邊用單獨的視圖把裁剪的視圖包起來,我們就可以解決這個問題(如圖4.9).
我們只把陰影用在最外層的視圖上,內層視圖進行裁剪。清單4.3是代碼實現,圖4.10是運行結果。
清單4.3 用一個額外的視圖來解決陰影裁切的問題
@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屬性
我們已經知道圖層陰影并不總是方的,而是從圖層內容的形狀繼承而來。這看上去不錯,但是實時計算陰影也是一個非常消耗資源的,尤其是圖層有多個子圖層,每個圖層還有一個有透明效果的寄宿圖的時候。
如果你事先知道你的陰影形狀會是什么樣子的,你可以通過指定一個shadowPath來提高性能。shadowPath是一個CGPathRef類型(一個指向CGPath的指針)。CGPath是一個Core Graphics對象,用來指定任意的一個矢量圖形。我們可以通過這個屬性單獨于圖層形狀之外指定陰影的形狀。
圖4.11 展示了同一寄宿圖的不同陰影設定。如你所見,我們使用的圖形很簡單,但是它的陰影可以是你想要的任何形狀。清單4.4是代碼實現。
清單4.4 創建簡單的陰影形狀
@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如果是一個矩形或者是圓,用CGPath會相當簡單明了。但是如果是更加復雜一點的圖形,UIBezierPath類會更合適,它是一個由UIKit提供的在CGPath基礎上的Objective-C包裝類。
圖4.6 大一些的陰影位移和角半徑會增加圖層的深度即視感
4.4 圖層蒙板
通過masksToBounds屬性,我們可以沿邊界裁剪圖形;通過cornerRadius屬性,我們還可以設定一個圓角。但是有時候你希望展現的內容不是在一個矩形或圓角矩形。比如,你想展示一個有星形框架的圖片,又或者想讓一些古卷文字慢慢漸變成背景色,而不是一個突兀的邊界。
使用一個32位有alpha通道的png圖片通常是創建一個無矩形視圖最方便的方法,你可以給它指定一個透明蒙板來實現。但是這個方法不能讓你以編碼的方式動態地生成蒙板,也不能讓子圖層或子視圖裁剪成同樣的形狀。
CALayer有一個屬性叫做mask可以解決這個問題。這個屬性本身就是個CALayer類型,有和其他圖層一樣的繪制和布局屬性。它類似于一個子圖層,相對于父圖層(即擁有該屬性的圖層)布局,但是它卻不是一個普通的子圖層。不同于那些繪制在父圖層中的子圖層,mask圖層定義了父圖層的部分可見區域。
mask圖層的Color屬性是無關緊要的,真正重要的是圖層的輪廓。mask屬性就像是一個餅干切割機,mask圖層實心的部分會被保留下來,其他的則會被拋棄。(如圖4.12)
如果mask圖層比父圖層要小,只有在mask圖層里面的內容才是它關心的,除此以外的一切都會被隱藏起來。
我們將代碼演示一下這個過程,創建一個簡單的項目,通過圖層的mask屬性來作用于圖片之上。為了簡便一些,我們用Interface Builder來創建一個包含UIImageView的圖片圖層。這樣我們就只要代碼實現蒙板圖層了。清單4.5是最終的代碼,圖4.13是運行后的結果。
清單4.5 應用蒙板圖層
@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蒙板圖層真正厲害的地方在于蒙板圖不局限于靜態圖。任何有圖層構成的都可以作為mask屬性,這意味著你的蒙板可以通過代碼甚至是動畫實時生成。
4.5 拉伸過濾
最后我們再來談談minificationFilter和magnificationFilter屬性。總得來講,當我們視圖顯示一個圖片的時候,都應該正確地顯示這個圖片(意即:以正確的比例和正確的1:1像素顯示在屏幕上)。原因如下:
- 能夠顯示最好的畫質,像素既沒有被壓縮也沒有被拉伸。
- 能更好的使用內存,因為這就是所有你要存儲的東西。
- 最好的性能表現,CPU不需要為此額外的計算。
不過有時候,顯示一個非真實大小的圖片確實是我們需要的效果。比如說一個頭像或是圖片的縮略圖,再比如說一個可以被拖拽和伸縮的大圖。這些情況下,為同一圖片的不同大小存儲不同的圖片顯得又不切實際。
當圖片需要顯示不同的大小的時候,有一種叫做拉伸過濾的算法就起到作用了。它作用于原圖的像素上并根據需要生成新的像素顯示在屏幕上。
事實上,重繪圖片大小也沒有一個統一的通用算法。這取決于需要拉伸的內容,放大或是縮小的需求等這些因素。CALayer為此提供了三種拉伸過濾方法,他們是:
- kCAFilterLinear
- kCAFilterNearest
- kCAFilterTrilinear
minification(縮小圖片)和magnification(放大圖片)默認的過濾器都是kCAFilterLinear,這個過濾器采用雙線性濾波算法,它在大多數情況下都表現良好。雙線性濾波算法通過對多個像素取樣最終生成新的值,得到一個平滑的表現不錯的拉伸。但是當放大倍數比較大的時候圖片就模糊不清了。
kCAFilterTrilinear和kCAFilterLinear非常相似,大部分情況下二者都看不出來有什么差別。但是,較雙線性濾波算法而言,三線性濾波算法存儲了多個大小情況下的圖片(也叫多重貼圖),并三維取樣,同時結合大圖和小圖的存儲進而得到最后的結果。
這個方法的好處在于算法能夠從一系列已經接近于最終大小的圖片中得到想要的結果,也就是說不要對很多像素同步取樣。這不僅提高了性能,也避免了小概率因舍入錯誤引起的取樣失靈的問題
kCAFilterNearest是一種比較武斷的方法。從名字不難看出,這個算法(也叫最近過濾)就是取樣最近的單像素點而不管其他的顏色。這樣做非常快,也不會使圖片模糊。但是,最明顯的效果就是,會使得壓縮圖片更糟,圖片放大之后也顯得塊狀或是馬賽克嚴重。
總的來說,對于比較小的圖或者是差異特別明顯,極少斜線的大圖,最近過濾算法會保留這種差異明顯的特質以呈現更好的結果。但是對于大多數的圖尤其是有很多斜線或是曲線輪廓的圖片來說,最近過濾算法會導致更差的結果。換句話說,線性過濾保留了形狀,最近過濾則保留了像素的差異。
讓我們來實驗一下。我們對第三章的時鐘項目改動一下,用LCD風格的數字方式顯示。我們用簡單的像素字體(一種用像素構成字符的字體,而非矢量圖形)創造數字顯示方式,用圖片存儲起來,而且用第二章介紹過的拼合技術來顯示(如圖4.16)。
我們在Interface Builder中放置了六個視圖,小時、分鐘、秒鐘各兩個,圖4.17顯示了這六個視圖是如何在Interface Builder中放置的。如果每個都用一個淡出的outlets對象就會顯得太多了,所以我們就用了一個IBOutletCollection對象把他們和控制器聯系起來,這樣我們就可以以數組的方式訪問視圖了。清單4.6是代碼實現。
清單4.6 顯示一個LCD風格的時鐘
@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,這樣做的確起了效果,但是圖片看起來模糊了。看起來默認的kCAFilterLinear選項讓我們失望了。
為了能像圖4.19中那樣,我們需要在for循環中加入如下代碼:
view.layer.magnificationFilter = kCAFilterNearest;
4.6 組透明
UIView有一個叫做alpha的屬性來確定視圖的透明度。CALayer有一個等同的屬性叫做opacity,這兩個屬性都是影響子層級的。也就是說,如果你給一個圖層設置了opacity屬性,那它的子圖層都會受此影響。
iOS常見的做法是把一個控件的alpha值設置為0.5(50%)以使其看上去呈現為不可用狀態。對于獨立的視圖來說還不錯,但是當一個控件有子視圖的時候就有點奇怪了,圖4.20展示了一個內嵌了UILabel的自定義UIButton;左邊是一個不透明的按鈕,右邊是50%透明度的相同按鈕。我們可以注意到,里面的標簽的輪廓跟按鈕的背景很不搭調。
這是由透明度的混合疊加造成的,當你顯示一個50%透明度的圖層時,圖層的每個像素都會一半顯示自己的顏色,另一半顯示圖層下面的顏色。這是正常的透明度的表現。但是如果圖層包含一個同樣顯示50%透明的子圖層時,你所看到的視圖,50%來自子視圖,25%來了圖層本身的顏色,另外的25%則來自背景色。
在我們的示例中,按鈕和表情都是白色背景。雖然他們都是50%的可見度,但是合起來的可見度是75%,所以標簽所在的區域看上去就沒有周圍的部分那么透明。所以看上去子視圖就高亮了,使得這個顯示效果都糟透了。
理想狀況下,當你設置了一個圖層的透明度,你希望它包含的整個圖層樹像一個整體一樣的透明效果。你可以通過設置Info.plist文件中的UIViewGroupOpacity為YES來達到這個效果,但是這個設置會影響到這個應用,整個app可能會受到不良影響。如果UIViewGroupOpacity并未設置,iOS 6和以前的版本會默認為NO(也許以后的版本會有一些改變)。
另一個方法就是,你可以設置CALayer的一個叫做shouldRasterize屬性(見清單4.7)來實現組透明的效果,如果它被設置為YES,在應用透明度之前,圖層及其子圖層都會被整合成一個整體的圖片,這樣就沒有透明度混合的問題了(如圖4.21)。
為了啟用shouldRasterize屬性,我們設置了圖層的rasterizationScale屬性。默認情況下,所有圖層拉伸都是1.0, 所以如果你使用了shouldRasterize屬性,你就要確保你設置了rasterizationScale屬性去匹配屏幕,以防止出現Retina屏幕像素化的問題。
當shouldRasterize和UIViewGroupOpacity一起的時候,性能問題就出現了(我們在第12章『速度』和第15章『圖層性能』將做出介紹),但是性能碰撞都本地化了(譯者注:這句話需要再翻譯)。
清單4.7 使用shouldRasterize屬性解決組透明問題
@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 總結
這一章介紹了一些可以通過代碼應用到圖層上的視覺效果,比如圓角,陰影和蒙板。我們也了解了拉伸過濾器和組透明。
在第五章,『變換』中,我們將會研究圖層變化和3D轉換
五、變換
本節轉載自ios核心動畫高級技巧
很不幸,沒人能告訴你母體是什么,你只能自己體會 – 駭客帝國
在第四章“可視效果”中,我們研究了一些增強圖層和它的內容顯示效果的一些技術,在這一章中,我們將要研究可以用來對圖層旋轉,擺放或者扭曲的CGAffineTransform,以及可以將扁平物體轉換成三維空間對象的CATransform3D(而不是僅僅對圓角矩形添加下沉陰影)。
5.1 仿射變換
在第三章“圖層幾何學”中,我們使用了UIView的transform屬性旋轉了鐘的指針,但并沒有解釋背后運作的原理,實際上UIView的transform屬性是一個CGAffineTransform類型,用于在二維空間做旋轉,縮放和平移。CGAffineTransform是一個可以和二維空間向量(例如CGPoint)做乘法的3X2的矩陣(見圖5.1)。
用CGPoint的每一列和CGAffineTransform矩陣的每一行對應元素相乘再求和,就形成了一個新的CGPoint類型的結果。要解釋一下圖中顯示的灰色元素,為了能讓矩陣做乘法,左邊矩陣的列數一定要和右邊矩陣的行數個數相同,所以要給矩陣填充一些標志值,使得既可以讓矩陣做乘法,又不改變運算結果,并且沒必要存儲這些添加的值,因為它們的值不會發生變化,但是要用來做運算。
因此,通常會用3×3(而不是2×3)的矩陣來做二維變換,你可能會見到3行2列格式的矩陣,這是所謂的以列為主的格式,圖5.1所示的是以行為主的格式,只要能保持一致,用哪種格式都無所謂。
當對圖層應用變換矩陣,圖層矩形內的每一個點都被相應地做變換,從而形成一個新的四邊形的形狀。CGAffineTransform中的“仿射”的意思是無論變換矩陣用什么值,圖層中平行的兩條線在變換之后任然保持平行,CGAffineTransform可以做出任意符合上述標注的變換,圖5.2顯示了一些仿射的和非仿射的變換:
創建一個CGAffineTransform
對矩陣數學做一個全面的闡述就超出本書的討論范圍了,不過如果你對矩陣完全不熟悉的話,矩陣變換可能會使你感到畏懼。幸運的是,Core Graphics提供了一系列函數,對完全沒有數學基礎的開發者也能夠簡單地做一些變換。如下幾個函數都創建了一個CGAffineTransform實例:
CGAffineTransformMakeRotation(CGFloat angle) CGAffineTransformMakeScale(CGFloat sx, CGFloat sy) CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)旋轉和縮放變換都可以很好解釋–分別旋轉或者縮放一個向量的值。平移變換是指每個點都移動了向量指定的x或者y值–所以如果向量代表了一個點,那它就平移了這個點的距離。
我們用一個很簡單的項目來做個demo,把一個原始視圖旋轉45度角度(圖5.3)
UIView可以通過設置transform屬性做變換,但實際上它只是封裝了內部圖層的變換。
CALayer同樣也有一個transform屬性,但它的類型是CATransform3D,而不是CGAffineTransform,本章后續將會詳細解釋。CALayer對應于UIView的transform屬性叫做affineTransform,清單5.1的例子就是使用affineTransform對圖層做了45度順時針旋轉。
清單5.1 使用affineTransform對圖層旋轉45度
注意我們使用的旋轉常量是M_PI_4,而不是你想象的45,因為iOS的變換函數使用弧度而不是角度作為單位。弧度用數學常量pi的倍數表示,一個pi代表180度,所以四分之一的pi就是45度。
C的數學函數庫(iOS會自動引入)提供了pi的一些簡便的換算,M_PI_4于是就是pi的四分之一,如果對換算不太清楚的話,可以用如下的宏做換算:
#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)
混合變換
Core Graphics提供了一系列的函數可以在一個變換的基礎上做更深層次的變換,如果做一個既要縮放又要旋轉的變換,這就會非常有用了。例如下面幾個函數:
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle) CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy) CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)當操縱一個變換的時候,初始生成一個什么都不做的變換很重要–也就是創建一個CGAffineTransform類型的空值,矩陣論中稱作單位矩陣,Core Graphics同樣也提供了一個方便的常量:
CGAffineTransformIdentity最后,如果需要混合兩個已經存在的變換矩陣,就可以使用如下方法,在兩個變換的基礎上創建一個新的變換:
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);我們來用這些函數組合一個更加復雜的變換,先縮小50%,再旋轉30度,最后向右移動200個像素(清單5.2)。圖5.4顯示了圖層變換最后的結果。
清單5.2 使用若干方法創建一個復合變換
- (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中有些需要注意的地方:圖片向右邊發生了平移,但并沒有指定距離那么遠(200像素),另外它還有點向下發生了平移。原因在于當你按順序做了變換,上一個變換的結果將會影響之后的變換,所以200像素的向右平移同樣也被旋轉了30度,縮小了50%,所以它實際上是斜向移動了100像素。
這意味著變換的順序會影響最終的結果,也就是說旋轉之后的平移和平移之后的旋轉結果可能不同。
#define DEGREES_TO_RADIANS(x) ((x)/180.0*M_PI)5.2 3D變換
CG的前綴告訴我們,CGAffineTransform類型屬于Core Graphics框架,Core Graphics實際上是一個嚴格意義上的2D繪圖API,并且CGAffineTransform僅僅對2D變換有效。
在第三章中,我們提到了zPosition屬性,可以用來讓圖層靠近或者遠離相機(用戶視角),transform屬性(CATransform3D類型)可以真正做到這點,即讓圖層在3D空間內移動或者旋轉。
和CGAffineTransform類似,CATransform3D也是一個矩陣,但是和2x3的矩陣不同,CATransform3D是一個可以在3維空間內做變換的4x4的矩陣(圖5.6)。
和CGAffineTransform矩陣類似,Core Animation提供了一系列的方法用來創建和組合CATransform3D類型的矩陣,和Core Graphics的函數類似,但是3D的平移和旋轉多處了一個z參數,并且旋轉函數除了angle之外多出了x,y,z三個參數,分別決定了每個坐標軸方向上的旋轉:
CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z) CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz) CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)你應該對X軸和Y軸比較熟悉了,分別以右和下為正方向(回憶第三章,這是iOS上的標準結構,在Mac OS,Y軸朝上為正方向),Z軸和這兩個軸分別垂直,指向視角外為正方向(圖5.7)。
由圖所見,繞Z軸的旋轉等同于之前二維空間的仿射旋轉,但是繞X軸和Y軸的旋轉就突破了屏幕的二維空間,并且在用戶視角看來發生了傾斜。
舉個例子:清單5.4的代碼使用了CATransform3DMakeRotation對視圖內的圖層繞Y軸做了45度角的旋轉,我們可以把視圖向右傾斜,這樣會看得更清晰。
結果見圖5.8,但并不像我們期待的那樣。
清單5.4 繞Y軸旋轉圖層
看起來圖層并沒有被旋轉,而是僅僅在水平方向上的一個壓縮,是哪里出了問題呢?
其實完全沒錯,視圖看起來更窄實際上是因為我們在用一個斜向的視角看它,而不是透視。
透視投影
在真實世界中,當物體遠離我們的時候,由于視角的原因看起來會變小,理論上說遠離我們的視圖的邊要比靠近視角的邊跟短,但實際上并沒有發生,而我們當前的視角是等距離的,也就是在3D變換中任然保持平行,和之前提到的仿射變換類似。
在等距投影中,遠處的物體和近處的物體保持同樣的縮放比例,這種投影也有它自己的用處(例如建筑繪圖,顛倒,和偽3D視頻),但當前我們并不需要。
為了做一些修正,我們需要引入投影變換(又稱作z變換)來對除了旋轉之外的變換矩陣做一些修改,Core Animation并沒有給我們提供設置透視變換的函數,因此我們需要手動修改矩陣值,幸運的是,很簡單:
CATransform3D的透視效果通過一個矩陣中一個很簡單的元素來控制:m34。m34(圖5.9)用于按比例縮放X和Y的值來計算到底要離視角多遠。
m34的默認值是0,我們可以通過設置m34為-1.0 / d來應用透視效果,d代表了想象中視角相機和屏幕之間的距離,以像素為單位,那應該如何計算這個距離呢?實際上并不需要,大概估算一個就好了。
因為視角相機實際上并不存在,所以可以根據屏幕上的顯示效果自由決定它的防止的位置。通常500-1000就已經很好了,但對于特定的圖層有時候更小后者更大的值會看起來更舒服,減少距離的值會增強透視效果,所以一個非常微小的值會讓它看起來更加失真,然而一個非常大的值會讓它基本失去透視效果,對視圖應用透視的代碼見清單5.5,結果見圖5.10。
清單5.5 對變換應用透視效果
@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滅點
當在透視角度繪圖的時候,遠離相機視角的物體將會變小變遠,當遠離到一個極限距離,它們可能就縮成了一個點,于是所有的物體最后都匯聚消失在同一個點。
在現實中,這個點通常是視圖的中心(圖5.11),于是為了在應用中創建擬真效果的透視,這個點應該聚在屏幕中點,或者至少是包含所有3D對象的視圖中點。
Core Animation定義了這個點位于變換圖層的anchorPoint(通常位于圖層中心,但也有例外,見第三章)。這就是說,當圖層發生變換時,這個點永遠位于圖層變換之前anchorPoint的位置。
當改變一個圖層的position,你也改變了它的滅點,做3D變換的時候要時刻記住這一點,當你視圖通過調整m34來讓它更加有3D效果,應該首先把它放置于屏幕中央,然后通過平移來把它移動到指定位置(而不是直接改變它的position),這樣所有的3D圖層都共享一個滅點。
sublayerTransform屬性
如果有多個視圖或者圖層,每個都做3D變換,那就需要分別設置相同的m34值,并且確保在變換之前都在屏幕中央共享同一個position,如果用一個函數封裝這些操作的確會更加方便,但仍然有限制(例如,你不能在Interface Builder中擺放視圖),這里有一個更好的方法。
CALayer有一個屬性叫做sublayerTransform。它也是CATransform3D類型,但和對一個圖層的變換不同,它影響到所有的子圖層。這意味著你可以一次性對包含這些圖層的容器做變換,于是所有的子圖層都自動繼承了這個變換方法。
相較而言,通過在一個地方設置透視變換會很方便,同時它會帶來另一個顯著的優勢:滅點被設置在容器圖層的中點,從而不需要再對子圖層分別設置了。這意味著你可以隨意使用position和frame來放置子圖層,而不需要把它們放置在屏幕中點,然后為了保證統一的滅點用變換來做平移。
我們來用一個demo舉例說明。這里用Interface Builder并排放置兩個視圖(圖5.12),然后通過設置它們容器視圖的透視變換,我們可以保證它們有相同的透視和滅點,代碼見清單5.6,結果見圖5.13。
清單5.6 應用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場景下旋轉圖層,那么也可以從背面去觀察它。如果我們在清單5.4中把角度修改為M_PI(180度)而不是當前的M_PI_4(45度),那么將會把圖層完全旋轉一個半圈,于是完全背對了相機視角。
那么從背部看圖層是什么樣的呢,見圖5.14
如你所見,圖層是雙面繪制的,反面顯示的是正面的一個鏡像圖片。
但這并不是一個很好的特性,因為如果圖層包含文本或者其他控件,那用戶看到這些內容的鏡像圖片當然會感到困惑。另外也有可能造成資源的浪費:想象用這些圖層形成一個不透明的固態立方體,既然永遠都看不見這些圖層的背面,那為什么浪費GPU來繪制它們呢?
CALayer有一個叫做doubleSided的屬性來控制圖層的背面是否要被繪制。這是一個BOOL類型,默認為YES,如果設置為NO,那么當圖層正面從相機視角消失的時候,它將不會被繪制。
扁平化圖層
如果對包含已經做過變換的圖層的圖層做反方向的變換將會發什么什么呢?是不是有點困惑?見圖5.15
注意做了-45度旋轉的內部圖層是怎樣抵消旋轉45度的圖層,從而恢復正常狀態的。
如果內部圖層相對外部圖層做了相反的變換(這里是繞Z軸的旋轉),那么按照邏輯這兩個變換將被相互抵消。
驗證一下,相應代碼見清單5.7,結果見5.16
清單5.7 繞Z軸做相反的旋轉變換
運行結果和我們預期的一致。現在在3D情況下再試一次。修改代碼,讓內外兩個視圖繞Y軸旋轉而不是Z軸,再加上透視效果,以便我們觀察。注意不能用sublayerTransform屬性,因為內部的圖層并不直接是容器圖層的子圖層,所以這里分別對圖層設置透視變換(清單5.8)。
清單5.8 繞Y軸相反的旋轉變換
- (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; }預期的效果應該如圖5.17所示。
但其實這并不是我們所看到的,相反,我們看到的結果如圖5.18所示。發什么了什么呢?內部的圖層仍然向左側旋轉,并且發生了扭曲,但按道理說它應該保持正面朝上,并且顯示正常的方塊。+
這是由于盡管Core Animation圖層存在于3D空間之內,但它們并不都存在同一個3D空間。每個圖層的3D場景其實是扁平化的,當你從正面觀察一個圖層,看到的實際上由子圖層創建的想象出來的3D場景,但當你傾斜這個圖層,你會發現實際上這個3D場景僅僅是被繪制在圖層的表面。
類似的,當你在玩一個3D游戲,實際上僅僅是把屏幕做了一次傾斜,或許在游戲中可以看見有一面墻在你面前,但是傾斜屏幕并不能夠看見墻里面的東西。所有場景里面繪制的東西并不會隨著你觀察它的角度改變而發生變化;圖層也是同樣的道理。
這使得用Core Animation創建非常復雜的3D場景變得十分困難。你不能夠使用圖層樹去創建一個3D結構的層級關系–在相同場景下的任何3D表面必須和同樣的圖層保持一致,這是因為每個的父視圖都把它的子視圖扁平化了。
至少當你用正常的CALayer的時候是這樣,CALayer有一個叫做CATransformLayer的子類來解決這個問題。具體在第六章“特殊的圖層”中將會具體討論。
5.3 固體對象
現在你懂得了在3D空間的一些圖層布局的基礎,我們來試著創建一個固態的3D對象(實際上是一個技術上所謂的空洞對象,但它以固態呈現)。我們用六個獨立的視圖來構建一個立方體的各個面。
在這個例子中,我們用Interface Builder來構建立方體的面(圖5.19),我們當然可以用代碼來寫,但是用Interface Builder的好處是可以方便的在每一個面上添加子視圖。記住這些面僅僅是包含視圖和控件的普通的用戶界面元素,它們完全是我們界面交互的部分,并且當把它折成一個立方體之后也不會改變這個性質。
這些面視圖并沒有放置在主視圖當中,而是松散地排列在根nib文件里面。我們并不關心在這個容器中如何擺放它們的位置,因為后續將會用圖層的transform對它們進行重新布局,并且用Interface Builder在容器視圖之外擺放他們可以讓我們容易看清楚它們的內容,如果把它們一個疊著一個都塞進主視圖,將會變得很難看。
我們把一個有顏色的UILabel放置在視圖內部,是為了清楚的辨別它們之間的關系,并且UIButton被放置在第三個面視圖里面,后面會做簡單的解釋。
具體把視圖組織成立方體的代碼見清單5.9,結果見圖5.20
清單5.9 創建一個立方體
從這個角度看立方體并不是很明顯;看起來只是一個方塊,為了更好地欣賞它,我們將更換一個不同的視角。
旋轉這個立方體將會顯得很笨重,因為我們要單獨對每個面做旋轉。另一個簡單的方案是通過調整容器視圖的sublayerTransform去旋轉照相機。
添加如下幾行去旋轉containerView圖層的perspective變換矩陣:
這就對相機(或者相對相機的整個場景,你也可以這么認為)繞Y軸旋轉45度,并且繞X軸旋轉45度。現在從另一個角度去觀察立方體,就能看出它的真實面貌(圖5.21)。
光亮和陰影
現在它看起來更像是一個立方體沒錯了,但是對每個面之間的連接還是很難分辨。Core Animation可以用3D顯示圖層,但是它對光線并沒有概念。如果想讓立方體看起來更加真實,需要自己做一個陰影效果。你可以通過改變每個面的背景顏色或者直接用帶光亮效果的圖片來調整。
如果需要動態地創建光線效果,你可以根據每個視圖的方向應用不同的alpha值做出半透明的陰影圖層,但為了計算陰影圖層的不透明度,你需要得到每個面的正太向量(垂直于表面的向量),然后根據一個想象的光源計算出兩個向量叉乘結果。叉乘代表了光源和圖層之間的角度,從而決定了它有多大程度上的光亮。
清單5.10實現了這樣一個結果,我們用GLKit框架來做向量的計算(你需要引入GLKit庫來運行代碼),每個面的CATransform3D都被轉換成GLKMatrix4,然后通過GLKMatrix4GetMatrix3函數得出一個3×3的旋轉矩陣。這個旋轉矩陣指定了圖層的方向,然后可以用它來得到正太向量的值。
結果如圖5.22所示,試著調整LIGHT_DIRECTION和AMBIENT_LIGHT的值來切換光線效果
清單5.10 對立方體的表面應用動態的光線效果
點擊事件
你應該能注意到現在可以在第三個表面的頂部看見按鈕了,點擊它,什么都沒發生,為什么呢?
這并不是因為iOS在3D場景下正確地處理響應事件,實際上是可以做到的。問題在于視圖順序。在第三章中我們簡要提到過,點擊事件的處理由視圖在父視圖中的順序決定的,并不是3D空間中的Z軸順序。當給立方體添加視圖的時候,我們實際上是按照一個順序添加,所以按照視圖/圖層順序來說,4,5,6在3的前面。
即使我們看不見4,5,6的表面(因為被1,2,3遮住了),iOS在事件響應上仍然保持之前的順序。當試圖點擊表面3上的按鈕,表面4,5,6截斷了點擊事件(取決于點擊的位置),這就和普通的2D布局在按鈕上覆蓋物體一樣。
你也許認為把doubleSided設置成NO可以解決這個問題,因為它不再渲染視圖后面的內容,但實際上并不起作用。因為背對相機而隱藏的視圖仍然會響應點擊事件(這和通過設置hidden屬性或者設置alpha為0而隱藏的視圖不同,那兩種方式將不會響應事件)。所以即使禁止了雙面渲染仍然不能解決這個問題(雖然由于性能問題,還是需要把它設置成NO)。
這里有幾種正確的方案:把除了表面3的其他視圖userInteractionEnabled屬性都設置成NO來禁止事件傳遞。或者簡單通過代碼把視圖3覆蓋在視圖6上。無論怎樣都可以點擊按鈕了(圖5.23)。
5.4 總結
這一章涉及了一些2D和3D的變換。你學習了一些矩陣計算的基礎,以及如何用Core Animation創建3D場景。你看到了圖層背后到底是如何呈現的,并且知道了不能把扁平的圖片做成真實的立體效果,最后我們用demo說明了觸摸事件的處理,視圖中圖層添加的層級順序會比屏幕上顯示的順序更有意義。
第六章我們會研究一些Core Animation提供不同功能的具體的CALayer子類。
六、專用圖層
本節轉載自ios核心動畫高級技巧
復雜的組織都是專門化的
Catharine R. Stimpson
到目前為止,我們已經探討過CALayer類了,同時我們也了解到了一些非常有用的繪圖和動畫功能。但是Core Animation圖層不僅僅能作用于圖片和顏色而已。本章就會學習其他的一些圖層類,進一步擴展使用Core Animation繪圖的能力。
6.1 CAShapeLayer
在第四章『視覺效果』我們學習到了不使用圖片的情況下用CGPath去構造任意形狀的陰影。如果我們能用同樣的方式創建相同形狀的圖層就好了。
CAShapeLayer是一個通過矢量圖形而不是bitmap來繪制的圖層子類。你指定諸如顏色和線寬等屬性,用CGPath來定義想要繪制的圖形,最后CAShapeLayer就自動渲染出來了。當然,你也可以用Core Graphics直接向原始的CALyer的內容中繪制一個路徑,相比直下,使用CAShapeLayer有以下一些優點:
- 渲染快速。CAShapeLayer使用了硬件加速,繪制同一圖形會比用Core Graphics快很多。
- 高效使用內存。一個CAShapeLayer不需要像普通CALayer一樣創建一個寄宿圖形,所以無論有多大,都不會占用太多的內存。
- 不會被圖層邊界剪裁掉。一個CAShapeLayer可以在邊界之外繪制。你的圖層路徑不會像在使用Core Graphics的普通CALayer一樣被剪裁掉(如我們在第二章所見)。
- 不會出現像素化。當你給CAShapeLayer做3D變換時,它不像一個有寄宿圖的普通圖層一樣變得像素化。
創建一個CGPath
CAShapeLayer可以用來繪制所有能夠通過CGPath來表示的形狀。這個形狀不一定要閉合,圖層路徑也不一定要不可破,事實上你可以在一個圖層上繪制好幾個不同的形狀。你可以控制一些屬性比如lineWith(線寬,用點表示單位),lineCap(線條結尾的樣子),和lineJoin(線條之間的結合點的樣子);但是在圖層層面你只有一次機會設置這些屬性。如果你想用不同顏色或風格來繪制多個形狀,就不得不為每個形狀準備一個圖層了。
清單6.1 的代碼用一個CAShapeLayer渲染一個簡單的火柴人。CAShapeLayer屬性是CGPathRef類型,但是我們用UIBezierPath幫助類創建了圖層路徑,這樣我們就不用考慮人工釋放CGPath了。圖6.1是代碼運行的結果。雖然還不是很完美,但是總算知道了大意對吧!
清單6.1 用CAShapeLayer繪制一個火柴人
#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為創建圓角視圖提供了一個方法,就是CALayer的cornerRadius屬性(譯者注:其實是在第四章提到的)。雖然使用CAShapeLayer類需要更多的工作,但是它有一個優勢就是可以單獨指定每個角。
我們創建圓角矩形其實就是人工繪制單獨的直線和弧度,但是事實上UIBezierPath有自動繪制圓角矩形的構造方法,下面這段代碼繪制了一個有三個圓角一個直角的矩形:
//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];我們可以通過這個圖層路徑繪制一個既有直角又有圓角的視圖。如果我們想依照此圖形來剪裁視圖內容,我們可以把CAShapeLayer作為視圖的宿主圖層,而不是添加一個子視圖(圖層蒙板的詳細解釋見第四章『視覺效果』)。
6.2 CATextLayer
用戶界面是無法從一個單獨的圖片里面構建的。一個設計良好的圖標能夠很好地表現一個按鈕或控件的意圖,不過你遲早都要需要一個不錯的老式風格的文本標簽。
如果你想在一個圖層里面顯示文字,完全可以借助圖層代理直接將字符串使用Core Graphics寫入圖層的內容(這就是UILabel的精髓)。如果越過寄宿于圖層的視圖,直接在圖層上操作,那其實相當繁瑣。你要為每一個顯示文字的圖層創建一個能像圖層代理一樣工作的類,還要邏輯上判斷哪個圖層需要顯示哪個字符串,更別提還要記錄不同的字體,顏色等一系列亂七八糟的東西。
萬幸的是這些都是不必要的,Core Animation提供了一個CALayer的子類CATextLayer,它以圖層的形式包含了UILabel幾乎所有的繪制特性,并且額外提供了一些新的特性。
同樣,CATextLayer也要比UILabel渲染得快得多。很少有人知道在iOS 6及之前的版本,UILabel其實是通過WebKit來實現繪制的,這樣就造成了當有很多文字的時候就會有極大的性能壓力。而CATextLayer使用了Core text,并且渲染得非常快。
讓我們來嘗試用CATextLayer來顯示一些文字。清單6.2的代碼實現了這一功能,結果如圖6.2所示。
清單6.2 用CATextLayer來實現一個UILabel
如果你仔細看這個文本,你會發現一個奇怪的地方:這些文本有一些像素化了。這是因為并沒有以Retina的方式渲染,第二章提到了這個contentScale屬性,用來決定圖層內容應該以怎樣的分辨率來渲染。contentsScale并不關心屏幕的拉伸因素而總是默認為1.0。如果我們想以Retina的質量來顯示文字,我們就得手動地設置CATextLayer的contentsScale屬性,如下:
textLayer.contentsScale = [UIScreen mainScreen].scale;
這樣就解決了這個問題(如圖6.3)
CATextLayer的font屬性不是一個UIFont類型,而是一個CFTypeRef類型。這樣可以根據你的具體需要來決定字體屬性應該是用CGFontRef類型還是CTFontRef類型(Core Text字體)。同時字體大小也是用fontSize屬性單獨設置的,因為CTFontRef和CGFontRef并不像UIFont一樣包含點大小。這個例子會告訴你如何將UIFont轉換成CGFontRef。
另外,CATextLayer的string屬性并不是你想象的NSString類型,而是id類型。這樣你既可以用NSString也可以用NSAttributedString來指定文本了(注意,NSAttributedString并不是NSString的子類)。屬性化字符串是iOS用來渲染字體風格的機制,它以特定的方式來決定指定范圍內的字符串的原始信息,比如字體,顏色,字重,斜體等。
富文本
iOS 6中,Apple給UILabel和其他UIKit文本視圖添加了直接的屬性化字符串的支持,應該說這是一個很方便的特性。不過事實上從iOS3.2開始CATextLayer就已經支持屬性化字符串了。這樣的話,如果你想要支持更低版本的iOS系統,CATextLayer無疑是你向界面中增加富文本的好辦法,而且也不用去跟復雜的Core Text打交道,也省了用UIWebView的麻煩。
讓我們編輯一下示例使用到NSAttributedString(見清單6.3).iOS 6及以上我們可以用新的NSTextAttributeName實例來設置我們的字符串屬性,但是練習的目的是為了演示在iOS 5及以下,所以我們用了Core Text,也就是說你需要把Core Text framework添加到你的項目中。否則,編譯器是無法識別屬性常量的。
圖6.4是代碼運行結果(注意那個紅色的下劃線文本)
清單6.3 用NSAttributedString實現一個富文本標簽。
行距和字距
有必要提一下的是,由于繪制的實現機制不同(Core Text和WebKit),用CATextLayer渲染和用UILabel渲染出的文本行距和字距也不是不盡相同的。
有二者的差異程度(由使用的字體和字符決定)總的來說挺小,但是如果你想正確的顯示普通便簽和CATextLayer就一定要記住這一點。
UILabel的替代品
有我們已經證實了CATextLayer比UILabel有著更好的性能表現,同時還有額外的布局選項并且在iOS 5上支持富文本。但是與一般的標簽比較而言會更加繁瑣一些。如果我們真的在需求一個UILabel的可用替代品,最好是能夠在Interface Builder上創建我們的標簽,而且盡可能地像一般的視圖一樣正常工作。
有我們應該繼承UILabel,然后添加一個子圖層CATextLayer并重寫顯示文本的方法。但是仍然會有由UILabel的-drawRect:方法創建的空寄宿圖。而且由于CALayer不支持自動縮放和自動布局,子視圖并不是主動跟蹤視圖邊界的大小,所以每次視圖大小被更改,我們不得不手動更新子圖層的邊界。
有我們真正想要的是一個用CATextLayer作為宿主圖層的UILabel子類,這樣就可以隨著視圖自動調整大小而且也沒有冗余的寄宿圖啦。
有就像我們在第一章『圖層樹』討論的一樣,每一個UIView都是寄宿在一個CALayer的示例上。這個圖層是由視圖自動創建和管理的,那我們可以用別的圖層類型替代它么?一旦被創建,我們就無法代替這個圖層了。但是如果我們繼承了UIView,那我們就可以重寫+layerClass方法使得在創建的時候能返回一個不同的圖層子類。UIView會在初始化的時候調用+layerClass方法,然后用它的返回類型來創建宿主圖層。
有清單6.4 演示了一個UILabel子類LayerLabel用CATextLayer繪制它的問題,而不是調用一般的UILabel使用的較慢的-drawRect:方法。LayerLabel示例既可以用代碼實現,也可以在Interface Builder實現,只要把普通的標簽拖入視圖之中,然后設置它的類是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如果你運行代碼,你會發現文本并沒有像素化,而我們也沒有設置contentsScale屬性。把CATextLayer作為宿主圖層的另一好處就是視圖自動設置了contentsScale屬性。
在這個簡單的例子中,我們只是實現了UILabel的一部分風格和布局屬性,不過稍微再改進一下我們就可以創建一個支持UILabel所有功能甚至更多功能的LayerLabel類(你可以在一些線上的開源項目中找到)。
如果你打算支持iOS 6及以上,基于CATextLayer的標簽可能就有有些局限性。但是總得來說,如果想在app里面充分利用CALayer子類,用+layerClass來創建基于不同圖層的視圖是一個簡單可復用的方法。
6.3 CATransformLayer
當我們在構造復雜的3D事物的時候,如果能夠組織獨立元素就太方便了。比如說,你想創造一個孩子的手臂:你就需要確定哪一部分是孩子的手腕,哪一部分是孩子的前臂,哪一部分是孩子的肘,哪一部分是孩子的上臂,哪一部分是孩子的肩膀等等。
當然是允許獨立地移動每個區域的啦。以肘為指點會移動前臂和手,而不是肩膀。Core Animation圖層很容易就可以讓你在2D環境下做出這樣的層級體系下的變換,但是3D情況下就不太可能,因為所有的圖層都把他的孩子都平面化到一個場景中(第五章『變換』有提到)。
CATransformLayer解決了這個問題,CATransformLayer不同于普通的CALayer,因為它不能顯示它自己的內容。只有當存在了一個能作用域子圖層的變換它才真正存在。CATransformLayer并不平面化它的子圖層,所以它能夠用于構造一個層級的3D結構,比如我的手臂示例。
用代碼創建一個手臂需要相當多的代碼,所以我就演示得更簡單一些吧:在第五章的立方體示例,我們將通過旋轉camara來解決圖層平面化問題而不是像立方體示例代碼中用的sublayerTransform。這是一個非常不錯的技巧,但是只能作用域單個對象上,如果你的場景包含兩個立方體,那我們就不能用這個技巧單獨旋轉他們了。
那么,就讓我們來試一試CATransformLayer吧,第一個問題就來了:在第五章,我們是用多個視圖來構造了我們的立方體,而不是單獨的圖層。我們不能在不打亂已有的視圖層次的前提下在一個本身不是有寄宿圖的圖層中放置一個寄宿圖圖層。我們可以創建一個新的UIView子類寄宿在CATransformLayer(用+layerClass方法)之上。但是,為了簡化案例,我們僅僅重建了一個單獨的圖層,而不是使用視圖。這意味著我們不能像第五章一樣在立方體表面顯示按鈕和標簽,不過我們現在也用不到這個特性。
清單6.5就是代碼。我們以我們在第五章使用過的相同基本邏輯放置立方體。但是并不像以前那樣直接將立方面添加到容器視圖的宿主圖層,我們將他們放置到一個CATransformLayer中創建一個獨立的立方體對象,然后將兩個這樣的立方體放進容器中。我們隨機地給立方面染色以將他們區分開來,這樣就不用靠標簽或是光亮來區分他們。圖6.5是運行結果。
清單6.5 用CATransformLayer裝配一個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是用來生成兩種或更多顏色平滑漸變的。用Core Graphics復制一個CAGradientLayer并將內容繪制到一個普通圖層的寄宿圖也是有可能的,但是CAGradientLayer的真正好處在于繪制使用了硬件加速。
基礎漸變
我們將從一個簡單的紅變藍的對角線漸變開始(見清單6.6).這些漸變色彩放在一個數組中,并賦給colors屬性。這個數組成員接受CGColorRef類型的值(并不是從NSObject派生而來),所以我們要用通過bridge轉換以確保編譯正常。
CAGradientLayer也有startPoint和endPoint屬性,他們決定了漸變的方向。這兩個參數是以單位坐標系進行的定義,所以左上角坐標是{0, 0},右下角坐標是{1, 1}。代碼運行結果如圖6.6
清單6.6 簡單的兩種顏色的對角線漸變
多重漸變
如果你愿意,colors屬性可以包含很多顏色,所以創建一個彩虹一樣的多重漸變也是很簡單的。默認情況下,這些顏色在空間上均勻地被渲染,但是我們可以用locations屬性來調整空間。locations屬性是一個浮點數值的數組(以NSNumber包裝)。這些浮點數定義了colors屬性中每個不同顏色的位置,同樣的,也是以單位坐標系進行標定。0.0代表著漸變的開始,1.0代表著結束。
locations數組并不是強制要求的,但是如果你給它賦值了就一定要確保locations的數組大小和colors數組大小一定要相同,否則你將會得到一個空白的漸變。
清單6.7展示了一個基于清單6.6的對角線漸變的代碼改造。現在變成了從紅到黃最后到綠色的漸變。locations數組指定了0.0,0.25和0.5三個數值,這樣這三個漸變就有點像擠在了左上角。(如圖6.7).
清單6.7 在漸變上使用locations
6.5 CAReplicatorLayer
CAReplicatorLayer的目的是為了高效生成許多相似的圖層。它會繪制一個或多個圖層的子圖層,并在每個復制體上應用不同的變換。看上去演示能夠更加解釋這些,我們來寫個例子吧。
重復圖層(Repeating Layers)
清單6.8中,我們在屏幕的中間創建了一個小白色方塊圖層,然后用CAReplicatorLayer生成十個圖層組成一個圓圈。instanceCount屬性指定了圖層需要重復多少次。instanceTransform指定了一個CATransform3D3D變換(這種情況下,下一圖層的位移和旋轉將會移動到圓圈的下一個點)。
變換是逐步增加的,每個實例都是相對于前一實例布局。這就是為什么這些復制體最終不會出現在同意位置上,圖6.8是代碼運行結果。
清單6.8 用CAReplicatorLayer重復圖層
注意到當圖層在重復的時候,他們的顏色也在變化:這是用instanceBlueOffset和instanceGreenOffset屬性實現的。通過逐步減少藍色和綠色通道,我們逐漸將圖層顏色轉換成了紅色。這個復制效果看起來很酷,但是CAReplicatorLayer真正應用到實際程序上的場景比如:一個游戲中導彈的軌跡云,或者粒子爆炸(盡管iOS 5已經引入了CAEmitterLayer,它更適合創建任意的粒子效果)。除此之外,還有一個實際應用是:反射。
反射
使用CAReplicatorLayer并應用一個負比例變換于一個復制圖層,你就可以創建指定視圖(或整個視圖層次)內容的鏡像圖片,這樣就創建了一個實時的『反射』效果。讓我們來嘗試實現這個創意:指定一個繼承于UIView的ReflectionView,它會自動產生內容的反射效果。實現這個效果的代碼很簡單(見清單6.9),實際上用ReflectionView實現這個效果會更簡單,我們只需要把ReflectionView的實例放置于Interface Builder(見圖6.9),它就會實時生成子視圖的反射,而不需要別的代碼(見圖6.10).
清單6.9 用CAReplicatorLayer自動繪制反射
開源代碼ReflectionView完成了一個自適應的漸變淡出效果(用CAGradientLayer
和圖層蒙板實現)
6.6 CAScrollLayer
對于一個未轉換的圖層,它的bounds和它的frame是一樣的,frame屬性是由bounds屬性自動計算而出的,所以更改任意一個值都會更新其他值。
但是如果你只想顯示一個大圖層里面的一小部分呢。比如說,你可能有一個很大的圖片,你希望用戶能夠隨意滑動,或者是一個數據或文本的長列表。在一個典型的iOS應用中,你可能會用到UITableView或是UIScrollView,但是對于獨立的圖層來說,什么會等價于剛剛提到的UITableView和UIScrollView呢?
在第二章中,我們探索了圖層的contentsRect屬性的用法,它的確是能夠解決在圖層中小地方顯示大圖片的解決方法。但是如果你的圖層包含子圖層那它就不是一個非常好的解決方案,因為,這樣做的話每次你想『滑動』可視區域的時候,你就需要手工重新計算并更新所有的子圖層位置。
這個時候就需要CAScrollLayer了。CAScrollLayer有一個-scrollToPoint:方法,它自動適應bounds的原點以便圖層內容出現在滑動的地方。注意,這就是它做的所有事情。前面提到過,Core Animation并不處理用戶輸入,所以CAScrollLayer并不負責將觸摸事件轉換為滑動事件,既不渲染滾動條,也不實現任何iOS指定行為例如滑動反彈(當視圖滑動超多了它的邊界的將會反彈回正確的地方)。
讓我們來用CAScrollLayer來常見一個基本的UIScrollView替代品。我們將會用CAScrollLayer作為視圖的宿主圖層,并創建一個自定義的UIView,然后用UIPanGestureRecognizer實現觸摸事件響應。這段代碼見清單6.10. 圖6.11是運行效果:ScrollView顯示了一個大于它的frame的UIImageView。
清單6.10 用CAScrollLayer實現滑動視圖
圖6.11 用UIScrollView創建一個湊合的滑動視圖
不同于UIScrollView,我們定制的滑動視圖類并沒有實現任何形式的邊界檢查(bounds checking)。圖層內容極有可能滑出視圖的邊界并無限滑下去。CAScrollLayer并沒有等同于UIScrollView中contentSize的屬性,所以當CAScrollLayer滑動的時候完全沒有一個全局的可滑動區域的概念,也無法自適應它的邊界原點至你指定的值。它之所以不能自適應邊界大小是因為它不需要,內容完全可以超過邊界。
那你一定會奇怪用CAScrollLayer的意義到底何在,因為你可以簡單地用一個普通的CALayer然后手動適應邊界原點啊。真相其實并不復雜,UIScrollView并沒有用CAScrollLayer,事實上,就是簡單的通過直接操作圖層邊界來實現滑動。
CAScrollLayer有一個潛在的有用特性。如果你查看CAScrollLayer的頭文件,你就會注意到有一個擴展分類實現了一些方法和屬性:
- (void)scrollPoint:(CGPoint)p; - (void)scrollRectToVisible:(CGRect)r; @property(readonly) CGRect visibleRect;看到這些方法和屬性名,你也許會以為這些方法給每個CALayer實例增加了滑動功能。但是事實上他們只是放置在CAScrollLayer中的圖層的實用方法。scrollPoint:方法從圖層樹中查找并找到第一個可用的CAScrollLayer,然后滑動它使得指定點成為可視的。scrollRectToVisible:方法實現了同樣的事情只不過是作用在一個矩形上的。visibleRect屬性決定圖層(如果存在的話)的哪部分是當前的可視區域。如果你自己實現這些方法就會相對容易明白一點,但是CAScrollLayer幫你省了這些麻煩,所以當涉及到實現圖層滑動的時候就可以用上了。
6.7 CATiledLayer
有些時候你可能需要繪制一個很大的圖片,常見的例子就是一個高像素的照片或者是地球表面的詳細地圖。iOS應用通暢運行在內存受限的設備上,所以讀取整個圖片到內存中是不明智的。載入大圖可能會相當地慢,那些對你看上去比較方便的做法(在主線程調用UIImage的-imageNamed:方法或者-imageWithContentsOfFile:方法)將會阻塞你的用戶界面,至少會引起動畫卡頓現象。
能高效繪制在iOS上的圖片也有一個大小限制。所有顯示在屏幕上的圖片最終都會被轉化為OpenGL紋理,同時OpenGL有一個最大的紋理尺寸(通常是2048 * 2048,或4096 * 4096,這個取決于設備型號)。如果你想在單個紋理中顯示一個比這大的圖,即便圖片已經存在于內存中了,你仍然會遇到很大的性能問題,因為Core Animation強制用CPU處理圖片而不是更快的GPU(見第12章『速度的曲調』,和第13章『高效繪圖』,它更加詳細地解釋了軟件繪制和硬件繪制)。
CATiledLayer為載入大圖造成的性能問題提供了一個解決方案:將大圖分解成小片然后將他們單獨按需載入。讓我們用實驗來證明一下。
小片裁剪
這個示例中,我們將會從一個2048 * 2048分辨率的雪人圖片入手。為了能夠從CATiledLayer中獲益,我們需要把這個圖片裁切成許多小一些的圖片。你可以通過代碼來完成這件事情,但是如果你在運行時讀入整個圖片并裁切,那CATiledLayer這些所有的性能優點就損失殆盡了。理想情況下來說,最好能夠逐個步驟來實現。
清單6.11 演示了一個簡單的Mac OS命令行程序,它用CATiledLayer將一個圖片裁剪成小圖并存儲到不同的文件中。
清單6.11 裁剪圖片成小圖的終端程序
這個程序將2048 * 2048分辨率的雪人圖案裁剪成了64個不同的256*256的小圖。(256*256是CATiledLayer的默認小圖大小,默認大小可以通過tileSize屬性更改)。程序接受一個圖片路徑作為命令行的第一個參數。我們可以在編譯的scheme將路徑參數硬編碼然后就可以在Xcode中運行了,但是以后作用在另一個圖片上就不方便了。所以,我們編譯了這個程序并把它保存到敏感的地方,然后從終端調用,如下面所示:
> path/to/TileCutterApp path/to/Snowman.jpg
這個程序相當基礎,但是能夠輕易地擴展支持額外的參數比如小圖大小,或者導出格式等等。運行結果是64個新圖的序列,如下面命名:
Snowman_00_00.jpg Snowman_00_01.jpg Snowman_00_02.jpg ... Snowman_07_07.jpg既然我們有了裁切后的小圖,我們就要讓iOS程序用到他們。CATiledLayer很好地和UIScrollView集成在一起。除了設置圖層和滑動視圖邊界以適配整個圖片大小,我們真正要做的就是實現-drawLayer:inContext:方法,當需要載入新的小圖時,CATiledLayer就會調用到這個方法。
清單6.12演示了代碼。圖6.12是代碼運行結果。
清單6.12 一個簡單的滾動CATiledLayer實現
當你滑動這個圖片,你會發現當CATiledLayer載入小圖的時候,他們會淡入到界面中。這是CATiledLayer的默認行為。(你可能已經在iOS 6之前的蘋果地圖程序中見過這個效果)你可以用fadeDuration屬性改變淡入時長或直接禁用掉。CATiledLayer(不同于大部分的UIKit和Core Animation方法)支持多線程繪制,-drawLayer:inContext:方法可以在多個線程中同時地并發調用,所以請小心謹慎地確保你在這個方法中實現的繪制代碼是線程安全的。
Retina小圖
你也許已經注意到了這些小圖并不是以Retina的分辨率顯示的。為了以屏幕的原生分辨率來渲染CATiledLayer,我們需要設置圖層的contentsScale來匹配UIScreen的scale屬性:
tileLayer.contentsScale = [UIScreen mainScreen].scale;
有趣的是,tileSize是以像素為單位,而不是點,所以增大了contentsScale就自動有了默認的小圖尺寸(現在它是128*128的點而不是256*256).所以,我們不需要手工更新小圖的尺寸或是在Retina分辨率下指定一個不同的小圖。我們需要做的是適應小圖渲染代碼以對應安排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);通過這個方法糾正scale
也意味著我們的雪人圖將以一半的大小渲染在Retina設備上(總尺寸是1024*1024,而不是2048*2048)。這個通常都不會影響到用CATiledLayer
正常顯示的圖片類型(比如照片和地圖,他們在設計上就是要支持放大縮小,能夠在不同的縮放條件下顯示),但是也需要在心里明白。
6.8 CAEmitterLayer
在iOS 5中,蘋果引入了一個新的CALayer子類叫做CAEmitterLayer。CAEmitterLayer是一個高性能的粒子引擎,被用來創建實時例子動畫如:煙霧,火,雨等等這些效果。
CAEmitterLayer看上去像是許多CAEmitterCell的容器,這些CAEmitierCell定義了一個例子效果。你將會為不同的例子效果定義一個或多個CAEmitterCell作為模版,同時CAEmitterLayer負責基于這些模版實例化一個粒子流。一個CAEmitterCell類似于一個CALayer:它有一個contents屬性可以定義為一個CGImage,另外還有一些可設置屬性控制著表現和行為。我們不會對這些屬性逐一進行詳細的描述,你們可以在CAEmitterCell類的頭文件中找到。
我們來舉個例子。我們將利用在一圓中發射不同速度和透明度的粒子創建一個火爆炸的效果。清單6.13包含了生成爆炸的代碼。圖6.13是運行結果
清單6.13 用CAEmitterLayer創建爆炸效果
圖6.13 火焰爆炸效果
CAEMitterCell的屬性基本上可以分為三種:
- 這種粒子的某一屬性的初始值。比如,color屬性指定了一個可以混合圖片內容顏色的混合色。在示例中,我們將它設置為桔色。
- 例子某一屬性的變化范圍。比如emissionRange屬性的值是2π,這意味著例子可以從360度任意位置反射出來。如果指定一個小一些的值,就可以創造出一個圓錐形
- 指定值在時間線上的變化。比如,在示例中,我們將alphaSpeed設置為-0.4,就是說例子的透明度每過一秒就是減少0.4,這樣就有發射出去之后逐漸小時的效果。
CAEmitterLayer的屬性它自己控制著整個例子系統的位置和形狀。一些屬性比如birthRate,lifetime和celocity,這些屬性在CAEmitterCell中也有。這些屬性會以相乘的方式作用在一起,這樣你就可以用一個值來加速或者擴大整個例子系統。其他值得提到的屬性有以下這些:
- preservesDepth,是否將3D例子系統平面化到一個圖層(默認值)或者可以在3D空間中混合其他的圖層
- renderMode,控制著在視覺上粒子圖片是如何混合的。你可能已經注意到了示例中我們把它設置為kCAEmitterLayerAdditive,它實現了這樣一個效果:合并例子重疊部分的亮度使得看上去更亮。如果我們把它設置為默認的kCAEmitterLayerUnordered,效果就沒那么好看了(見圖6.14).
6.8 CAEAGLLayer
當iOS要處理高性能圖形繪制,必要時就是OpenGL。應該說它應該是最后的殺手锏,至少對于非游戲的應用來說是的。因為相比Core Animation和UIkit框架,它不可思議地復雜。
OpenGL提供了Core Animation的基礎,它是底層的C接口,直接和iPhone,iPad的硬件通信,極少地抽象出來的方法。OpenGL沒有對象或是圖層的繼承概念。它只是簡單地處理三角形。OpenGL中所有東西都是3D空間中有顏色和紋理的三角形。用起來非常復雜和強大,但是用OpenGL繪制iOS用戶界面就需要很多很多的工作了。
為了能夠以高性能使用Core Animation,你需要判斷你需要繪制哪種內容(矢量圖形,例子,文本,等等),但后選擇合適的圖層去呈現這些內容,Core Animation中只有一些類型的內容是被高度優化的;所以如果你想繪制的東西并不能找到標準的圖層類,想要得到高性能就比較費事情了。
因為OpenGL根本不會對你的內容進行假設,它能夠繪制得相當快。利用OpenGL,你可以繪制任何你知道必要的集合信息和形狀邏輯的內容。所以很多游戲都喜歡用OpenGL(這些情況下,Core Animation的限制就明顯了:它優化過的內容類型并不一定能滿足需求),但是這樣依賴,方便的高度抽象接口就沒了。
在iOS 5中,蘋果引入了一個新的框架叫做GLKit,它去掉了一些設置OpenGL的復雜性,提供了一個叫做CLKView的UIView的子類,幫你處理大部分的設置和繪制工作。前提是各種各樣的OpenGL繪圖緩沖的底層可配置項仍然需要你用CAEAGLLayer完成,它是CALayer的一個子類,用來顯示任意的OpenGL圖形。
大部分情況下你都不需要手動設置CAEAGLLayer(假設用GLKView),過去的日子就不要再提了。特別的,我們將設置一個OpenGL ES 2.0的上下文,它是現代的iOS設備的標準做法。
盡管不需要GLKit也可以做到這一切,但是GLKit囊括了很多額外的工作,比如設置頂點和片段著色器,這些都以類C語言叫做GLSL自包含在程序中,同時在運行時載入到圖形硬件中。編寫GLSL代碼和設置EAGLayer沒有什么關系,所以我們將用GLKBaseEffect類將著色邏輯抽象出來。其他的事情,我們還是會有以往的方式。
在開始之前,你需要將GLKit和OpenGLES框架加入到你的項目中,然后就可以實現清單6.14中的代碼,里面是設置一個GAEAGLLayer的最少工作,它使用了OpenGL ES 2.0 的繪圖上下文,并渲染了一個有色三角(見圖6.15).
清單6.14 用CAEAGLLayer繪制一個三角形
#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
在一個真正的OpenGL應用中,我們可能會用NSTimer或CADisplayLink周期性地每秒鐘調用-drawRrame方法60次,同時會將幾何圖形生成和繪制分開以便不會每次都重新生成三角形的頂點(這樣也可以讓我們繪制其他的一些東西而不是一個三角形而已),不過上面這個例子已經足夠演示了繪圖原則了。
6.10 AVPlayerLayer
最后一個圖層類型是AVPlayerLayer。盡管它不是Core Animation框架的一部分(AV前綴看上去像),AVPlayerLayer是有別的框架(AVFoundation)提供的,它和Core Animation緊密地結合在一起,提供了一個CALayer子類來顯示自定義的內容類型。
AVPlayerLayer是用來在iOS上播放視頻的。他是高級接口例如MPMoivePlayer的底層實現,提供了顯示視頻的底層控制。AVPlayerLayer的使用相當簡單:你可以用+playerLayerWithPlayer:方法創建一個已經綁定了視頻播放器的圖層,或者你可以先創建一個圖層,然后用player屬性綁定一個AVPlayer實例。
在我們開始之前,我們需要添加AVFoundation到我們的項目中。然后,清單6.15創建了一個簡單的電影播放器,圖6.16是代碼運行結果。
清單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我們用代碼創建了一個AVPlayerLayer,但是我們仍然把它添加到了一個容器視圖中,而不是直接在controller中的主視圖上添加。這樣其實是為了可以使用自動布局限制使得圖層在最中間;否則,一旦設備被旋轉了我們就要手動重新放置位置,因為Core Animation并不支持自動大小和自動布局(見第三章『圖層幾何學』)。
當然,因為AVPlayerLayer是CALayer的子類,它繼承了父類的所有特性。我們并不會受限于要在一個矩形中播放視頻;清單6.16演示了在3D,圓角,有色邊框,蒙板,陰影等效果(見圖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 總結
這一章我們簡要概述了一些專用圖層以及用他們實現的一些效果,我們只是了解到這些圖層的皮毛,像CATiledLayer和CAEMitterLayer這些類可以單獨寫一章的。但是,重點是記住CALayer是用處很大的,而且它并沒有為所有可能的場景進行優化。為了獲得Core Animation最好的性能,你需要為你的工作選對正確的工具,希望你能夠挖掘這些不同的CALayer子類的功能。 這一章我們通過CAEmitterLayer和AVPlayerLayer類簡單地接觸到了一些動畫,在第二章,我們將繼續深入研究動畫,就從隱式動畫開始。
總結
以上是生活随笔為你收集整理的CoreAnimation的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 51单片机 Proteus仿真 四路倒计
- 下一篇: 验证JDK是否安装成功