iOS CALayer介紹

聲明

該篇文章的內(nèi)容參考自 iOS核心動(dòng)畫高級(jí)技巧 一文,非常感謝其作者和中文版的作者,讓我能夠相對(duì)系統(tǒng)的學(xué)習(xí) CoreAnimation 的知識(shí),我受益匪淺,再次感謝。

如果有興趣的小伙伴可以訪問其網(wǎng)站,詳細(xì)的,完整的學(xué)習(xí) CoreAnimation。

Core Animation 介紹

Core Animation,核心動(dòng)畫,似乎第一次看到這個(gè)名字的人都會(huì)認(rèn)為這是一個(gè)和動(dòng)畫相關(guān)的庫(kù),但是實(shí)際上,做動(dòng)畫只是 Core Animation 特性中的一種。由于做動(dòng)畫是依附于某種介質(zhì)(Layer圖層)的,因此可以猜想得到,該類中包含了Layer相關(guān)的東西。

Core Animation是一個(gè)復(fù)合引擎,它的職責(zé)就是盡可能快地組合屏幕上不同的可是內(nèi)容,這個(gè)內(nèi)容是被分解成獨(dú)立的圖層,存儲(chǔ)在一個(gè)叫做 圖層樹 的體系中,這個(gè)體系形成了 UIKit 以及在iOS應(yīng)用程序中你所看到的一切基礎(chǔ)。

圖層

那么圖層是什么呢?它和我們 iOS 日常開發(fā)使用到的 UIView 有什么區(qū)別呢?

視圖(UIView)和圖層(CALayer)

  • UIView

視圖就是在屏幕上顯示的一塊矩形塊(如圖片、文字等),它能夠響應(yīng)用戶觸摸的手勢(shì)等操作,也可以支持基于 Core Graphics 繪圖,可以做仿射變換(transform,平移、旋轉(zhuǎn)等)。在視圖的層級(jí)關(guān)系中可以相互嵌套,一個(gè)視圖可以管理它的子視圖。

  • CALayer

CALayer 概念上和 UIView 類似,同樣是一些被層級(jí)關(guān)系樹管理的矩形塊,同樣也可以包含圖片、文字等內(nèi)容,可用來做動(dòng)畫和變換,可以管理子圖層等功能,但是和 UIView 最大的不同是 CALayer 不會(huì)去處理用戶的交互事件。

  • UIView vs CALayer

每個(gè) UIView 都擁有一個(gè) CALayer 實(shí)例的圖層,而 UIView 的職責(zé)就是創(chuàng)建并管理這個(gè)圖層。實(shí)際上這個(gè)依附在視圖上的圖層才是真正用來在屏幕上顯示和做動(dòng)畫的,UIView 僅僅是對(duì) CALayer 的一個(gè)封裝,提供了響應(yīng)用戶手勢(shì)觸摸等相關(guān)的功能,以及 Core Animation 底層方法的高級(jí)接口(UIView的動(dòng)畫塊)。

使用圖層的必要性

在大部分的情況下,我們可通過蘋果封裝的 UIView 提供的高級(jí) API 就可以完成動(dòng)畫等功能,但是高度封裝伴隨著不夠靈活的缺陷,但我們想要做一些更底層的變化時(shí),UIView 又缺乏相應(yīng)的接口功能,這時(shí)候就需要我們接觸到 CALayer 圖層了,例如設(shè)置陰影、圓角、邊框等、做3D變換、非矩形范圍、不規(guī)則遮罩等等。

簡(jiǎn)單使用圖層

每當(dāng)你創(chuàng)建一個(gè)視圖時(shí),系統(tǒng)都默認(rèn)為你關(guān)聯(lián)了一個(gè)圖層,可以稱其為關(guān)聯(lián)圖層,為了演示圖層的使用,我們不對(duì)關(guān)聯(lián)層操作,而是新創(chuàng)建一個(gè)圖層。

我們可以直接創(chuàng)建 CALayer 對(duì)象,將其添加到視圖關(guān)聯(lián)的圖層上。

示例:我們創(chuàng)建一個(gè)藍(lán)色背景圖層,將添加到視圖的關(guān)聯(lián)圖層上。

// 創(chuàng)建子圖層
CALayer* subLayer = [CALayer new];
// 布局
subLayer.frame = CGRectMake(0, 0, 150, 150);
subLayer.position = self.view.center;
// 設(shè)置顏色
subLayer.backgroundColor = UIColor.blueColor.CGColor;
// 添加到視圖關(guān)聯(lián)圖層上
[self.view.layer addSublayer:subLayer];
使用圖層

寄宿圖

我們演示了圖層的簡(jiǎn)單實(shí)用示例:我們給該圖層填充上了顏色背景,但是如果僅僅是填充顏色的話,未免有點(diǎn)太單調(diào)了,下面我們來給該圖層填充一些其他的東西吧。

contents

CALayer 有一個(gè)叫 contents 的屬性,在 Mac OS 中,它對(duì) CGImage 和 NSImage 類型的值都起到作用,但是在 iOS 中,你如果將 UIImage 的值賦于它,雖然可以通過編譯,但是只能得到一個(gè)空白內(nèi)容。不過好在 UIImage 有一個(gè) CGImage 屬性,它返回一個(gè) “CGImageRef” 對(duì)象,它是一個(gè) Core Foundation 類型,你需要使用 bridge 關(guān)鍵字將其轉(zhuǎn)換為 Cocoa 類型。就像下面:

layer.contents = (__bridge id)image.CGImage;

我們修改下剛剛的代碼,讓其顯示圖像內(nèi)容。

// 設(shè)置 contents
subLayer.contents = (__bridge id)([UIImage imageNamed:@"snowman"].CGImage);
效果圖

這樣,我們通過contents為自己創(chuàng)建了一個(gè)具有 UIImageView 功能的圖層了呢。如果該圖層是某個(gè) UIView 視圖的關(guān)聯(lián)層,那么是否可以認(rèn)為我們自己創(chuàng)建了一個(gè) UIImageView 呢?

contentGravity

在使用 UIImageView 中,我們經(jīng)常與到圖片和圖片控件的大小不一致的情況,這就導(dǎo)致我圖片看起來被壓縮、拉伸的情況(就如上面情況),這時(shí)候我們通常使用 UIView 的 contentMode 屬性將圖片設(shè)置為比較合適的填充模式,讓圖片變得不是那么的“奇怪”。

imageView.contentMode = UIViewContentModeScaleAspectFit;

與之對(duì)應(yīng)的,圖層中有一個(gè) contentsGravity 屬性,它是一個(gè) NSString 類型。實(shí)質(zhì)上,視圖的 contentMode 就是圖層 contentsGravity 的一個(gè)封裝。contentsGravity可選的常量值有以下一些:

  • kCAGravityCenter
  • kCAGravityTop
  • kCAGravityBottom
  • kCAGravityLeft
  • kCAGravityRight
  • kCAGravityTopLeft
  • kCAGravityTopRight
  • kCAGravityBottomLeft
  • kCAGravityBottomRight
  • kCAGravityResize
  • kCAGravityResizeAspect
  • kCAGravityResizeAspectFill

我們來試著修改一些圖層的“填充模式”,在之前的代碼基礎(chǔ)上添加以下代碼:

// 設(shè)置填充模式
subLayer.contentsGravity = kCAGravityResizeAspect;
效果圖

我們可以看到,相比于之前那種拉伸以填充整個(gè)圖層的效果,新的填充模式似乎更好。

contentsScale

contentsScale 屬性定義了寄宿圖的像素尺寸和視圖大小的比例,默認(rèn)情況下它是一個(gè)值為1.0的浮點(diǎn)數(shù)。該屬性其實(shí)屬于支持高分辨率(Retina)屏幕機(jī)制的一部分。它用來判斷在繪制圖層的時(shí)候應(yīng)該為寄宿圖創(chuàng)建的空間大小,和需要顯示的圖片的拉伸度。如果 contentsScale 設(shè)置為1.0,表示每個(gè)點(diǎn)1個(gè)像素繪制圖片,設(shè)置為2.0,則會(huì)以每個(gè)2個(gè)像素繪制圖片,這就是我們的 Retina 屏幕。

在 contentGravity 設(shè)置為非拉伸的模式下,我們來演示一下 contentsScale 的效果。

首先我們來看一下 contentsScale 默認(rèn)情況下的效果:

// 創(chuàng)建子圖層
CALayer* subLayer = [CALayer new];
// 布局
subLayer.frame = CGRectMake(0, 0, 150, 150);
subLayer.position = self.view.center;
// 設(shè)置顏色
subLayer.backgroundColor = UIColor.groupTableViewBackgroundColor.CGColor;
// 設(shè)置 contents
subLayer.contents = (__bridge id)([UIImage imageNamed:@"snowman"].CGImage);
// 設(shè)置填充模式
subLayer.contentsGravity = kCAGravityCenter;
// 將其放大,讓其模糊來演示contentsScale
subLayer.transform = CATransform3DMakeScale(3, 3, 0);
// 添加到視圖關(guān)聯(lián)圖層上
[self.view.layer addSublayer:subLayer];
效果圖

我們發(fā)現(xiàn),圖片邊緣部分變得模糊不清,這由于拉伸一種因素在轉(zhuǎn)換的時(shí)候丟失了。我們可以手動(dòng)設(shè)置 contentsScale 的值。

// 設(shè)置 contentsScale
subLayer.contentsScale = 2;  // 一般設(shè)置為 [UIScreen mainScreen].scale,可根據(jù)設(shè)備自動(dòng)調(diào)整
效果圖

這樣我們的圖片在 Retina 設(shè)備上顯示正常。

另外,在我們使用 CATextLayer 時(shí),我們會(huì)發(fā)現(xiàn)文字變成像素形式,這個(gè)時(shí)候就需要你將 contentsScale 設(shè)置為比較合適的值。

maskToBounds

maskToBounds 可以切除超出圖層的部分,UIView中類似的有 clipsToBounds。

contentsRect

contentsRect 可以讓我們選擇圖片的一個(gè)子域。它是采用單位坐標(biāo)來指定區(qū)域的,默認(rèn)情況下, contentsRect 是 {0, 0, 1, 1},即顯示整個(gè)寄宿圖,如果我們指定小一點(diǎn)的區(qū)域時(shí),圖片就會(huì)被裁減顯示。


顯示小區(qū)域

這樣我們來裁剪左上角的區(qū)域作為填充圖,代碼如下:

// 設(shè)置 contentsRect
subLayer.contentsRect = CGRectMake(0, 0, 0.5, 0.5);
效果圖

另外,我們利用 contentsRect 的特性一次載入拼合圖,通過裁剪之后填充到不同圖層中。這樣做的好處是節(jié)省內(nèi)存的使用、縮短載入時(shí)間、提高渲染性能等等。就像下面的情況:

拼合圖

經(jīng)過裁剪之后,填充到不同的圖層:

裁剪圖層

contentsCenter

有時(shí)候,我們需要將圖片進(jìn)行局部拉伸,例如在社交軟件中,我們需要根據(jù)消息的文字信息將控件拉伸到合適的大小,如果該文本控件設(shè)置了背景圖片,可能因?yàn)槔斓脑虍a(chǎn)生了差異效果,這不是我們想看到的。contentsCenter 是一個(gè) CGRect,它定義了一個(gè)固定的邊框和一個(gè)在圖層上可拉伸的區(qū)域。

默認(rèn)情況下,contentsCenter 是{0, 0, 1, 1}。如果我們?cè)O(shè)置為{0.25, 0.25, 0.5, 0.5},拉伸的效果就如下面:

{0.25, 0.25, 0.5, 0.5}效果

這樣當(dāng)被拉伸之后,效果如下:


image

在 Interface Builder 中,可以在下圖中控制 contentsCenter 屬性。


Interface Builder

繪制圖形

除了給圖層填充寄宿圖,我們可以直接使用圖層繪制圖形。在 UIView 中,我們可以重寫 drawRect: 來繪制圖形(開發(fā)者可以調(diào)用setNeedsDisplay方法觸發(fā))。實(shí)質(zhì)上該方法封裝了 CALayer 的繪制方法。

在 CALayer 中,有一個(gè) delegate 屬性,你可以在代理方法中完成 CALayer 的繪制。你可以調(diào)用 -display 觸發(fā)代理方法。

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

該方法是默認(rèn)的代理方法。你可以在這里設(shè)置 contents 屬性。

如果不實(shí)現(xiàn)該代理方法,系統(tǒng)會(huì)調(diào)用下面的方法:

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;

該方法傳遞了一個(gè)繪制的圖形上下文環(huán)境,你可以使用它完成圖層的繪制工作。

示例:

// 其他創(chuàng)建代碼
// 設(shè)置代理
subLayer.delegate = self;
// 觸發(fā) CALayer 的繪制
[subLayer display];

// 完成繪制的代理方法
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
    CGContextSetLineWidth(ctx, 10.0f);
    CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
    CGContextStrokeEllipseInRect(ctx, layer.bounds);
}
使用代理完成圖形繪制

需要注意的事情:

  • 我們需要顯示的調(diào)用 -display 觸發(fā)重繪
  • CALayerDelegate 的代理方法并沒有對(duì)超出圖層的繪制內(nèi)容提供繪制支持,因此即使調(diào)用 masksToBounds 為 NO時(shí),依舊會(huì)被裁減掉

一般來說,除非你創(chuàng)建了一個(gè)單獨(dú)的圖層,你幾乎沒有機(jī)會(huì)用到 CALayerDelegate 協(xié)議。因?yàn)楫?dāng) UIView 創(chuàng)建了它的宿主圖層時(shí),它就會(huì)自動(dòng)地把圖層的 delegate 設(shè)置為它自己,并提供了一個(gè) -displayLayer: 的實(shí)現(xiàn),你所需要做的就是實(shí)現(xiàn) UIView 的 -drawRect: 方法。

圖層幾何學(xué)概念

布局

UIView 有三個(gè)比較重要的布局屬性:frame,bounds 和 center,CALayer 對(duì)應(yīng)地叫做 frame,bounds 和 position。為了能清楚區(qū)分,圖層用了“position”,視圖用了“center”,但是他們都代表同樣的值。
frame 代表了圖層的外部坐標(biāo)(也就是在父圖層上占據(jù)的空間),bounds 是內(nèi)部坐標(biāo)({0, 0}通常是圖層的左上角),center 和 position 都代表了相對(duì)于父圖層 anchorPoint 所在的位置。


UIView 和 CALayer 的坐標(biāo)系

當(dāng)操縱視圖的 frame,實(shí)際上是在改變位于視圖下方 CALayer 的frame,不能夠獨(dú)立于圖層之外改變視圖的 frame。

對(duì)于視圖或者圖層來說,frame 并不是一個(gè)非常清晰的屬性,它其實(shí)是一個(gè)虛擬屬性,是根據(jù) bounds,position 和 transform 計(jì)算而來,所以當(dāng)其中任何一個(gè)值發(fā)生改變,frame都會(huì)變化。相反,改變frame的值同樣會(huì)影響到他們當(dāng)中的值。

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

CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
view.transform = transform;
將視圖旋轉(zhuǎn)45度后的布局

錨點(diǎn)

anchorPoint 可以看作是移動(dòng)圖層的把柄,默認(rèn)情況下位于圖層的中點(diǎn),當(dāng)你旋轉(zhuǎn)視圖時(shí),都是圍繞 anchorPoint 來進(jìn)行旋轉(zhuǎn)的,及在視圖中心點(diǎn)做旋轉(zhuǎn)。你可以通過移動(dòng) anchorPoint 來改變 frame 以及 旋轉(zhuǎn)的中心點(diǎn),比如你將它置于圖層 frame 的左上角,這時(shí)圖層會(huì)向右下角的 position 方向移動(dòng),此時(shí)雖然依舊是圍繞 anchorPoint 來進(jìn)行旋轉(zhuǎn),但是已經(jīng)是視圖的左上角了。

改變anchorPoint的效果

坐標(biāo)系

和視圖一樣,圖層在圖層樹當(dāng)中也是相對(duì)于父圖層按層級(jí)關(guān)系放置,如果父圖層發(fā)生了移動(dòng),它的所有子圖層也會(huì)跟著移動(dòng)。但是有時(shí)候你需要知道一個(gè)圖層的絕對(duì)位置,或者是相對(duì)于另一個(gè)圖層的位置,而不是它當(dāng)前父圖層的位置。

CALayer給不同坐標(biāo)系之間的圖層轉(zhuǎn)換提供了一些工具類方法:

- (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer;
- (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer *)layer;
- (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer *)layer;
- (CGRect)convertRect:(CGRect)rect toLayer:(CALayer *)layer;

Z坐標(biāo)軸

和 UIView 嚴(yán)格的二維坐標(biāo)系不同,CALayer存在于一個(gè)三維空間當(dāng)中。除了我們已經(jīng)討論過的 position 和 anchorPoint 屬性之外, CALayer 還有另外兩個(gè)屬性,zPosition 和 anchorPointZ ,二者都是在Z軸上描述圖層位置的浮點(diǎn)類型。

zPosition 最實(shí)用的功能就是改變圖層的顯示順序了。通過增加圖層的zPosition ,就可以把圖層向相機(jī)方向前置,于是它就在小于它的zPosition 值的圖層的前面。

和 UIView 添加視圖一樣,先添加視圖數(shù)上的會(huì)被后面的視圖覆蓋住,圖層也是同樣的道理,后繪制的圖層將會(huì)遮蓋住之前的圖層。

CALayer* subLayer = [CALayer new];
subLayer.frame = CGRectMake(0, 0, 150, 150);
subLayer.position = self.view.layer.position;
subLayer.backgroundColor = UIColor.redColor.CGColor;
// 先 添加到視圖關(guān)聯(lián)圖層上
[self.view.layer addSublayer:subLayer];
CALayer* subLayer2 = [CALayer new];
subLayer2.frame = CGRectMake(0, 0, 150, 150);
subLayer2.position = CGPointMake(self.view.layer.position.x+50, self.view.layer.position.y+50);
subLayer2.backgroundColor = UIColor.blueColor.CGColor;
// 后
[self.view.layer addSublayer:subLayer2];
后繪制的圖層將覆蓋之前的圖層

不過,類似 UIView,你可以使用一下方法調(diào)整視圖的層級(jí)關(guān)系:

- (void)insertSublayer:(CALayer *)layer atIndex:(unsigned)idx;
- (void)insertSublayer:(CALayer *)layer below:(nullable CALayer *)sibling;
- (void)insertSublayer:(CALayer *)layer above:(nullable CALayer *)sibling;

當(dāng)然,我們?yōu)榱孙@示 zPosition 的作用,我們不調(diào)用上述的方法,我們來修改一下圖層的 zPosition 值,其他的代碼不做任何變化:

subLayer.zPosition = 1.0;
設(shè)置zPosition后的圖層關(guān)系

Hit Testing

如前面所有,CALayer 和 UIView 最大的區(qū)別就是,CALayer 并不關(guān)心任何響應(yīng)鏈?zhǔn)录圆荒苤苯犹幚碛|摸事件或者手勢(shì),但是在實(shí)際開發(fā)過程中,我們使用了圖層繪制圖形并且需要處理用戶的觸摸事件,那該怎么辦呢?好在 CALayer 有一系列的方法幫你處理觸摸事件:-containsPoint:-hitTest:。

-containsPoint:

該方法可以判斷一個(gè)點(diǎn)是否在圖層的 frame 范圍內(nèi),如果在就返回 YES,反之為 NO。

判斷測(cè)試的核心代碼如下:

-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    CGPoint point = [[touches anyObject] locationInView:self.view];
    // 將落點(diǎn)轉(zhuǎn)換到紅色背景的圖層的層級(jí)上,以判斷是否被包含
    point = [self.subLayer convertPoint:point fromLayer:self.view.layer];
    if ([self.subLayer containsPoint:point]) {
        [[[UIAlertView alloc] initWithTitle:@"在紅色圖層中"
                                    message:nil
                                   delegate:nil
                          cancelButtonTitle:@"OK"
                          otherButtonTitles:nil] show];
    }
    else{
        [[[UIAlertView alloc] initWithTitle:@"未在紅色圖層中"
                                    message:nil
                                   delegate:nil
                          cancelButtonTitle:@"OK"
                          otherButtonTitles:nil] show];
    }
}
觸摸測(cè)試結(jié)果

-hitTest:

-hitTest: 同樣接受一個(gè)點(diǎn),但是它返回的是包含這個(gè)點(diǎn)的圖層,而不是 BOOL 類型。如果這個(gè)點(diǎn)在被檢測(cè)的父類圖層之外,則返回 nil。

我們修改后的代碼如下:

-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    CGPoint point = [[touches anyObject] locationInView:self.view];
    // 將落點(diǎn)轉(zhuǎn)換到紅色背景的圖層上,以判斷是否被包含
    point = [self.subLayer convertPoint:point fromLayer:self.view.layer];
    CALayer *layer = [self.view.layer hitTest:point];
    if (layer == self.subLayer) {
        [[[UIAlertView alloc] initWithTitle:@"在紅色圖層中"
                                          message:nil
                                         delegate:nil
                                cancelButtonTitle:@"OK"
                                otherButtonTitles:nil] show];
    }else{

        [[[UIAlertView alloc] initWithTitle:@"未在紅色圖層中"
                                    message:nil
                                   delegate:nil
                          cancelButtonTitle:@"OK"
                          otherButtonTitles:nil] show];
    }
}
觸摸測(cè)試結(jié)果

特殊效果

在我們開發(fā)過程中,想必經(jīng)常遇到過設(shè)置視圖的圓角,邊框,陰影等特殊效果吧,實(shí)際上這些都是都過設(shè)置 CALayer 來達(dá)到效果的。如果大家都比較熟悉,就可以跳過這一節(jié)。

圓角和裁剪

conrnerRadius 和 masksToBounds

CALayer 有一個(gè)叫做 conrnerRadius 的屬性控制著圖層角的曲率。它是一個(gè)浮點(diǎn)數(shù),默認(rèn)為0(為0的時(shí)候就是直角),但是你可以把它設(shè)置成任意值。默認(rèn)情況下,這個(gè)曲率值只影響背景顏色而不影響背景圖片或是子圖層。不過,如果把 masksToBounds 設(shè)置成 YES 的話,圖層里面的所有東西都會(huì)被截取。

示例:

// 設(shè)置圓角
self.bgLayer.cornerRadius = 20;

// 設(shè)置圓角和裁剪
self.bgLayer2.cornerRadius = 20;
self.bgLayer2.masksToBounds = YES;
圓角和裁剪

邊框和邊框顏色

CALayer 另外兩個(gè)非常有用屬性就是 borderWidth 和 borderColor。二者共同定義了圖層邊的繪制樣式。

我們修改代碼如下:

// 設(shè)置圓角
self.bgLayer.cornerRadius = 20;
// 設(shè)置邊框和邊框顏色
self.bgLayer.borderWidth = 5;
self.bgLayer.borderColor = UIColor.brownColor.CGColor;

// 設(shè)置圓角和裁剪
self.bgLayer2.cornerRadius = 20;
self.bgLayer2.masksToBounds = YES;
// 設(shè)置邊框和邊框顏色
self.bgLayer2.borderWidth = 5;
self.bgLayer2.borderColor = UIColor.brownColor.CGColor;
添加邊框和邊框顏色

陰影

shadowOpacity、shadowColor、shadowOffset 和 shadowRadius

CALayer 中的常用到的四個(gè)屬性 shadowOpacity、shadowColor、shadowOffset 和 shadowRadius。其中:

  • shadowOpacity 控制圖層陰影的透明度
  • shadowColor 陰影的顏色
  • shadowOffset 陰影的方向和距離
  • shadowRadius 陰影的的模糊度

那么我們來給上個(gè)例子中的圖層添加上陰影部分的效果:

self.bgLayer.shadowOpacity = 1;
self.bgLayer.shadowColor = UIColor.brownColor.CGColor;
self.bgLayer.shadowOffset = CGSizeMake(0, -3);
self.bgLayer.shadowRadius = 10;
陰影效果

我們看到,左邊圖層被我們?cè)O(shè)置陰影了效果。

需要說明的是,shadowOffset 是一個(gè) CGSize,寬度控制陰影橫向位移,高度控制縱向位移,默認(rèn)值是{0.-3},即陰影相對(duì)Y軸向上位移3個(gè)點(diǎn)。shadowRadius 值越大陰影越模糊,默認(rèn)值為3。

寄宿圖的陰影

另外,圖層的陰影是根據(jù)內(nèi)容的形狀產(chǎn)生,而不是父圖層的外形或者圓角,我們從上圖就可以看出來,藍(lán)色圖層是內(nèi)容部分,未裁剪的部分也有陰影效果。

我們來驗(yàn)證一下,將填充內(nèi)容改為寄宿圖:

self.bgLayer.shadowOpacity = 1;
self.bgLayer.shadowColor = UIColor.blackColor.CGColor;
self.bgLayer.shadowOffset = CGSizeMake(0, 3);
self.bgLayer.shadowRadius = 10;
self.bgLayer.contentsGravity = kCAGravityResizeAspect;
self.bgLayer.contents = (__bridge id)([UIImage imageNamed:@"snowman"].CGImage);

注意:背景填充色也是寄宿圖中的一部分,這里需要注意將填充色去掉。

陰影是根據(jù)寄宿圖的輪廓來確定

陰影的裁剪

回到之前的例子中,我們看到在兩個(gè)圖層中,左邊圖層被我們?cè)O(shè)置上了陰影效果,接下來我們嘗試給右邊的有裁剪的圖層添加陰影:

// 左邊圖層
self.bgLayer.shadowOpacity = 1;
self.bgLayer.shadowColor = UIColor.brownColor.CGColor;
self.bgLayer.shadowOffset = CGSizeMake(0, -3);
self.bgLayer.shadowRadius = 10;
// 右邊圖層
self.bgLayer2.shadowOpacity = 1;
self.bgLayer2.shadowColor = UIColor.brownColor.CGColor;
self.bgLayer2.shadowOffset = CGSizeMake(0, -3);
self.bgLayer2.shadowRadius = 10;
masksToBounds裁剪掉了陰影效果

我們發(fā)現(xiàn),右邊視圖并沒有陰影效果。而兩個(gè)圖層的唯一區(qū)別就是左邊未做裁剪(即 masksToBounds 設(shè)置為 YES)。那么為什么會(huì)產(chǎn)生這種效果差異呢?這是由于陰影通常是在圖層的邊界之外,而 masksToBounds 則會(huì)將超出圖層邊界的部分裁剪掉,因此陰影部分實(shí)際上是被裁減掉了,所以我們看不到陰影部分。

那如果我們需要裁剪掉超出部分,也需要設(shè)置陰影該怎么辦呢?這時(shí)候,我們就需要額外新增加一個(gè) frame 一致的圖層作為其陰影層,以便讓用戶能夠看到該圖層的陰影(實(shí)際上是下層的陰影),我們代碼修改如下:

// 添加陰影層
[self.view.layer addSublayer:self.bgShadowLayer];
// 將圖層添加到陰影層上
[self.bgShadowLayer addSublayer:self.bgLayer2];
// 陰影層做陰影部分的設(shè)置
self.bgShadowLayer.cornerRadius = self.bgLayer2.cornerRadius;
self.bgShadowLayer.shadowOpacity = 1;
self.bgShadowLayer.shadowColor = UIColor.brownColor.CGColor;
self.bgShadowLayer.shadowOffset = CGSizeMake(0, -3);
self.bgShadowLayer.shadowRadius = 10;
masksToBounds下的陰影效果

mask蒙版/遮罩

我們可以通過 masksToBounds 屬性可以將圖層沿邊界裁剪圖形,通過 cornerRadius 屬性可以將圖層設(shè)定一個(gè)圓角。但是如果我們希望展現(xiàn)的內(nèi)容是一個(gè)不規(guī)則的形狀,那該如何做呢?

CALayer 有一個(gè)屬性 mask,這個(gè)屬性本身就是一個(gè) CALayer 類型,它的內(nèi)容輪廓定義了父圖層的可見部分,其他部分則會(huì)被拋棄。

mask的效果

下面我們演示一下,兩種 mask 的效果,一種是采用圖片,一種是自定義圖形。

@interface ViewController ()
@property (strong, nonatomic)CALayer* maskLayer1;
@property (strong, nonatomic)CAShapeLayer* maskLayer2;

@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (weak, nonatomic) IBOutlet UIImageView *imageView1;
@property (weak, nonatomic) IBOutlet UIImageView *imageView2;
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    self.imageView1.layer.mask = self.maskLayer1;
    self.imageView2.layer.mask = self.maskLayer2;
}

// 寄宿圖為圖片
-(CALayer *)maskLayer1{
    if (_maskLayer1==nil) {
        _maskLayer1 = [CALayer new];
        _maskLayer1.frame = self.imageView.bounds;
        UIImage* image = [UIImage imageNamed:@"clip"];
        _maskLayer1.contents = (__bridge id)image.CGImage;
    }
    return _maskLayer1;
}

// 寄宿圖為自定義圖形
-(CAShapeLayer *)maskLayer2{
    if (_maskLayer2==nil) {
        _maskLayer2 = [CAShapeLayer new];
        _maskLayer2.frame = self.imageView.bounds;
        UIBezierPath* path = [UIBezierPath bezierPathWithOvalInRect:_maskLayer1.bounds];
        _maskLayer2.path = path.CGPath;
    }
    return _maskLayer2;
}
mask的效果

CALayer 蒙板圖層真正厲害的地方在于蒙板圖不局限于靜態(tài)圖。任何有圖層構(gòu)成的都可以作為 mask 屬性,這意味著你的蒙板可以通過代碼甚至是動(dòng)畫實(shí)時(shí)生成。

如果我們將上面的 mask 蒙板圖層加上動(dòng)畫:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.imageView1.layer.mask = self.maskLayer1;
    self.imageView2.layer.mask = self.maskLayer2;
    
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.y"];
    animation.duration = 1.0f;
    animation.toValue = @(M_PI);
    animation.autoreverses = YES;
    animation.repeatCount = HUGE_VALF;
    [self.maskLayer1 addAnimation:animation forKey:@"animation"];
    [self.maskLayer2 addAnimation:animation forKey:@"animation"];
}
mask圖層動(dòng)畫

你可以利用 mask 屬性做很多有趣的事情,例如你可以將文字(CATextLayer,或者UILabel.layer)加到彩色圖層上,從而產(chǎn)生漸變效果的文字。


漸變文字

圖層的變換

在 UIView 中,有一個(gè) transform 屬性,它是一個(gè) CGAffineTransform 類型,可以用來對(duì)視圖做二維空間做旋轉(zhuǎn)、縮放和平移的變換。

實(shí)際上,UIView 的 transform 只是封裝了內(nèi)部圖層的變換,只不過 CALayer 做二維空間變換的屬性叫做 affineTransform,而其屬性 transform 是一個(gè) CATransform3D 類型,可以將圖層在三維空間變換效果。

關(guān)于變換可以參考之前的一篇文章:iOS 視圖的二維變換
,文章中介紹的 transform 是在 UIView 的基礎(chǔ)上介紹的,不過沒關(guān)系,原理是一樣的,將 UIView 的 transform 改為 CALayer 的 affineTransform 屬性即可。

子類圖層

CAShapeLayer

繪制圖形圖層

CATextLayer

顯示文字

CAGradientLayer

漸變圖層

CAReplicatorLayer

復(fù)制圖層

CATransformLayer

CAScrollLayer

CATiledLayer

CAEmitterLayer

發(fā)射粒子器圖層

CAEAGLLayer

AVPlayerLayer

視頻播放圖層

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

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

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