iOS -圖層樹視圖與層的關(guān)系

Core Animation其實是一個令人誤解的命名。你可能認為它只是用來做動畫的,但實際上它是從一個叫做Layer Kit這么一個不怎么和動畫有關(guān)的名字演變而來,所以做動畫這只是Core Animation特性的冰山一角。Core Animation是一個復合引擎,它的職責就是盡可能快地組合屏幕上不同的可視內(nèi)容,這個內(nèi)容是被分解成獨立的圖層,存儲在一個叫做圖層樹的體系之中。于是這個樹形成了UIKit以及在iOS應用程序當中你所能在屏幕上看見的一切的基礎(chǔ)。

1、圖層與視圖。

如果你曾經(jīng)在iOS或者Mac OS平臺上寫過應用程序,你可能會對視圖的概念比較熟悉。一個視圖就是在屏幕上顯示的一個矩形塊(比如圖片,文字或者視頻),它能夠攔截類似于鼠標點擊或者觸摸手勢等用戶輸入。視圖在層級關(guān)系中可以互相嵌套,一個視圖可以管理它的所有子視圖的位置。

在iOS當中,所有的視圖都從一個叫做UIVIew的基類派生而來,UIView可以處理觸摸事件,可以支持基于Core Graphics繪圖,可以做仿射變換(例如旋轉(zhuǎn)或者縮放),或者簡單的類似于滑動或者漸變的動畫

CALayer類在概念上和UIView類似,同樣也是一些被層級關(guān)系樹管理的矩形塊,同樣也可以包含一些內(nèi)容(像圖片,文本或者背景色),管理子圖層的位置。它們有一些方法和屬性用來做動畫和變換。和UIView最大的不同是CALayer不處理用戶的交互。

CALayer并不清楚具體的響應鏈(iOS通過視圖層級關(guān)系用來傳送觸摸事件的機制),于是它并不能夠響應事件,即使它提供了一些方法來判斷是否一個觸點在圖層的范圍之內(nèi)(具體見第三章,“圖層的幾何學”)


每一個UIview都有一個CALayer實例的圖層屬性,也就是所謂的backing layer,視圖的職責就是創(chuàng)建并管理這個圖層,以確保當子視圖在層級關(guān)系中添加或者被移除的時候,他們關(guān)聯(lián)的圖層也同樣對應在層級關(guān)系樹當中有相同的操作

實際上這些背后關(guān)聯(lián)的圖層才是真正用來在屏幕上顯示和做動畫,UIView僅僅是對它的一個封裝,提供了一些iOS類似于處理觸摸的具體功能,以及Core Animation底層方法的高級接口

但是為什么iOS要基于UIView和CALayer提供兩個平行的層級關(guān)系呢?為什么不用一個簡單的層級來處理所有事情呢?原因在于要做職責分離,這樣也能避免很多重復代碼。在iOS和Mac OS兩個平臺上,事件和用戶交互有很多地方的不同,基于多點觸控的用戶界面和基于鼠標鍵盤有著本質(zhì)的區(qū)別,這就是為什么iOS有UIKit和UIView,但是Mac OS有AppKit和NSView的原因。他們功能上很相似,但是在實現(xiàn)上有著顯著的區(qū)別。

1.2 圖層的能力

如果說CALayer是UIView內(nèi)部實現(xiàn)細節(jié),那我們?yōu)槭裁匆娴亓私馑??蘋果當然為我們提供了優(yōu)美簡潔的UIView接口,那么我們是否就沒必要直接去處理Core Animation的細節(jié)了呢?

某種意義上說的確是這樣,對一些簡單的需求來說,我們確實沒必要處理CALayer,因為蘋果已經(jīng)通過UIView的高級API間接地使得動畫變得很簡單。

但是這種簡單會不可避免地帶來一些靈活上的缺陷。如果你略微想在底層做一些改變,或者使用一些蘋果沒有在UIView上實現(xiàn)的接口功能,這時除了介入Core Animation底層之外別無選擇。

我們已經(jīng)證實了圖層不能像視圖那樣處理觸摸事件,那么他能做哪些視圖不能做的呢?這里有一些UIView沒有暴露出來的CALayer的功能:陰影,圓角,帶顏色的邊框;3D變換;非矩形范圍,透明遮罩、多級非線性動畫。

一個視圖只有一個相關(guān)聯(lián)的圖層(自動創(chuàng)建),同時它也可以支持添加無數(shù)多個子圖層,你可以顯示創(chuàng)建一個單獨的圖層,并且把它直接添加到視圖關(guān)聯(lián)圖層的子圖層。盡管可以這樣添加圖層,但往往我們只是見簡單地處理視圖,他們關(guān)聯(lián)的圖層并不需要額外地手動添加子圖層。

使用圖層關(guān)聯(lián)的視圖而不是CALayer的好處在于,你能在使用所有CALayer底層特性的同時,也可以使用UIView的高級API(比如自動排版,布局和事件處理)

然而,當滿足以下條件的時候,你可能更需要使用CALayer而不是UIView:

1)、開發(fā)同時可以在Mac OS上運行的跨平臺應用

2). 使用多種CALayer的子類,并且不想創(chuàng)建額外的UIView去包封裝它們所有;

但是這些例子都很少見,總的來說,處理視圖會比單獨處理圖層更加方便

使用圖層

在屏幕中創(chuàng)建一個UIView對象正方形的,我們要在這個view上添加一個藍色的色塊。我們可以添加一個子view用代碼或者IB都OK。但是這里我們準備使用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];

每一個視圖只有一個相關(guān)聯(lián)的圖層,可以在這個自動創(chuàng)建的圖層上添加無數(shù)多的字圖層。

寄宿圖

寄宿圖就是指在CAlayer中包含的圖。

對寄宿圖的處理主要在layer的contents屬性上,可以參考文章:iOS-CAlayer之contents 屬性。給contents賦值CGImage并不是唯一的設(shè)置寄宿圖的方法。我們可以通過CoreGraphics直接繪制寄宿圖。可以通過重寫drawRect方法來繪制。

-drawRect:方法沒有默認的實現(xiàn),因為對UIView來說,寄宿圖并不是必須的,它不在意是單調(diào)的顏色還是一個圖片的實例。如果UIView檢測到-drawRect:方法被調(diào)用了,它就會為視圖分配一個寄宿圖,這個寄宿圖的像素尺寸就是視圖大小乘以contentsScale。如果你不需要激素圖,那就不要這個方法了,這會造成CPU資源和內(nèi)存的浪費。這就是如果沒有自定義的繪制任務(wù),不要重寫-drawRect:方法。

? ? ?當視圖在屏幕出現(xiàn)的時候-drawRect:方法就會自動調(diào)用。-drawRect:方法里的代碼利用coregraphics去繪制一個寄宿圖,然后內(nèi)容就會被緩存起來直到它需要被更新(通常是因為開發(fā)者調(diào)用-setNeedsDisplay方法,盡管影響到表現(xiàn)效果的屬性值被更改時,一些視圖類型會被自動重繪,如bounds屬性)。

CAlayer有一個可選的delegate屬性,實現(xiàn)了CAlayerDelegate協(xié)議,當CAlayer需要一個內(nèi)容特定的信息時,就會從協(xié)議中請求。CAlayerDelegate是一個非正式協(xié)議,沒有CAlayerDelegate可以在類中引用。

當被要求重繪時,CAlayer會請求它的代理給它一個寄宿圖來顯示。它通過下面這個方法做到的:

-(void)displayLayer:(CAlayer*)layer;

我們就可以在方法中直接設(shè)置contents屬性,如果代理不實現(xiàn)此方法,CAlayer的代理就會嘗試調(diào)用下面這個方法:

-(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,當CAlayer顯示在屏幕上時,CALayer不會自動重繪它的內(nèi)容。它把重繪的決定權(quán)交給了developer。

? ? ? ? ? ?2、盡管我們沒有用maskToBounds屬性,繪制的那個圓仍然沿邊界被裁剪了,這是因為當你使用CAlayerDelegate繪制激素圖的時候,比沒有對超出layer邊界的內(nèi)容提供繪制支持。

現(xiàn)在我們理解并知道怎么使用CAlayerDelegate,但是除非你創(chuàng)建一個單獨的圖層,你幾乎沒有機會用奧CAlayerDelegate協(xié)議。因為當UIView創(chuàng)建它自己的宿主layer的時候,它自動的就把layer的delegate設(shè)置為它自己,并提供了-displayer:的實現(xiàn)。

當使用寄宿圖層的時候,你也不必實現(xiàn)CALayerDelegate的方法。通常都是重寫-drawRect:方法,UIView就會幫你完成剩下的工作,包括重繪時候調(diào)用-display方法。

? ? ?這一小節(jié)介紹了寄宿圖和一些相關(guān)屬性。學到了如何顯示和放置圖片,使用拼合技術(shù)來顯示,以及用CAlayerDelegate 和coreGraphics繪制圖層的。

圖層幾何學

在上一節(jié)中,我們介紹了圖層背后的圖片,和一些控制圖層的坐標和旋轉(zhuǎn)的屬性,現(xiàn)在我們要學習如何根據(jù)父圖層和兄弟圖層來控制位置和尺寸,如何管理圖層的幾何結(jié)構(gòu),以及它們是如何被自動調(diào)整和自動布局影響的。

1、UIView有三個比較重要的布局屬性:frame,bounds,center;CAlayer對應的叫做frame,bounds和position。為了區(qū)分清楚,layer用position,Uiview用center它們都是代表同樣的值。

frame代表了圖層的外部坐標也就是在父圖層上占據(jù)的空間,bounds時內(nèi)部坐標({0,0}通常是圖層的左上角),center和position都代表了相對于父圖層anchorPoint:錨點所在的位置。現(xiàn)在把它想成圖層的重點就好。

UIView和CAlayer的坐標系

? ? ? ? 視圖的frame、bounds、center屬性僅僅是存取方法,當操作視圖frame,實際上是在改變位于視圖下方CAlayer的frame,不能獨立于圖層之外改變視圖的frame。

對于視圖或者圖層來說,frame并不是一個非常清晰的屬性,他是一個虛擬的屬性,是根據(jù)bounds,positon和transform計算而來,所以當其中任何一餓值發(fā)生變化,frame都會變化。相反,改變frame的值一樣會影響bounds、tranform的值。

當圖層做變換的時候,比如旋轉(zhuǎn)或者縮放,frame實際上代表了覆蓋在圖層旋轉(zhuǎn)之后的整個軸對齊的矩形區(qū)域,也就是frame的寬高和bounds的寬高不再一致了。

旋轉(zhuǎn)一個視圖或者圖層之后的frame

上文提到過視圖的center和圖層的position都是指定了一個anchorPoint相對于父圖層的位置。圖層的anchorPoint通過position來控制它的frame的位置,可以認為anchorPoint是用來移動圖層的。

? ? 默認來說,anchorPoint位于圖層的中點,所以圖層以這個點為中心放置。anchorPoint屬性并沒有被UIView接口暴露出來,這也是視圖positon屬性被叫做center的原因。但是圖層的anchorPoint,可以被移動,比如你把anchorPoint設(shè)置位于圖層的frame的左上角,于是圖層的內(nèi)容將會想右下角的positon方法移動而不是居中了。

改變了anchorPoint效果

和前面contentsRect以及contentsCenter類似,anchorPoint是用單位坐標來描述的,也就是圖岑的相對坐標,圖層左上角是(0,0)右下角是(1,1),因此默認是(0.5,0.5)。anchorPoint可以通過制定x、y小于0或者大于1,使它放置在圖層范圍外。 ?

? ? 在圖中我們可以看到當anchorPoint變化時候,postion屬性保持固定值并沒有變,但是frame卻移動了。什么時候需要改變anchorPoint呢?我們舉例說明。創(chuàng)建一個模擬鬧鐘的項目。

鐘面和三個指針

鬧鐘的組件通過IB來排列,這些圖片嵌套在一個容器視圖中,并且自動調(diào)整和自動布局都被禁用了。這是因為自動調(diào)整會影響到視圖的frame,當視圖旋轉(zhuǎn)時候,frame是會發(fā)生變化的,這將會導致一些布局上的失靈。

鐘面和指針的布局

我們用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);

}

運行項目看起來有點兒奇怪,因為鐘表的指針在圍繞中心旋轉(zhuǎn),這并不是是想要的。

鐘面和不對齊的鐘指針

怎么解決這個問題呢?可以在圖片的末尾添加一個透明空間,但是這樣會讓圖片變大,也會消耗更多內(nèi)存。更好的方案是使用anchorPoint屬性,我們在viewDidload中添加幾行代碼讓鐘表每個指針的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向下移動。最后效果如下:

調(diào)整后的鐘表圖片

個人感覺anchorPoint就是圖層旋轉(zhuǎn)時候的旋轉(zhuǎn)軸。

坐標系

和視圖一樣,圖層在圖層數(shù)當中也是相對于父圖層按層級關(guān)系放置,一個圖層的position依賴于它父圖層的bounds,如果父圖層發(fā)生了移動,它的所有子圖層也會跟著移動。但是有時候你需要知道一個圖層的絕對位置,或者是相對于另一個圖層的位置,而不是它當前父圖層的位置。CAlayer提供了一些轉(zhuǎn)換工具類方法,這些以conver開頭的方法可以把定義在一個圖層坐標系下的點或者矩形轉(zhuǎn)換成另一個圖層坐標系下的點或者矩形。

翻轉(zhuǎn)的幾何結(jié)構(gòu)

通常說來,在iOS上Original位于父圖層的左上角,但在OSX上,通常位于坐下角。CoreAnimation可以通過geometryFlipped屬性來適配這兩種情況,它決定了一個圖層的坐標是否相對于父圖層垂直翻轉(zhuǎn),是一個Bool類型。在iOS上通過設(shè)置它為YES意味著它的子圖層將會被垂直翻轉(zhuǎn),也就是將會沿著地步排版而不是通暢的頂部。

Z坐標軸

和UIView嚴格的二維坐標系不同,CAlayer存在于一個三維空間中。除了我們討論過的position和anchorPoint屬性外,CAlayer還有另外兩個屬性,zPosition和anchorPointZ,二者都是在z軸上描述圖層位置的浮點類型。

zPosition屬性在大多數(shù)情況下并不常用。在涉及CATransform3D,在三維空間移動和旋轉(zhuǎn)圖層會用到,除此最實用的功能就是改變圖層的顯示順序了。通常,圖層是根據(jù)它們子圖層的sublayers出現(xiàn)的順序來繪制的,這就是所謂的畫家算法-就像一個畫家在墻上作畫,后繪制的圖層灰遮蓋住之前的圖層,但是通過增加圖層的zPositon,就可以把圖層向相機方向前置,于是它就在所有其他圖層的前面了。這里的相機實際上是相對于用戶的視角,這里和iPhone背面的內(nèi)置相機沒有任何關(guān)系。

//move the green view zPosition nearer to the camera

self.greenView.layer.zPosition = 1.0f;

就可以改變圖層的前后順序了。

Hit Testing

在開始圖層樹證實了最好使用圖層相關(guān)的視圖,而不是創(chuàng)建獨立的圖層關(guān)系。其中一個原因就是要額外處理復雜的觸摸事件。

CAlayer并不關(guā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:方法同樣接受一個CGPoint類型參數(shù),它返回的不是Bool類型,它返回的是圖層本身,或者包含這個坐標點的葉子節(jié)點圖層。如果這個點在最外層的范圍之外,則返回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:當調(diào)用圖層的-hitTest:方法時,測試的順序嚴格按照圖層樹當中圖層的順序(和UIView處理事件類似)。之前說的zPosition屬性可以明顯的改變屏幕上圖層的順序,但不能改變事件傳遞的順序。這意味著如果改變了圖層的zPosition,你會發(fā)現(xiàn)獎不能檢測到最前方的視圖點擊事件,這是因為被另一個圖層遮蓋住了,雖然前面的圖層zPosition值較小,但是在圖層順序中靠前。

自動布局

你可能用過UIViewAutoresizingMask類型的一些常量,應用于當父視圖改變尺寸的時候,相對應UIView的frame也跟著更新的場景。當使用視圖的時候,可以充分利用UIView類接口暴露出來的UiViewAutoresizeingMask和NSlayoutConstraint API,但是如果隨意控制CALayer的布局,就需要手工操作。最簡單的方法就是使用CALayerDelegate:

- (void)layoutSublayersOfLayer:(CALayer *)layer;

當圖層bounds發(fā)生改變,或者圖層的-setNeedslayout方法被調(diào)用的時候,這個函數(shù)將會執(zhí)行。這使得你可以手動地重新擺放或者重新調(diào)整子圖層的大小,但是不能像UIView的autoresizingMask和constraints屬性做到自適應屏幕旋轉(zhuǎn)。

這也是最好使用視圖而不是單獨的圖層來構(gòu)建應用的一個重要原因。

視覺效果

我們在前一節(jié)圖層幾何學中討論了圖層的frame,之前還討論了圖層的寄宿圖。但是圖層不僅僅可以時圖片或者顏色的容器,還有一系列內(nèi)建的特性使得創(chuàng)造美麗優(yōu)雅的令人深刻的界面元素稱為可能。這一節(jié)我們學習能夠通過CAlayer屬性是心啊的視覺效果。

圓角

圓角矩形在iOS中比較流行。除了直接使用有圓角的原始圖外,CAlayer的conrnerRadius屬性控制著圖層角的曲率。默認情況下這個曲率值只影響背景顏色而不是背景圖片或者子圖層。不過,如果把maskTobounds設(shè)置為YES,圖層理的所有東西都會被截取。

self.layerView2.layer.masksToBounds = YES;

? ? ? CALayer另外兩個非常有用屬性就是borderWidth和borderColor。二者共同定義了圖層邊的繪制樣式。這條線(也被稱作stroke)沿著圖層的bounds繪制,同時也包含圖層的角。邊框是根據(jù)圖層邊界變化的,而不是圖層里面的內(nèi)容。

陰影

shadowOpacity屬性的值大于0,陰影就可以顯示在任意圖層之下。若要改動陰影的表現(xiàn),你可以使用CAlayer的另外三個屬性:shadowColor、shadowOffset和shadowRadius。shadowOffset屬性控制著陰影的方向和距離。它是一個CGSize的值,默認是{0,-3},陰影相對Y軸有3個點的向上位移,這個是因為CAlayer的坐標和視圖的坐標是不同的,CAlayer的坐標是在MacOS上使用的。

shadowRadius屬性控制著陰影的模糊度,當它值為0,陰影就和視圖一樣有一個非常確定的邊界線,當值越大的時候,邊界線就越來越模糊和自然。和圖層的邊框不同,圖層的陰影繼承自內(nèi)容的外形,而不是根據(jù)邊界和角半徑來確定。為了計算出陰影的形狀,CoreAnimation會將寄宿圖考慮在內(nèi),然后通過這些完美搭配圖層來創(chuàng)建一個陰影。陰影通常就是在layer的邊界之外,如果開啟了masksToBounds屬性,所有從圖層中突出來的內(nèi)容都會被剪掉。如果像沿著內(nèi)容裁切而且還有陰影,就需要兩個圖層:一個花陰影的空的外圖層,和一個用masksToBounds裁剪內(nèi)容的內(nèi)圖層。

實時計算陰影是一個非常消耗資源的事情,尤其是有多個子圖層的時候。如果事先知道陰影的形狀,可以通過shadowPath來提高性能。shadowPath是一個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è)定一個圓角。到那時有時候你希望展示的內(nèi)容不實載一個矩形或者圓角的矩形。比如,展示一個有星形框架的圖片,或者讓一些古卷文字慢慢漸變成背景色,而不是一個突兀的邊界。

? ? ? 使用一個32位的有alpha通道的png圖片通常是創(chuàng)建一個非矩形視圖最方便的方法,可以通過指定一個透明蒙版來實現(xiàn)。但是這個方法不能讓我們通過代碼來實現(xiàn)蒙版,也不能讓子圖層或者子視圖裁剪成同樣的形狀。

CAlayer有一個屬性mask可以解決這個問題,這個屬性本身就是一個CAlayer類型,有和其他圖層一樣繪制和布局的屬性。它類似一個子圖層,相對于父圖層布局,但是它卻不是一個普通的子圖層。不同于那些繪制在父圖層中的子圖層,mask圖層定義了父圖層的部分可見區(qū)域。

mask圖層的color屬性是無關(guān)要緊的,真正重要的是圖層的輪廓。mask屬性就像一個餅干切割機,mask圖層實心的部分會被保留,其它被舍棄。

如果mask圖層比父圖層小,只有在mask圖層里面的內(nèi)容才顯示

圖片和蒙版圖層作用一起的效果

我們將演示下這個過程。

@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屬性,這意味著蒙版可以通過代碼甚至動畫實時生成。

拉伸過濾

當我們視圖顯示一個圖片的時候,都應該正確的顯示這個圖片。滿足如下條件:

1)能夠顯示最好的畫質(zhì),像素既沒有被壓塑也沒有被拉伸。

2)能更好的使用內(nèi)存。3)最好的性能表現(xiàn),CPU不需要為此額外計算。

不過有時候,顯示一個非真實大小的圖片也是我們需要的效果。比如一個頭像或者圖片的縮略圖,再比如一個可以被拖拽和伸縮的大圖。當圖片需要顯示不同大小的時候,拉伸過濾的算法就起作用了,它作用于原圖的像素上并根據(jù)需要生成新的像素顯示在屏幕上。重繪圖片大小取決于需要拉伸的內(nèi)容,放大或者縮小的需求。CAlayer為此提供了三種拉伸過濾常量:kCAFilterLinear,kCAFilterNearest,kCAFilterTrilinear。

ninification縮小圖片和magnification放大圖片,默認的過濾器都是kCAFillterLinear,這個過濾器采用雙線性濾波算法,他在大多數(shù)情況下都表現(xiàn)良好。雙線性濾波算法通過多個像素取樣最終生成新的值,得到一個平滑的表現(xiàn)不錯的拉伸,但是當放大倍數(shù)比較大的時候圖片就模糊了。

kCAFilterTrilinear和kCAFilterLinear非常相似,大部分情況下二者都看不出區(qū)別,但是比較kCAFilterLinear,該三線性濾波算法存儲了多個大小情況下的圖片,并三位取樣,同時結(jié)合大圖和小圖的存儲進而得到最后結(jié)果。

總的來說,對于比較小的圖或者是差異特別明顯,極少斜線的大圖,kCAFilterNearest算法會保留這種差異明顯的特質(zhì)以呈現(xiàn)更好的結(jié)果。但是對于大多數(shù)的圖尤其是很多斜線或者曲線輪廓的圖片來說,kCAFilterNearest算法會導致更差的結(jié)果,應該用線性過濾算法

我們來驗證一下,改動前面鬧鐘的項目,用LCD風格的數(shù)字顯示。我們用簡單的像素字體創(chuàng)造數(shù)字顯示方式。

存儲在本地的數(shù)字圖片

用簡單的拼合技術(shù)來顯示LCD數(shù)字風格的像素字體

用IB放置六個視圖,小時,分鐘,秒鐘各兩個;

@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]];

}

運行的效果如下:

有點模糊,是由默認的kCAFilterLinear引起的

為了能夠顯示的更加清晰,通過上面我們知道小圖和沒有斜線的圖,kCAFilterNearest顯示效果更好。所以在for循環(huán)中加入:

view.layer.magnificationFilter = kCAFillterNearest;像素放大過濾器。


設(shè)置過濾之后的清晰顯示

透明度

? ? ? ? UIView有個alpha的屬性來去定視圖的透明度。CAlayer有一個等同的屬性叫做opacity,這兩個屬性都是影響子層級的,也就是說,如果你給一個圖層設(shè)置了opacity屬性,那么它的子圖層都會受到影響。

iOS常見的做法是把一個控件的alpha設(shè)置為0.5,使其看上去呈現(xiàn)不可用狀態(tài)。對于獨立的視圖來說還不錯,但是對一個有子視圖的控件就又點兒奇怪了。這是由透明度的混合疊加造成的,當你顯示一個50%透明度的圖層時,圖層的每一個像素都會一半顯示自己的顏色另一半顯示下面的顏色。

? ? ? ? 理想狀況下,當你設(shè)置了一個圖層的透明度,我們希望它包含的整個圖層樹上一個整體一樣的透明效果。我們可以通過CAlayer的一個叫做shouldRasterize屬性來時現(xiàn)組透明的效果,如果他被設(shè)置為YES,在應用透明度之前,圖層及子圖層都會被整合成為一個整體的圖片,這樣就沒有透明度混合的問題了。

? ? ? ?為了啟用shouldRasterize屬性,我們設(shè)置了圖層的rasterizationScale屬性。默認情況下,所有圖層都拉伸1.0,所以使用shouldRasterize屬性,確保設(shè)置了rasterizetionScale屬性去匹配屏幕,防止出現(xiàn)Retina屏幕像素化的問題。

//創(chuàng)建一個不透明button,用來對比

UIButton *button1 = [self customButton];

button1.center = CGPointMake(50, 150);

[self.containerView addSubview:button1];

//創(chuàng)建一個變化的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像一個整體一樣被設(shè)置了透明度。

變換

1、仿射變換

在圖層幾何學中,我們使用了UIView的transform屬性旋轉(zhuǎn)了鐘的指針,但是沒有解釋背后的運作原理。實際上UIView的transform屬性是一個CGAffinetransfor的類型,用來在二維空間做旋轉(zhuǎn),縮放和平移。CGAffineTransform是一個可以和二維空間向量例如CGPoint做乘法的3x2矩陣。

用矩陣表示CGPoint和CGAffineTransform的關(guān)系

圖中顯示灰色的元素是為了滿足矩陣的乘法規(guī)則添加的信息,不改變運算結(jié)果。

當圖層應用變換矩陣,圖層內(nèi)的一個點都被相應的變換。CGAffineTransform中的仿射的意思是無論變換矩陣用什么值,圖層中平行的兩條線變換以后仍然保持平行。

CGAffineTransformMakeRotation(CGFloat angle)//旋轉(zhuǎn)

CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)//縮放

CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)//平移

UIView可以通過設(shè)置transform屬性做變換,但實際上它只是封裝了內(nèi)部圖層的變換。CAlayer也有一個transform屬性,但是它的類型是CATransform3D,而不是平面CGAffineTransform,圖層對應的屬性是affineTransform。例如:

CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);

self.layerView.layer.affineTransform = transform;

混合變換

CoreGraphics 提供一系列的函數(shù)可以在一個變換的基礎(chǔ)上做更深層次的變換,例如可以做一個既要縮放又要旋轉(zhuǎn)的變換。

當操縱一個變換時候,初始化一個單位矩陣是很重要的,CGAffineTransformIdentity 提供一個方便的常量。

最后,如果要混合兩個已經(jīng)存在的變換矩陣,就可以使用如下方法,在兩個變換的基礎(chǔ)上創(chuàng)建一個新的變換:

CGAffineTransformconcat(CGAffineTransform t1,t2);

下面我們完成一個組合變換,先縮小50%,再旋轉(zhuǎn)30度,最后像右平移200個像素。

//create a new transform初始化一個單位矩陣

CGAffineTransform transform = CGAffineTransformIdentity;

//scale by 50%

transform = CGAffineTransformScale(transform, 0.5, 0.5);

//rotate by 30 degrees

transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0);

//translate by 200 points

transform = CGAffineTransformTranslate(transform, 200, 0);

self.layerView.layer.affineTransform = transform;//在圖層上應用變換。

在實驗中我們發(fā)現(xiàn),圖片向右邊發(fā)生了平移但是沒有200像素,另外還有點兒向下平移。原因在于我們按照順序做了變換,上一個變換的結(jié)果會影響之后的變換。也就是說順序的改變造成的結(jié)果是不一樣的,若想分開效果就要用?

CGAffineTransformConcat去結(jié)合生成新的仿射。

3D變換

CG前綴告訴我們CGAffineTransform類型屬于CoreGraphics,是一個2D繪圖API,前面我們提到zPositon屬性,可以讓圖層靠近或者用戶視角,transform屬性(CATransform3D)可以做到這點,讓圖層在3D空間內(nèi)移動或者旋轉(zhuǎn)。

? ?和CGAffineTransform類似,CATransform3D也是一個矩陣,不過是一個4x4的矩陣。

和CGAfineTransform矩陣類似,CoreAnimation提供了一系列的方法來創(chuàng)建和組合CATransform3D類型的矩陣,和CoreGraphics類似,但是3D的平移和旋轉(zhuǎn)多了一個z參數(shù),旋轉(zhuǎn)的函數(shù)除了angle之外,多了x,y,z三個參數(shù),分別決定了每個坐標軸方向的旋轉(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)

x、y、z、軸,以及圍繞它們的方向

有圖可見,繞Z軸旋轉(zhuǎn)等同于之前二維空間的仿射旋轉(zhuǎn),但是圍繞X軸和Y軸的旋轉(zhuǎn)就突破了屏幕的二維空間,并且在用戶視角看來發(fā)生了傾斜。

我們做一個例子。CATransform3DMakeRotation對視圖內(nèi)的圖層繞Y軸做了45度的旋轉(zhuǎn),我們可以吧視圖向右傾斜,這樣看得更清晰。

CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);

self.layerView.layer.transform = transform;繞Y軸向右

看起來圖層并木有被旋轉(zhuǎn),僅僅是在水平方向上的一個壓縮,其實沒錯,視圖看起來更窄是因為我們在一個斜向的視角看它,不是透視。

? ? ? ?在真實世界中,當物體遠離我們的時候,由于視角的原因看起來會變小,理論上說遠離我們的視圖的邊要比靠近視角的邊跟短,但實際上并沒有發(fā)生,而我們當前的視角是等距離的,也就是在3D變換中任然保持平行,和之前提到的仿射變換類似

? ? ? 為了做一些修正,我們需要引入投影變換(又稱作z變換)來對除了旋轉(zhuǎn)之外的變換矩陣做一些修改,Core Animation并沒有給我們提供設(shè)置透視變換的函數(shù),因此我們需要手動修改矩陣值,幸運的是,很簡單:

CATransform3D的透視效果通過一個矩陣中一個很簡單的元素來控制:m34,m34用于按比例縮放X和Y值來計算到底離視角多遠。

m34元素,用來做透視

m34的默認值是0,我們可以通過設(shè)置m34為-1.0/d來應用透視效果,d代表了,想象中視角相機和屏幕之間的距離,以像素為單位,這個距離不需要計算,估算一個就好,因為視角相機并不存在,可以根據(jù)效果自由決定。通常在500-1000之間,減少距離的值會增強透視效果。對視圖應用透視的代碼如下:

//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)了,更符合用戶視角。

滅點

在透視角度繪圖的時候,遠離相機視角的物體會變小,當遠到一個極限距離的時候,它們可能就縮成了一個點,于是所有物體最后都匯聚消失在同一個點。在現(xiàn)實中,這個點通常是視圖的中心,于是為了在應用中創(chuàng)建擬真效果的透視,這個點應該聚在屏幕中點,或者至少包含所有3D對象的視圖中點。

滅點

CoreAnimation定義了這個點位于變換圖層anchorPoint,通常初始化時候在圖層的中心,但并不是永遠都在中心,如果手動改變也是會變的,比如鬧鐘的例子。也就是說,當圖層發(fā)生變換時候,這個點位置不變的,變換的軸線就是這個點。

當改變一個圖層postion,也改變了它的滅點,做3D變換的時候要記住這點。當視圖通過m34來讓它更加有3D效果,應該首先把它放倒屏幕中央,然后通過平移來把它移動到指定位置,而不是直接改變它的position,這樣3D圖層都共享一個滅點。

如果有多個視圖或者圖層,每個都做3D變換,那就需要分別設(shè)置相同的m34值,并且確保在變換之前都在屏幕中央享受同一個position。CAlayer中又一個更好的方法。

CAlayer有個sublayerTransform屬性。它也是CATransform3D類型,它能影響到所有的子圖層。這意味著,我們可以一次性的對包含這些圖層的容器做變換,所有的子圖層都自動繼承了這個變換方法。

通過一個地方設(shè)置透視變換的一個顯著優(yōu)勢就是,滅點被設(shè)置在容器圖層中點,不要再對子圖層分別設(shè)置了。下面是一個例子

一個視圖容器內(nèi)并排放置兩個視圖

@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;//初始化一個,position在中點

perspective.m34 = - 1.0 / 500.0;

//應用了sublayerTranform屬性,保證子layer變換都在同一個滅點。

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)一個半圓,我們就從背面去看它了。

視圖的背面,一個鏡像對稱的圖片

但這并不是一個很好的特性,因為如果圖層包含文本或者其他控件,用戶看到這些內(nèi)容的鏡像圖片會很奇怪,也會造成資源的浪費:想象一個不透明的固體立方體,既然永遠都看不到這些圖層的背面,為什么要浪費GPU來繪制它們?

CAlayer有個doubleSided的屬性來控制圖層的背面是否要被繪制。這是一個BOOL類型,默認為YES,設(shè)置為NO,那么當圖層正面從相機視角消失的時候,它背面不會被重繪。

扁平化圖層

如果對包含已經(jīng)做過變換的圖層的圖層做反方向的變換會出現(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情況下再試一個。修改代碼,讓內(nèi)外兩個視圖繞Y軸,而不是z軸,再加上透視效果。注意不能用sublayerTransform,因為內(nèi)部圖層并不直接是容器圖層的子圖層,所以要分別對圖層設(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軸做相反旋轉(zhuǎn)的預期結(jié)果

但我們看到的卻不是這樣的,我們看到的實際畫面是下面這樣,內(nèi)部圖層仍然向左旋轉(zhuǎn),并發(fā)生了扭曲:

繞Y軸做相反旋轉(zhuǎn)的真實結(jié)果

這是由于CA圖層盡管都存在于3D空間中,但是不同的圖層不都存在于同一個3D空間。每一個圖層的3D場景其實都是扁平化的。當你從正面觀察一個圖層,看到的實際上由子圖層創(chuàng)建的想象出來的3D場景,但當我們傾斜這個圖層,你會發(fā)現(xiàn)這個3D場景僅僅是被繪制在圖層的表面上。

? ? ? ?類似的,當你在玩一個3D游戲,實際上僅僅是把屏幕做了一次傾斜,或許在游戲中可以看見有一面墻在你面前,但是傾斜屏幕并不能夠看見墻里面的東西。所有場景里面繪制的東西并不會隨著你觀察它的角度改變而發(fā)生變化;圖層也是同樣的道理。

? ? ? ?這使得用Core Animation創(chuàng)建非常復雜的3D場景變得十分困難。你不能夠使用圖層樹去創(chuàng)建一個3D結(jié)構(gòu)的層級關(guān)系--在相同場景下的任何3D表面必須和同樣的圖層保持一致,這是因為每個的父視圖都把它的子視圖扁平化了。

至少當你用正常的CALayer的時候是這樣,CALayer有一個叫做CATransformLayer的子類來解決這個問題。

另開一篇介紹這個“iOS-CATransformlayer”。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容