Core Animation其實(shí)是一個(gè)令人誤解的命名栈顷。你可能認(rèn)為它只是用來做動(dòng)畫的,但實(shí)際上它是從一個(gè)叫做Layer Kit這么一個(gè)不怎么和動(dòng)畫有關(guān)的名字演變而來涡扼,所以做動(dòng)畫這只是Core Animation特性的冰山一角亮蒋。Core Animation是一個(gè)復(fù)合引擎闻书,它的職責(zé)就是盡可能快地組合屏幕上不同的可視內(nèi)容楞黄,這個(gè)內(nèi)容是被分解成獨(dú)立的圖層兄裂,存儲(chǔ)在一個(gè)叫做圖層樹的體系之中句旱。于是這個(gè)樹形成了UIKit以及在iOS應(yīng)用程序當(dāng)中你所能在屏幕上看見的一切的基礎(chǔ)。
1晰奖、圖層與視圖谈撒。
如果你曾經(jīng)在iOS或者M(jìn)ac OS平臺(tái)上寫過應(yīng)用程序,你可能會(huì)對(duì)視圖的概念比較熟悉匾南。一個(gè)視圖就是在屏幕上顯示的一個(gè)矩形塊(比如圖片啃匿,文字或者視頻),它能夠攔截類似于鼠標(biāo)點(diǎn)擊或者觸摸手勢等用戶輸入蛆楞。視圖在層級(jí)關(guān)系中可以互相嵌套溯乒,一個(gè)視圖可以管理它的所有子視圖的位置。
在iOS當(dāng)中豹爹,所有的視圖都從一個(gè)叫做UIVIew的基類派生而來裆悄,UIView可以處理觸摸事件,可以支持基于Core Graphics繪圖臂聋,可以做仿射變換(例如旋轉(zhuǎn)或者縮放)光稼,或者簡單的類似于滑動(dòng)或者漸變的動(dòng)畫
CALayer類在概念上和UIView類似崖技,同樣也是一些被層級(jí)關(guān)系樹管理的矩形塊,同樣也可以包含一些內(nèi)容(像圖片钟哥,文本或者背景色)迎献,管理子圖層的位置。它們有一些方法和屬性用來做動(dòng)畫和變換腻贰。和UIView最大的不同是CALayer不處理用戶的交互吁恍。
CALayer并不清楚具體的響應(yīng)鏈(iOS通過視圖層級(jí)關(guān)系用來傳送觸摸事件的機(jī)制),于是它并不能夠響應(yīng)事件播演,即使它提供了一些方法來判斷是否一個(gè)觸點(diǎn)在圖層的范圍之內(nèi)(具體見第三章冀瓦,“圖層的幾何學(xué)”)
每一個(gè)UIview都有一個(gè)CALayer實(shí)例的圖層屬性,也就是所謂的backing layer写烤,視圖的職責(zé)就是創(chuàng)建并管理這個(gè)圖層翼闽,以確保當(dāng)子視圖在層級(jí)關(guān)系中添加或者被移除的時(shí)候,他們關(guān)聯(lián)的圖層也同樣對(duì)應(yīng)在層級(jí)關(guān)系樹當(dāng)中有相同的操作
實(shí)際上這些背后關(guān)聯(lián)的圖層才是真正用來在屏幕上顯示和做動(dòng)畫洲炊,UIView僅僅是對(duì)它的一個(gè)封裝感局,提供了一些iOS類似于處理觸摸的具體功能,以及Core Animation底層方法的高級(jí)接口
但是為什么iOS要基于UIView和CALayer提供兩個(gè)平行的層級(jí)關(guān)系呢暂衡?為什么不用一個(gè)簡單的層級(jí)來處理所有事情呢询微?原因在于要做職責(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ū)別胯杭。
1.2 圖層的能力
如果說CALayer是UIView內(nèi)部實(shí)現(xiàn)細(xì)節(jié),那我們?yōu)槭裁匆娴亓私馑厝唬刻O果當(dāng)然為我們提供了優(yōu)美簡潔的UIView接口歉摧,那么我們是否就沒必要直接去處理Core Animation的細(xì)節(jié)了呢?
某種意義上說的確是這樣腔呜,對(duì)一些簡單的需求來說叁温,我們確實(shí)沒必要處理CALayer,因?yàn)樘O果已經(jīng)通過UIView的高級(jí)API間接地使得動(dòng)畫變得很簡單核畴。
但是這種簡單會(huì)不可避免地帶來一些靈活上的缺陷膝但。如果你略微想在底層做一些改變,或者使用一些蘋果沒有在UIView上實(shí)現(xiàn)的接口功能谤草,這時(shí)除了介入Core Animation底層之外別無選擇跟束。
我們已經(jīng)證實(shí)了圖層不能像視圖那樣處理觸摸事件莺奸,那么他能做哪些視圖不能做的呢?這里有一些UIView沒有暴露出來的CALayer的功能:陰影冀宴,圓角灭贷,帶顏色的邊框;3D變換略贮;非矩形范圍甚疟,透明遮罩、多級(jí)非線性動(dòng)畫逃延。
一個(gè)視圖只有一個(gè)相關(guān)聯(lián)的圖層(自動(dòng)創(chuàng)建)览妖,同時(shí)它也可以支持添加無數(shù)多個(gè)子圖層,你可以顯示創(chuàng)建一個(gè)單獨(dú)的圖層揽祥,并且把它直接添加到視圖關(guān)聯(lián)圖層的子圖層讽膏。盡管可以這樣添加圖層,但往往我們只是見簡單地處理視圖拄丰,他們關(guān)聯(lián)的圖層并不需要額外地手動(dòng)添加子圖層府树。
使用圖層關(guān)聯(lián)的視圖而不是CALayer的好處在于,你能在使用所有CALayer底層特性的同時(shí)愈案,也可以使用UIView的高級(jí)API(比如自動(dòng)排版挺尾,布局和事件處理)
然而,當(dāng)滿足以下條件的時(shí)候站绪,你可能更需要使用CALayer而不是UIView:
1)、開發(fā)同時(shí)可以在Mac OS上運(yùn)行的跨平臺(tái)應(yīng)用
2). 使用多種CALayer的子類,并且不想創(chuàng)建額外的UIView去包封裝它們所有;
但是這些例子都很少見丽柿,總的來說恢准,處理視圖會(huì)比單獨(dú)處理圖層更加方便
使用圖層
在屏幕中創(chuàng)建一個(gè)UIView對(duì)象正方形的,我們要在這個(gè)view上添加一個(gè)藍(lán)色的色塊甫题。我們可以添加一個(gè)子view用代碼或者IB都OK馁筐。但是這里我們準(zhǔn)備使用CAlayer。如果要想用layer相關(guān)的接口和屬性需要添加QuartzCore框架坠非。
//create sublayer
CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;
//add it to our view
[self.layerView.layer addSublayer:blueLayer];
每一個(gè)視圖只有一個(gè)相關(guān)聯(lián)的圖層敏沉,可以在這個(gè)自動(dòng)創(chuàng)建的圖層上添加無數(shù)多的字圖層。
寄宿圖
寄宿圖就是指在CAlayer中包含的圖炎码。
對(duì)寄宿圖的處理主要在layer的contents屬性上盟迟,可以參考文章:iOS-CAlayer之contents 屬性。給contents賦值CGImage并不是唯一的設(shè)置寄宿圖的方法潦闲。我們可以通過CoreGraphics直接繪制寄宿圖攒菠。可以通過重寫drawRect方法來繪制歉闰。
-drawRect:方法沒有默認(rèn)的實(shí)現(xiàn)辖众,因?yàn)閷?duì)UIView來說卓起,寄宿圖并不是必須的,它不在意是單調(diào)的顏色還是一個(gè)圖片的實(shí)例凹炸。如果UIView檢測到-drawRect:方法被調(diào)用了戏阅,它就會(huì)為視圖分配一個(gè)寄宿圖,這個(gè)寄宿圖的像素尺寸就是視圖大小乘以contentsScale啤它。如果你不需要激素圖奕筐,那就不要這個(gè)方法了,這會(huì)造成CPU資源和內(nèi)存的浪費(fèi)蚕键。這就是如果沒有自定義的繪制任務(wù)救欧,不要重寫-drawRect:方法。
? ? ?當(dāng)視圖在屏幕出現(xiàn)的時(shí)候-drawRect:方法就會(huì)自動(dòng)調(diào)用锣光。-drawRect:方法里的代碼利用coregraphics去繪制一個(gè)寄宿圖笆怠,然后內(nèi)容就會(huì)被緩存起來直到它需要被更新(通常是因?yàn)殚_發(fā)者調(diào)用-setNeedsDisplay方法,盡管影響到表現(xiàn)效果的屬性值被更改時(shí)誊爹,一些視圖類型會(huì)被自動(dòng)重繪蹬刷,如bounds屬性)。
CAlayer有一個(gè)可選的delegate屬性频丘,實(shí)現(xiàn)了CAlayerDelegate協(xié)議办成,當(dāng)CAlayer需要一個(gè)內(nèi)容特定的信息時(shí),就會(huì)從協(xié)議中請(qǐng)求搂漠。CAlayerDelegate是一個(gè)非正式協(xié)議迂卢,沒有CAlayerDelegate可以在類中引用。
當(dāng)被要求重繪時(shí)桐汤,CAlayer會(huì)請(qǐng)求它的代理給它一個(gè)寄宿圖來顯示而克。它通過下面這個(gè)方法做到的:
-(void)displayLayer:(CAlayer*)layer;
我們就可以在方法中直接設(shè)置contents屬性,如果代理不實(shí)現(xiàn)此方法怔毛,CAlayer的代理就會(huì)嘗試調(diào)用下面這個(gè)方法:
-(void)drawLayer:(CAlayer*)layer in Context:(CGContextRef)ctx;
下面利用CAlayerDelegate做一些繪圖工作员萍。
//create sublayer
CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;
//set controller as layer delegate
blueLayer.delegate = self;
//ensure that layer backing image uses correct scale
blueLayer.contentsScale = [UIScreen mainScreen].scale; //add layer to our view
[self.layerView.layer addSublayer:blueLayer];
//force layer to redraw
[blueLayer display];
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
//draw a thick red circle
CGContextSetLineWidth(ctx, 10.0f);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextStrokeEllipseInRect(ctx, layer.bounds);
}
Tips:1、我們在blueLayer上顯式的調(diào)用-display拣度。不同于UIView碎绎,當(dāng)CAlayer顯示在屏幕上時(shí),CALayer不會(huì)自動(dòng)重繪它的內(nèi)容抗果。它把重繪的決定權(quán)交給了developer筋帖。
? ? ? ? ? ?2、盡管我們沒有用maskToBounds屬性窖张,繪制的那個(gè)圓仍然沿邊界被裁剪了幕随,這是因?yàn)楫?dāng)你使用CAlayerDelegate繪制激素圖的時(shí)候,比沒有對(duì)超出layer邊界的內(nèi)容提供繪制支持宿接。
現(xiàn)在我們理解并知道怎么使用CAlayerDelegate赘淮,但是除非你創(chuàng)建一個(gè)單獨(dú)的圖層辕录,你幾乎沒有機(jī)會(huì)用奧CAlayerDelegate協(xié)議。因?yàn)楫?dāng)UIView創(chuàng)建它自己的宿主layer的時(shí)候梢卸,它自動(dòng)的就把layer的delegate設(shè)置為它自己走诞,并提供了-displayer:的實(shí)現(xiàn)。
當(dāng)使用寄宿圖層的時(shí)候蛤高,你也不必實(shí)現(xiàn)CALayerDelegate的方法蚣旱。通常都是重寫-drawRect:方法,UIView就會(huì)幫你完成剩下的工作戴陡,包括重繪時(shí)候調(diào)用-display方法塞绿。
? ? ?這一小節(jié)介紹了寄宿圖和一些相關(guān)屬性。學(xué)到了如何顯示和放置圖片恤批,使用拼合技術(shù)來顯示异吻,以及用CAlayerDelegate 和coreGraphics繪制圖層的。
圖層幾何學(xué)
在上一節(jié)中喜庞,我們介紹了圖層背后的圖片诀浪,和一些控制圖層的坐標(biāo)和旋轉(zhuǎn)的屬性,現(xiàn)在我們要學(xué)習(xí)如何根據(jù)父圖層和兄弟圖層來控制位置和尺寸延都,如何管理圖層的幾何結(jié)構(gòu)雷猪,以及它們是如何被自動(dòng)調(diào)整和自動(dòng)布局影響的。
1晰房、UIView有三個(gè)比較重要的布局屬性:frame求摇,bounds,center殊者;CAlayer對(duì)應(yīng)的叫做frame月帝,bounds和position。為了區(qū)分清楚幽污,layer用position,Uiview用center它們都是代表同樣的值簿姨。
frame代表了圖層的外部坐標(biāo)也就是在父圖層上占據(jù)的空間距误,bounds時(shí)內(nèi)部坐標(biāo)({0,0}通常是圖層的左上角),center和position都代表了相對(duì)于父圖層anchorPoint:錨點(diǎn)所在的位置”馕唬現(xiàn)在把它想成圖層的重點(diǎn)就好准潭。
? ? ? ? 視圖的frame、bounds域仇、center屬性僅僅是存取方法刑然,當(dāng)操作視圖frame,實(shí)際上是在改變位于視圖下方CAlayer的frame暇务,不能獨(dú)立于圖層之外改變視圖的frame泼掠。
對(duì)于視圖或者圖層來說怔软,frame并不是一個(gè)非常清晰的屬性,他是一個(gè)虛擬的屬性择镇,是根據(jù)bounds挡逼,positon和transform計(jì)算而來,所以當(dāng)其中任何一餓值發(fā)生變化腻豌,frame都會(huì)變化家坎。相反,改變frame的值一樣會(huì)影響bounds吝梅、tranform的值虱疏。
當(dāng)圖層做變換的時(shí)候,比如旋轉(zhuǎn)或者縮放苏携,frame實(shí)際上代表了覆蓋在圖層旋轉(zhuǎn)之后的整個(gè)軸對(duì)齊的矩形區(qū)域做瞪,也就是frame的寬高和bounds的寬高不再一致了。
上文提到過視圖的center和圖層的position都是指定了一個(gè)anchorPoint相對(duì)于父圖層的位置兜叨。圖層的anchorPoint通過position來控制它的frame的位置穿扳,可以認(rèn)為anchorPoint是用來移動(dòng)圖層的。
? ? 默認(rèn)來說国旷,anchorPoint位于圖層的中點(diǎn)矛物,所以圖層以這個(gè)點(diǎn)為中心放置。anchorPoint屬性并沒有被UIView接口暴露出來跪但,這也是視圖positon屬性被叫做center的原因履羞。但是圖層的anchorPoint,可以被移動(dòng)屡久,比如你把a(bǔ)nchorPoint設(shè)置位于圖層的frame的左上角忆首,于是圖層的內(nèi)容將會(huì)想右下角的positon方法移動(dòng)而不是居中了。
和前面contentsRect以及contentsCenter類似被环,anchorPoint是用單位坐標(biāo)來描述的糙及,也就是圖岑的相對(duì)坐標(biāo),圖層左上角是(0筛欢,0)右下角是(1浸锨,1),因此默認(rèn)是(0.5版姑,0.5)柱搜。anchorPoint可以通過制定x、y小于0或者大于1剥险,使它放置在圖層范圍外聪蘸。 ?
? ? 在圖中我們可以看到當(dāng)anchorPoint變化時(shí)候,postion屬性保持固定值并沒有變,但是frame卻移動(dòng)了健爬。什么時(shí)候需要改變anchorPoint呢控乾?我們舉例說明。創(chuàng)建一個(gè)模擬鬧鐘的項(xiàng)目浑劳。
鬧鐘的組件通過IB來排列阱持,這些圖片嵌套在一個(gè)容器視圖中,并且自動(dòng)調(diào)整和自動(dòng)布局都被禁用了魔熏。這是因?yàn)樽詣?dòng)調(diào)整會(huì)影響到視圖的frame衷咽,當(dāng)視圖旋轉(zhuǎn)時(shí)候,frame是會(huì)發(fā)生變化的蒜绽,這將會(huì)導(dǎo)致一些布局上的失靈镶骗。
鐘面和指針的布局
我們用NSTimer來更新鬧鐘,使用視圖的transform屬性來旋轉(zhuǎn)鐘表躲雅。
@property (nonatomic, weak) IBOutlet UIImageView *hourHand;
@property (nonatomic, weak) IBOutlet UIImageView *minuteHand;
@property (nonatomic, weak) IBOutlet UIImageView *secondHand;
@property (nonatomic, weak) NSTimer *timer;
- (void)viewDidLoad
{
[super viewDidLoad];
//start timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
//set initial hand positions
[self tick];
}
- (void)tick
{
//convert time to hours, minutes and seconds
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
CGFloat hoursAngle = (components.hour / 12.0) * M_PI * 2.0;
//calculate hour hand angle //calculate minute hand angle
CGFloat minsAngle = (components.minute / 60.0) * M_PI * 2.0;
//calculate second hand angle
CGFloat secsAngle = (components.second / 60.0) * M_PI * 2.0;
//rotate hands
self.hourHand.transform = CGAffineTransformMakeRotation(hoursAngle);
self.minuteHand.transform = CGAffineTransformMakeRotation(minsAngle);
self.secondHand.transform = CGAffineTransformMakeRotation(secsAngle);
}
運(yùn)行項(xiàng)目看起來有點(diǎn)兒奇怪鼎姊,因?yàn)殓姳淼闹羔樤趪@中心旋轉(zhuǎn),這并不是是想要的相赁。
怎么解決這個(gè)問題呢相寇?可以在圖片的末尾添加一個(gè)透明空間,但是這樣會(huì)讓圖片變大钮科,也會(huì)消耗更多內(nèi)存唤衫。更好的方案是使用anchorPoint屬性,我們在viewDidload中添加幾行代碼讓鐘表每個(gè)指針的anchorPoint做一些平移
self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
指針向上anchorPoint向下移動(dòng)绵脯。最后效果如下:
個(gè)人感覺anchorPoint就是圖層旋轉(zhuǎn)時(shí)候的旋轉(zhuǎn)軸佳励。
坐標(biāo)系
和視圖一樣,圖層在圖層數(shù)當(dāng)中也是相對(duì)于父圖層按層級(jí)關(guān)系放置蛆挫,一個(gè)圖層的position依賴于它父圖層的bounds赃承,如果父圖層發(fā)生了移動(dòng),它的所有子圖層也會(huì)跟著移動(dòng)悴侵。但是有時(shí)候你需要知道一個(gè)圖層的絕對(duì)位置瞧剖,或者是相對(duì)于另一個(gè)圖層的位置,而不是它當(dāng)前父圖層的位置可免。CAlayer提供了一些轉(zhuǎn)換工具類方法筒繁,這些以conver開頭的方法可以把定義在一個(gè)圖層坐標(biāo)系下的點(diǎn)或者矩形轉(zhuǎn)換成另一個(gè)圖層坐標(biāo)系下的點(diǎn)或者矩形。
翻轉(zhuǎn)的幾何結(jié)構(gòu)
通常說來巴元,在iOS上Original位于父圖層的左上角,但在OSX上驮宴,通常位于坐下角逮刨。CoreAnimation可以通過geometryFlipped屬性來適配這兩種情況,它決定了一個(gè)圖層的坐標(biāo)是否相對(duì)于父圖層垂直翻轉(zhuǎn),是一個(gè)Bool類型修己。在iOS上通過設(shè)置它為YES意味著它的子圖層將會(huì)被垂直翻轉(zhuǎn)恢总,也就是將會(huì)沿著地步排版而不是通暢的頂部。
Z坐標(biāo)軸
和UIView嚴(yán)格的二維坐標(biāo)系不同睬愤,CAlayer存在于一個(gè)三維空間中片仿。除了我們討論過的position和anchorPoint屬性外,CAlayer還有另外兩個(gè)屬性尤辱,zPosition和anchorPointZ砂豌,二者都是在z軸上描述圖層位置的浮點(diǎn)類型。
zPosition屬性在大多數(shù)情況下并不常用光督。在涉及CATransform3D阳距,在三維空間移動(dòng)和旋轉(zhuǎn)圖層會(huì)用到,除此最實(shí)用的功能就是改變圖層的顯示順序了结借。通常筐摘,圖層是根據(jù)它們子圖層的sublayers出現(xiàn)的順序來繪制的,這就是所謂的畫家算法-就像一個(gè)畫家在墻上作畫船老,后繪制的圖層灰遮蓋住之前的圖層咖熟,但是通過增加圖層的zPositon,就可以把圖層向相機(jī)方向前置柳畔,于是它就在所有其他圖層的前面了馍管。這里的相機(jī)實(shí)際上是相對(duì)于用戶的視角,這里和iPhone背面的內(nèi)置相機(jī)沒有任何關(guān)系荸镊。
//move the green view zPosition nearer to the camera
self.greenView.layer.zPosition = 1.0f;
就可以改變圖層的前后順序了咽斧。
Hit Testing
在開始圖層樹證實(shí)了最好使用圖層相關(guān)的視圖,而不是創(chuàng)建獨(dú)立的圖層關(guān)系躬存。其中一個(gè)原因就是要額外處理復(fù)雜的觸摸事件张惹。
CAlayer并不關(guān)心任何響應(yīng)鏈?zhǔn)录圆荒苤苯犹幚碛|摸事件或者手勢岭洲。但是它有一系列的方法幫你處理了事件:-containsPoint:和-hitTest:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get touch position relative to main view
CGPoint point = [[touches anyObject] locationInView:self.view];
//convert point to the white layer's coordinates
point = [self.layerView.layer convertPoint:point fromLayer:self.view.layer];
//get layer using containsPoint:
if ([self.layerView.layer containsPoint:point]) {}
}
-hitTest:方法同樣接受一個(gè)CGPoint類型參數(shù)宛逗,它返回的不是Bool類型,它返回的是圖層本身盾剩,或者包含這個(gè)坐標(biāo)點(diǎn)的葉子節(jié)點(diǎn)圖層雷激。如果這個(gè)點(diǎn)在最外層的范圍之外,則返回nil告私,使用如下
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get touch position
CGPoint point = [[touches anyObject] locationInView:self.view];
//get touched layer
CALayer *layer = [self.layerView.layer hitTest:point];
//get layer using hitTest
if (layer == self.blueLayer) {
[[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer"
message:nil
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
} else if (layer == self.layerView.layer) {
[[[UIAlertView alloc] initWithTitle:@"Inside White Layer"
message:nil
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
}
}
Notes:當(dāng)調(diào)用圖層的-hitTest:方法時(shí)屎暇,測試的順序嚴(yán)格按照?qǐng)D層樹當(dāng)中圖層的順序(和UIView處理事件類似)。之前說的zPosition屬性可以明顯的改變屏幕上圖層的順序驻粟,但不能改變事件傳遞的順序根悼。這意味著如果改變了圖層的zPosition,你會(huì)發(fā)現(xiàn)獎(jiǎng)不能檢測到最前方的視圖點(diǎn)擊事件,這是因?yàn)楸涣硪粋€(gè)圖層遮蓋住了挤巡,雖然前面的圖層zPosition值較小剩彬,但是在圖層順序中靠前。
自動(dòng)布局
你可能用過UIViewAutoresizingMask類型的一些常量矿卑,應(yīng)用于當(dāng)父視圖改變尺寸的時(shí)候喉恋,相對(duì)應(yīng)UIView的frame也跟著更新的場景。當(dāng)使用視圖的時(shí)候母廷,可以充分利用UIView類接口暴露出來的UiViewAutoresizeingMask和NSlayoutConstraint API轻黑,但是如果隨意控制CALayer的布局,就需要手工操作徘意。最簡單的方法就是使用CALayerDelegate:
- (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ú)的圖層來構(gòu)建應(yīng)用的一個(gè)重要原因勤讽。
視覺效果
我們在前一節(jié)圖層幾何學(xué)中討論了圖層的frame蟋座,之前還討論了圖層的寄宿圖。但是圖層不僅僅可以時(shí)圖片或者顏色的容器脚牍,還有一系列內(nèi)建的特性使得創(chuàng)造美麗優(yōu)雅的令人深刻的界面元素稱為可能向臀。這一節(jié)我們學(xué)習(xí)能夠通過CAlayer屬性是心啊的視覺效果。
圓角
圓角矩形在iOS中比較流行诸狭。除了直接使用有圓角的原始圖外券膀,CAlayer的conrnerRadius屬性控制著圖層角的曲率。默認(rèn)情況下這個(gè)曲率值只影響背景顏色而不是背景圖片或者子圖層驯遇。不過芹彬,如果把maskTobounds設(shè)置為YES,圖層理的所有東西都會(huì)被截取叉庐。
self.layerView2.layer.masksToBounds = YES;
? ? ? CALayer另外兩個(gè)非常有用屬性就是borderWidth和borderColor舒帮。二者共同定義了圖層邊的繪制樣式。這條線(也被稱作stroke)沿著圖層的bounds繪制陡叠,同時(shí)也包含圖層的角玩郊。邊框是根據(jù)圖層邊界變化的,而不是圖層里面的內(nèi)容枉阵。
陰影
shadowOpacity屬性的值大于0译红,陰影就可以顯示在任意圖層之下。若要改動(dòng)陰影的表現(xiàn)兴溜,你可以使用CAlayer的另外三個(gè)屬性:shadowColor临庇、shadowOffset和shadowRadius反璃。shadowOffset屬性控制著陰影的方向和距離。它是一個(gè)CGSize的值假夺,默認(rèn)是{0,-3},陰影相對(duì)Y軸有3個(gè)點(diǎn)的向上位移,這個(gè)是因?yàn)镃Alayer的坐標(biāo)和視圖的坐標(biāo)是不同的斋攀,CAlayer的坐標(biāo)是在MacOS上使用的已卷。
shadowRadius屬性控制著陰影的模糊度,當(dāng)它值為0淳蔼,陰影就和視圖一樣有一個(gè)非常確定的邊界線侧蘸,當(dāng)值越大的時(shí)候,邊界線就越來越模糊和自然鹉梨。和圖層的邊框不同讳癌,圖層的陰影繼承自內(nèi)容的外形,而不是根據(jù)邊界和角半徑來確定存皂。為了計(jì)算出陰影的形狀晌坤,CoreAnimation會(huì)將寄宿圖考慮在內(nèi),然后通過這些完美搭配圖層來創(chuàng)建一個(gè)陰影旦袋。陰影通常就是在layer的邊界之外骤菠,如果開啟了masksToBounds屬性,所有從圖層中突出來的內(nèi)容都會(huì)被剪掉疤孕。如果像沿著內(nèi)容裁切而且還有陰影商乎,就需要兩個(gè)圖層:一個(gè)花陰影的空的外圖層,和一個(gè)用masksToBounds裁剪內(nèi)容的內(nèi)圖層祭阀。
實(shí)時(shí)計(jì)算陰影是一個(gè)非常消耗資源的事情鹉戚,尤其是有多個(gè)子圖層的時(shí)候。如果事先知道陰影的形狀专控,可以通過shadowPath來提高性能抹凳。shadowPath是一個(gè)CGPathRef的類型。
//create a square shadow
CGMutablePathRef squarePath = CGPathCreateMutable();
CGPathAddRect(squarePath, NULL, self.layerView1.bounds);
self.layerView1.layer.shadowPath = squarePath; CGPathRelease(squarePath);
//create a circular shadow
CGMutablePathRef circlePath = CGPathCreateMutable();
CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds);
self.layerView2.layer.shadowPath = circlePath; CGPathRelease(circlePath);
圖層蒙版
? ? ? ? 通過maskToBounds屬性踩官,我們可以沿著邊界裁剪圖形却桶;通過cornerRadius屬性,我們可以設(shè)定一個(gè)圓角蔗牡。到那時(shí)有時(shí)候你希望展示的內(nèi)容不實(shí)載一個(gè)矩形或者圓角的矩形颖系。比如,展示一個(gè)有星形框架的圖片辩越,或者讓一些古卷文字慢慢漸變成背景色嘁扼,而不是一個(gè)突兀的邊界。
? ? ? 使用一個(gè)32位的有alpha通道的png圖片通常是創(chuàng)建一個(gè)非矩形視圖最方便的方法黔攒,可以通過指定一個(gè)透明蒙版來實(shí)現(xiàn)趁啸。但是這個(gè)方法不能讓我們通過代碼來實(shí)現(xiàn)蒙版强缘,也不能讓子圖層或者子視圖裁剪成同樣的形狀。
CAlayer有一個(gè)屬性mask可以解決這個(gè)問題不傅,這個(gè)屬性本身就是一個(gè)CAlayer類型旅掂,有和其他圖層一樣繪制和布局的屬性。它類似一個(gè)子圖層访娶,相對(duì)于父圖層布局商虐,但是它卻不是一個(gè)普通的子圖層。不同于那些繪制在父圖層中的子圖層崖疤,mask圖層定義了父圖層的部分可見區(qū)域秘车。
mask圖層的color屬性是無關(guān)要緊的,真正重要的是圖層的輪廓劫哼。mask屬性就像一個(gè)餅干切割機(jī)叮趴,mask圖層實(shí)心的部分會(huì)被保留,其它被舍棄权烧。
如果mask圖層比父圖層小眯亦,只有在mask圖層里面的內(nèi)容才顯示
我們將演示下這個(gè)過程。
@property (nonatomic, weak) IBOutlet UIImageView *imageView;//圖層
- (void)viewDidLoad
{
[super viewDidLoad];
//create mask layer
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = self.layerView.bounds;
//self.layerView是蒙版模型的view
UIImage *maskImage = [UIImage imageNamed:@"Cone.png"];
maskLayer.contents = (__bridge id)maskImage.CGImage;
//apply mask to image layer
self.imageView.layer.mask = maskLayer;
}
CAlayer蒙版不限于靜態(tài)圖豪嚎。任何有圖層構(gòu)成的都可以作為mask屬性搔驼,這意味著蒙版可以通過代碼甚至動(dòng)畫實(shí)時(shí)生成。
拉伸過濾
當(dāng)我們視圖顯示一個(gè)圖片的時(shí)候侈询,都應(yīng)該正確的顯示這個(gè)圖片舌涨。滿足如下條件:
1)能夠顯示最好的畫質(zhì),像素既沒有被壓塑也沒有被拉伸扔字。
2)能更好的使用內(nèi)存囊嘉。3)最好的性能表現(xiàn),CPU不需要為此額外計(jì)算革为。
不過有時(shí)候扭粱,顯示一個(gè)非真實(shí)大小的圖片也是我們需要的效果。比如一個(gè)頭像或者圖片的縮略圖震檩,再比如一個(gè)可以被拖拽和伸縮的大圖琢蛤。當(dāng)圖片需要顯示不同大小的時(shí)候,拉伸過濾的算法就起作用了抛虏,它作用于原圖的像素上并根據(jù)需要生成新的像素顯示在屏幕上博其。重繪圖片大小取決于需要拉伸的內(nèi)容,放大或者縮小的需求迂猴。CAlayer為此提供了三種拉伸過濾常量:kCAFilterLinear慕淡,kCAFilterNearest,kCAFilterTrilinear沸毁。
ninification縮小圖片和magnification放大圖片峰髓,默認(rèn)的過濾器都是kCAFillterLinear良蛮,這個(gè)過濾器采用雙線性濾波算法驯击,他在大多數(shù)情況下都表現(xiàn)良好字币。雙線性濾波算法通過多個(gè)像素取樣最終生成新的值覆积,得到一個(gè)平滑的表現(xiàn)不錯(cuò)的拉伸,但是當(dāng)放大倍數(shù)比較大的時(shí)候圖片就模糊了徐紧。
kCAFilterTrilinear和kCAFilterLinear非常相似个绍,大部分情況下二者都看不出區(qū)別,但是比較kCAFilterLinear浪汪,該三線性濾波算法存儲(chǔ)了多個(gè)大小情況下的圖片,并三位取樣凛虽,同時(shí)結(jié)合大圖和小圖的存儲(chǔ)進(jìn)而得到最后結(jié)果死遭。
總的來說,對(duì)于比較小的圖或者是差異特別明顯凯旋,極少斜線的大圖呀潭,kCAFilterNearest算法會(huì)保留這種差異明顯的特質(zhì)以呈現(xiàn)更好的結(jié)果。但是對(duì)于大多數(shù)的圖尤其是很多斜線或者曲線輪廓的圖片來說至非,kCAFilterNearest算法會(huì)導(dǎo)致更差的結(jié)果钠署,應(yīng)該用線性過濾算法
我們來驗(yàn)證一下,改動(dòng)前面鬧鐘的項(xiàng)目荒椭,用LCD風(fēng)格的數(shù)字顯示谐鼎。我們用簡單的像素字體創(chuàng)造數(shù)字顯示方式。
用簡單的拼合技術(shù)來顯示LCD數(shù)字風(fēng)格的像素字體
用IB放置六個(gè)視圖趣惠,小時(shí)狸棍,分鐘,秒鐘各兩個(gè)味悄;
@interface ViewController ()
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *digitViews;
@property (nonatomic, weak) NSTimer *timer;
@end
- (void)viewDidLoad
{
[super viewDidLoad]; //get spritesheet image
UIImage *digits = [UIImage imageNamed:@"Digits.png"];
//set up digit views
for (UIView *view in self.digitViews) {
//set contents
view.layer.contents = (__bridge id)digits.CGImage;
view.layer.contentsRect = CGRectMake(0, 0, 0.1, 1.0);
view.layer.contentsGravity = kCAGravityResizeAspect;
}
//start timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
//set initial clock time
[self tick];
}
- (void)setDigit:(NSInteger)digit forView:(UIView *)view
{//拼合圖層
//adjust contentsRect to select correct digit
view.layer.contentsRect = CGRectMake(digit * 0.1, 0, 0.1, 1.0);
}
- (void)tick
{
//convert time to hours, minutes and seconds
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier: NSGregorianCalendar];
NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
//set hours
[self setDigit:components.hour / 10 forView:self.digitViews[0]];
[self setDigit:components.hour % 10 forView:self.digitViews[1]];
//set minutes
[self setDigit:components.minute / 10 forView:self.digitViews[2]];
[self setDigit:components.minute % 10 forView:self.digitViews[3]];
//set seconds
[self setDigit:components.second / 10 forView:self.digitViews[4]];
[self setDigit:components.second % 10 forView:self.digitViews[5]];
}
運(yùn)行的效果如下:
為了能夠顯示的更加清晰,通過上面我們知道小圖和沒有斜線的圖侍瑟,kCAFilterNearest顯示效果更好唐片。所以在for循環(huán)中加入:
view.layer.magnificationFilter = kCAFillterNearest;像素放大過濾器。
透明度
? ? ? ? UIView有個(gè)alpha的屬性來去定視圖的透明度涨颜。CAlayer有一個(gè)等同的屬性叫做opacity费韭,這兩個(gè)屬性都是影響子層級(jí)的,也就是說咐低,如果你給一個(gè)圖層設(shè)置了opacity屬性揽思,那么它的子圖層都會(huì)受到影響。
iOS常見的做法是把一個(gè)控件的alpha設(shè)置為0.5见擦,使其看上去呈現(xiàn)不可用狀態(tài)钉汗。對(duì)于獨(dú)立的視圖來說還不錯(cuò)羹令,但是對(duì)一個(gè)有子視圖的控件就又點(diǎn)兒奇怪了。這是由透明度的混合疊加造成的损痰,當(dāng)你顯示一個(gè)50%透明度的圖層時(shí)福侈,圖層的每一個(gè)像素都會(huì)一半顯示自己的顏色另一半顯示下面的顏色。
? ? ? ? 理想狀況下卢未,當(dāng)你設(shè)置了一個(gè)圖層的透明度肪凛,我們希望它包含的整個(gè)圖層樹上一個(gè)整體一樣的透明效果。我們可以通過CAlayer的一個(gè)叫做shouldRasterize屬性來時(shí)現(xiàn)組透明的效果辽社,如果他被設(shè)置為YES伟墙,在應(yīng)用透明度之前,圖層及子圖層都會(huì)被整合成為一個(gè)整體的圖片滴铅,這樣就沒有透明度混合的問題了戳葵。
? ? ? ?為了啟用shouldRasterize屬性,我們設(shè)置了圖層的rasterizationScale屬性汉匙。默認(rèn)情況下拱烁,所有圖層都拉伸1.0,所以使用shouldRasterize屬性噩翠,確保設(shè)置了rasterizetionScale屬性去匹配屏幕戏自,防止出現(xiàn)Retina屏幕像素化的問題。
//創(chuàng)建一個(gè)不透明button伤锚,用來對(duì)比
UIButton *button1 = [self customButton];
button1.center = CGPointMake(50, 150);
[self.containerView addSubview:button1];
//創(chuàng)建一個(gè)變化的button
UIButton *button2 = [self customButton];
button2.center = CGPointMake(250, 150);
button2.alpha = 0.5;
[self.containerView addSubview:button2];
//enable rasterization for the translucent button
button2.layer.shouldRasterize = YES;//組透明
button2.layer.rasterizationScale = [UIScreen mainScreen].scale;變化的拉伸比例擅笔。
這樣button2像一個(gè)整體一樣被設(shè)置了透明度。
變換
1见芹、仿射變換
在圖層幾何學(xué)中剂娄,我們使用了UIView的transform屬性旋轉(zhuǎn)了鐘的指針,但是沒有解釋背后的運(yùn)作原理玄呛。實(shí)際上UIView的transform屬性是一個(gè)CGAffinetransfor的類型阅懦,用來在二維空間做旋轉(zhuǎn),縮放和平移徘铝。CGAffineTransform是一個(gè)可以和二維空間向量例如CGPoint做乘法的3x2矩陣耳胎。
圖中顯示灰色的元素是為了滿足矩陣的乘法規(guī)則添加的信息,不改變運(yùn)算結(jié)果惕它。
當(dāng)圖層應(yīng)用變換矩陣怕午,圖層內(nèi)的一個(gè)點(diǎn)都被相應(yīng)的變換。CGAffineTransform中的仿射的意思是無論變換矩陣用什么值淹魄,圖層中平行的兩條線變換以后仍然保持平行郁惜。
CGAffineTransformMakeRotation(CGFloat angle)//旋轉(zhuǎn)
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)//縮放
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)//平移
UIView可以通過設(shè)置transform屬性做變換,但實(shí)際上它只是封裝了內(nèi)部圖層的變換甲锡。CAlayer也有一個(gè)transform屬性兆蕉,但是它的類型是CATransform3D羽戒,而不是平面CGAffineTransform,圖層對(duì)應(yīng)的屬性是affineTransform虎韵。例如:
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
self.layerView.layer.affineTransform = transform;
混合變換
CoreGraphics 提供一系列的函數(shù)可以在一個(gè)變換的基礎(chǔ)上做更深層次的變換易稠,例如可以做一個(gè)既要縮放又要旋轉(zhuǎn)的變換。
當(dāng)操縱一個(gè)變換時(shí)候包蓝,初始化一個(gè)單位矩陣是很重要的驶社,CGAffineTransformIdentity 提供一個(gè)方便的常量。
最后测萎,如果要混合兩個(gè)已經(jīng)存在的變換矩陣亡电,就可以使用如下方法,在兩個(gè)變換的基礎(chǔ)上創(chuàng)建一個(gè)新的變換:
CGAffineTransformconcat(CGAffineTransform t1硅瞧,t2)逊抡;
下面我們完成一個(gè)組合變換,先縮小50%零酪,再旋轉(zhuǎn)30度,最后像右平移200個(gè)像素拇勃。
//create a new transform初始化一個(gè)單位矩陣
CGAffineTransform transform = CGAffineTransformIdentity;
//scale by 50%
transform = CGAffineTransformScale(transform, 0.5, 0.5);
//rotate by 30 degrees
transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0);
//translate by 200 points
transform = CGAffineTransformTranslate(transform, 200, 0);
self.layerView.layer.affineTransform = transform;//在圖層上應(yīng)用變換四苇。
在實(shí)驗(yàn)中我們發(fā)現(xiàn),圖片向右邊發(fā)生了平移但是沒有200像素方咆,另外還有點(diǎn)兒向下平移月腋。原因在于我們按照順序做了變換,上一個(gè)變換的結(jié)果會(huì)影響之后的變換瓣赂。也就是說順序的改變造成的結(jié)果是不一樣的榆骚,若想分開效果就要用?
CGAffineTransformConcat去結(jié)合生成新的仿射。
3D變換
CG前綴告訴我們CGAffineTransform類型屬于CoreGraphics煌集,是一個(gè)2D繪圖API妓肢,前面我們提到zPositon屬性,可以讓圖層靠近或者用戶視角苫纤,transform屬性(CATransform3D)可以做到這點(diǎn)碉钠,讓圖層在3D空間內(nèi)移動(dòng)或者旋轉(zhuǎn)。
? ?和CGAffineTransform類似卷拘,CATransform3D也是一個(gè)矩陣喊废,不過是一個(gè)4x4的矩陣。
和CGAfineTransform矩陣類似栗弟,CoreAnimation提供了一系列的方法來創(chuàng)建和組合CATransform3D類型的矩陣污筷,和CoreGraphics類似,但是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)
有圖可見,繞Z軸旋轉(zhuǎn)等同于之前二維空間的仿射旋轉(zhuǎn)纫塌,但是圍繞X軸和Y軸的旋轉(zhuǎn)就突破了屏幕的二維空間诊县,并且在用戶視角看來發(fā)生了傾斜。
我們做一個(gè)例子措左。CATransform3DMakeRotation對(duì)視圖內(nèi)的圖層繞Y軸做了45度的旋轉(zhuǎn)依痊,我們可以吧視圖向右傾斜,這樣看得更清晰怎披。
CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
self.layerView.layer.transform = transform;繞Y軸向右
看起來圖層并木有被旋轉(zhuǎn)胸嘁,僅僅是在水平方向上的一個(gè)壓縮,其實(shí)沒錯(cuò)凉逛,視圖看起來更窄是因?yàn)槲覀冊谝粋€(gè)斜向的視角看它性宏,不是透視。
? ? ? ?在真實(shí)世界中状飞,當(dāng)物體遠(yuǎn)離我們的時(shí)候毫胜,由于視角的原因看起來會(huì)變小,理論上說遠(yuǎn)離我們的視圖的邊要比靠近視角的邊跟短诬辈,但實(shí)際上并沒有發(fā)生酵使,而我們當(dāng)前的視角是等距離的,也就是在3D變換中任然保持平行焙糟,和之前提到的仿射變換類似
? ? ? 為了做一些修正口渔,我們需要引入投影變換(又稱作z變換)來對(duì)除了旋轉(zhuǎn)之外的變換矩陣做一些修改,Core Animation并沒有給我們提供設(shè)置透視變換的函數(shù)穿撮,因此我們需要手動(dòng)修改矩陣值搓劫,幸運(yùn)的是,很簡單:
CATransform3D的透視效果通過一個(gè)矩陣中一個(gè)很簡單的元素來控制:m34混巧,m34用于按比例縮放X和Y值來計(jì)算到底離視角多遠(yuǎn)枪向。
m34的默認(rèn)值是0咧党,我們可以通過設(shè)置m34為-1.0/d來應(yīng)用透視效果秘蛔,d代表了,想象中視角相機(jī)和屏幕之間的距離,以像素為單位深员,這個(gè)距離不需要計(jì)算负蠕,估算一個(gè)就好,因?yàn)橐暯窍鄼C(jī)并不存在倦畅,可以根據(jù)效果自由決定遮糖。通常在500-1000之間,減少距離的值會(huì)增強(qiáng)透視效果叠赐。對(duì)視圖應(yīng)用透視的代碼如下:
//create a new transform
CATransform3D transform = CATransform3DIdentity;
//apply perspective
transform.m34 = - 1.0 / 500.0;
//rotate by 45 degrees along the Y axis
transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
//apply to layer
self.layerView.layer.transform = transform;
增加了透視感之后欲账,結(jié)果更像是在空間中3D翻轉(zhuǎn)了,更符合用戶視角芭概。
滅點(diǎn)
在透視角度繪圖的時(shí)候赛不,遠(yuǎn)離相機(jī)視角的物體會(huì)變小,當(dāng)遠(yuǎn)到一個(gè)極限距離的時(shí)候罢洲,它們可能就縮成了一個(gè)點(diǎn)踢故,于是所有物體最后都匯聚消失在同一個(gè)點(diǎn)。在現(xiàn)實(shí)中惹苗,這個(gè)點(diǎn)通常是視圖的中心殿较,于是為了在應(yīng)用中創(chuàng)建擬真效果的透視,這個(gè)點(diǎn)應(yīng)該聚在屏幕中點(diǎn)桩蓉,或者至少包含所有3D對(duì)象的視圖中點(diǎn)斜脂。
CoreAnimation定義了這個(gè)點(diǎn)位于變換圖層anchorPoint,通常初始化時(shí)候在圖層的中心触机,但并不是永遠(yuǎn)都在中心,如果手動(dòng)改變也是會(huì)變的玷或,比如鬧鐘的例子儡首。也就是說,當(dāng)圖層發(fā)生變換時(shí)候偏友,這個(gè)點(diǎn)位置不變的蔬胯,變換的軸線就是這個(gè)點(diǎn)。
當(dāng)改變一個(gè)圖層postion位他,也改變了它的滅點(diǎn)氛濒,做3D變換的時(shí)候要記住這點(diǎn)。當(dāng)視圖通過m34來讓它更加有3D效果鹅髓,應(yīng)該首先把它放倒屏幕中央舞竿,然后通過平移來把它移動(dòng)到指定位置,而不是直接改變它的position窿冯,這樣3D圖層都共享一個(gè)滅點(diǎn)骗奖。
如果有多個(gè)視圖或者圖層,每個(gè)都做3D變換,那就需要分別設(shè)置相同的m34值执桌,并且確保在變換之前都在屏幕中央享受同一個(gè)position鄙皇。CAlayer中又一個(gè)更好的方法。
CAlayer有個(gè)sublayerTransform屬性仰挣。它也是CATransform3D類型伴逸,它能影響到所有的子圖層。這意味著膘壶,我們可以一次性的對(duì)包含這些圖層的容器做變換错蝴,所有的子圖層都自動(dòng)繼承了這個(gè)變換方法。
通過一個(gè)地方設(shè)置透視變換的一個(gè)顯著優(yōu)勢就是香椎,滅點(diǎn)被設(shè)置在容器圖層中點(diǎn)漱竖,不要再對(duì)子圖層分別設(shè)置了。下面是一個(gè)例子
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//apply perspective transform to container
CATransform3D perspective = CATransform3DIdentity;//初始化一個(gè)畜伐,position在中點(diǎn)
perspective.m34 = - 1.0 / 500.0;
//應(yīng)用了sublayerTranform屬性馍惹,保證子layer變換都在同一個(gè)滅點(diǎn)。
self.containerView.layer.sublayerTransform = perspective;
//rotate layerView1 by 45 degrees along the Y axis
CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
self.layerView1.layer.transform = transform1;
//rotate layerView2 by 45 degrees along the Y axis
CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
self.layerView2.layer.transform = transform2;
}
背面
上邊例子中我們都是在正面旋轉(zhuǎn)一定角度看到的玛界,如果旋轉(zhuǎn)180度呢万矾?圖層完全旋轉(zhuǎn)一個(gè)半圓,我們就從背面去看它了慎框。
但這并不是一個(gè)很好的特性,因?yàn)槿绻麍D層包含文本或者其他控件笨枯,用戶看到這些內(nèi)容的鏡像圖片會(huì)很奇怪薪丁,也會(huì)造成資源的浪費(fèi):想象一個(gè)不透明的固體立方體,既然永遠(yuǎn)都看不到這些圖層的背面馅精,為什么要浪費(fèi)GPU來繪制它們严嗜?
CAlayer有個(gè)doubleSided的屬性來控制圖層的背面是否要被繪制。這是一個(gè)BOOL類型洲敢,默認(rèn)為YES漫玄,設(shè)置為NO,那么當(dāng)圖層正面從相機(jī)視角消失的時(shí)候压彭,它背面不會(huì)被重繪睦优。
扁平化圖層
如果對(duì)包含已經(jīng)做過變換的圖層的圖層做反方向的變換會(huì)出現(xiàn)什么呢?
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *outerView;
@property (nonatomic, weak) IBOutlet UIView *innerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//rotate the outer layer 45 degrees
CATransform3D outer = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);
self.outerView.layer.transform = outer;
//rotate the inner layer -45 degrees
CATransform3D inner = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1);
self.innerView.layer.transform = inner;
}
@end
在2D平面轉(zhuǎn)換結(jié)果和我們想的一樣壮不。
如果在3D情況下再試一個(gè)汗盘。修改代碼,讓內(nèi)外兩個(gè)視圖繞Y軸询一,而不是z軸衡未,再加上透視效果尸执。注意不能用sublayerTransform,因?yàn)閮?nèi)部圖層并不直接是容器圖層的子圖層缓醋,所以要分別對(duì)圖層設(shè)置透視變換如失。
//讓outer layer繞著Y軸45度
CATransform3D outer = CATransform3DIdentity;
outer.m34 = -1.0 / 500.0;
outer = CATransform3DRotate(outer, M_PI_4, 0, 1, 0);
self.outerView.layer.transform = outer;
//rotate the inner layer -45 degrees:內(nèi)圖層繞著Y軸-45度
CATransform3D inner = CATransform3DIdentity;
inner.m34 = -1.0 / 500.0;
inner = CATransform3DRotate(inner, -M_PI_4, 0, 1, 0);
self.innerView.layer.transform = inner;
我們預(yù)期效果如下:
但我們看到的卻不是這樣的,我們看到的實(shí)際畫面是下面這樣送粱,內(nèi)部圖層仍然向左旋轉(zhuǎn)褪贵,并發(fā)生了扭曲:
這是由于CA圖層盡管都存在于3D空間中,但是不同的圖層不都存在于同一個(gè)3D空間抗俄。每一個(gè)圖層的3D場景其實(shí)都是扁平化的脆丁。當(dāng)你從正面觀察一個(gè)圖層,看到的實(shí)際上由子圖層創(chuàng)建的想象出來的3D場景动雹,但當(dāng)我們傾斜這個(gè)圖層槽卫,你會(huì)發(fā)現(xiàn)這個(gè)3D場景僅僅是被繪制在圖層的表面上。
? ? ? ?類似的胰蝠,當(dāng)你在玩一個(gè)3D游戲歼培,實(shí)際上僅僅是把屏幕做了一次傾斜,或許在游戲中可以看見有一面墻在你面前茸塞,但是傾斜屏幕并不能夠看見墻里面的東西躲庄。所有場景里面繪制的東西并不會(huì)隨著你觀察它的角度改變而發(fā)生變化;圖層也是同樣的道理钾虐。
? ? ? ?這使得用Core Animation創(chuàng)建非常復(fù)雜的3D場景變得十分困難噪窘。你不能夠使用圖層樹去創(chuàng)建一個(gè)3D結(jié)構(gòu)的層級(jí)關(guān)系--在相同場景下的任何3D表面必須和同樣的圖層保持一致,這是因?yàn)槊總€(gè)的父視圖都把它的子視圖扁平化了效扫。
至少當(dāng)你用正常的CALayer的時(shí)候是這樣倔监,CALayer有一個(gè)叫做CATransformLayer的子類來解決這個(gè)問題。
另開一篇介紹這個(gè)“iOS-CATransformlayer”菌仁。