玩轉iOS開發:2.《Core Animation》初識CALayer

文章轉至個人我的技術博客:https://cainluo.github.io/14771496782021.htmlhtml


做者感言

前面咱們簡單介紹了一些圖層與視圖的關係, 也介紹了CALayer以及UIView的相同點和區別, 沒看過的朋友能夠去看看《Core Animation》基礎概念今天咱們就着重的來說講CALayer. 最後: 若是你有更好的建議或者對這篇文章有不滿的地方, 請聯繫我, 我會參考大家的意見再進行修改, 聯繫我時, 請備註Core Animation若是以爲好的話, 但願你們也能夠打賞一下~嘻嘻~祝你們學習愉快~謝謝~git


簡介

前面咱們已經簡單介紹了什麼是CALayer, CALayer主要是用來作什麼的, 如今咱們來更加深刻的瞭解CALayer到底有些什麼東西供給咱們去使用的.github


CALayer的Contents屬性

CALayer中, 有這麼一個contents屬性, 那麼contents這個屬性是用來作什麼的呢? 咱們先來看一段官方解釋.緩存

An object that provides the contents of the layer. Animatable.微信

從字面上意思來看, 這是一個提供圖層內容的對象, 而且是id類型, 也就意味着, 咱們能夠給這個contents屬性賦任意的值, 畢竟是id類型嘛, 但在實際操做中, 是行不通的, 爲何?ide

這裏就要牽扯到Mac OS了, 由於在Mac OS當中, 給CALayer中的contents屬性賦值, 不管是CGImage仍是NSImage, 都能獲得對應的效果, 而在iOS當中, 只能賦值CGImage, 或許到了這裏, 你會以爲挺簡單的, 但呵呵了, 這裏還要牽扯到指針的問題(我的對指針有些暈).佈局

實際上, 咱們給contents屬性賦CGImage的時候, 真正賦值的是CGImageRef, 它是指向CGImage的指針, 用過UIImage的朋友應該會發現,UIImage當中有一個CGImage的屬性, 這個屬性的返回值就是CGImageRef.性能

某些童鞋會說, 既然是CGImageRef類型的話, 那直接賦值給contents不就行了麼, 其實並非滴, 直接這麼賦值會報編譯錯誤滴, 爲何??學習

由於CGImageRef並非一個真正的Cocoa對象, 它是屬於Core Foundation裏的東西(什麼是Core Foundation? 嘿嘿, 自行百度去吧~~), 那麼咱們就沒辦法給contents賦值嗎? 確定不是啦, 咱們能夠經過一些關鍵字進行賦值, 就可以獲得對應的效果了, 代碼以下:ui

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor lightGrayColor];
    
    UIView *layerView = [[UIView alloc] init];
    layerView.backgroundColor = [UIColor whiteColor];
    layerView.center = self.view.center;
    layerView.bounds = CGRectMake(0, 0, 200, 200);
    
    [self.view addSubview:layerView];
    
    
    UIImage *image = [UIImage imageNamed:@"bear"];
    
    layerView.layer.contents = (__bridge id _Nullable)(image.CGImage);
}
複製代碼

這裏提一點哈, 這個關鍵字在非ARC內存管理機制中是不須要加滴, 可是呢, 特麼的, 你爲啥不用ARC? 估計連你本身都會這麼問你本身了.

效果圖:

1

看完效果圖以後, 咱們再看看它的層級結構, 你就會發現和咱們使用的UIImageView徹底如出一轍, 不信? 我給大家加個UIImageView看看~

2

3

雖然咱們並非經過使用UIImageView來實現加載圖片的, 但咱們能夠經過Layer層給UIView進行加載圖片, 醬紫的話, 你們是否是對蘋果如何封裝UIImageView有了一個思路呢?


CALayer的ContentGravity屬性

不知道大家有沒有發現, 咱們所展現的圖片有一些變形, 若是是用UIImageView的話, 咱們能夠直接設置contentModel這個屬性, 使得圖片正常顯示, 但若是是在CALayer呢?

固然CALayer也有一個相似contentModel的屬性, 它叫作contentGravity, 雖然名字有些差異, 可是使用效果都是差很少的~~

進入頭文件以後咱們會看到contentGravity能夠賦值的選項有:

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

如今咱們就來改改工程裏的代碼:

layerView.layer.contentsGravity = kCAGravityResizeAspect;
複製代碼

效果圖:

4

醬紫圖片顯示正確啦~~貌似UIImageViewcontentModel也就是這麼實現的~


CALayer的ContentsScale屬性

其實我在想, 我是否是不該該把CALayer裏的全部屬性都拿出來說一講? 只是簡單講一些重要的好了, 接下來的就是contentsScale.

contentsScale這個屬性主要是定義圖層的像素和視圖比例大小, 默認狀況是1.0, 並且是CGFloat類型.

但若是你的CALayer已經設置了contentsGravity, 那麼再設置contentsScale, 效果就是沒多大影響, 或者直接說壓根就沒影響吧, 若是你只是想着單純的放大縮小CALayer, 能夠直接使用transformaffineTransForm實現你想要的效果, 後續會詳細講解transforms.

固然放大縮小確定也不是contentsScale屬性的主要做用, 這裏就須要解釋一下contentsScale:

contentsScale主要是支持Retina機制的一部分, 它是用來判斷繪製圖層時應該須要建立多大的空間, 和須要顯示圖片的拉伸度,UIView也有一個相似的屬性, 叫作contentScaleFactor, 只是咱們很是少的去使用罷了.

這個時候咱們來改改工程裏的代碼, 讓contentsScale呈現效果:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor lightGrayColor];
    
    UIView *layerView = [[UIView alloc] init];
    layerView.backgroundColor = [UIColor whiteColor];
    layerView.center = self.view.center;
    layerView.bounds = CGRectMake(0, 0, 200, 200);
    
    [self.view addSubview:layerView];
    
    
    UIImage *image = [UIImage imageNamed:@"bear"];
    
    layerView.layer.contents = (__bridge id _Nullable)(image.CGImage);
    
    layerView.layer.contentsGravity = kCAGravityCenter;
    layerView.layer.contentsScale = image.scale;
}
複製代碼

效果圖:

5


CALayer的MaskToBounds屬性

看完了contentsScale屬性, 如今咱們繼續來看maskToBounds屬性.

咱們先來看一段官方文字介紹:

A Boolean indicating whether sublayers are clipped to the layer’s bounds. Animatable.

看完以後, 咱們知道這個屬性是一個BOOL類型, 問你若是子圖層超出了視圖層, 是否剪切掉, 若是你設置爲YES, 那就剪切掉了, 默認爲NO.

就拿咱們剛剛的工程做爲一個事例來說, 那張圖片確定是超過了視圖層的, 若是咱們把maskToBounds屬性設置爲YES, 效果就不同了:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor lightGrayColor];
    
    UIView *layerView = [[UIView alloc] init];
    layerView.backgroundColor = [UIColor whiteColor];
    layerView.center = self.view.center;
    layerView.bounds = CGRectMake(0, 0, 200, 200);
    
    [self.view addSubview:layerView];
    
    
    UIImage *image = [UIImage imageNamed:@"bear"];
    
    layerView.layer.contents = (__bridge id _Nullable)(image.CGImage);
    
    layerView.layer.contentsGravity = kCAGravityCenter;
    layerView.layer.contentsScale = image.scale;
    layerView.layer.masksToBounds = YES;
}
複製代碼

效果圖:

6


CALayer的ContentsRect屬性

CALayer有一個屬性叫作contentsRect, 它是能夠根據輸入的座標軸來顯示區域塊的圖層, 而frame,bounds則是以點來計算的, 在這裏它們有一些區別.

還有一個注意點, contentsRect的座標軸默認是**{0, 0, 1, 1}, 單位座標通常指定的是0~1**之間, 若是小於這個數, 或者是大於這個數, 哼哼, 你本身試試看吧~

說到這裏, 應該會有人有些疑惑, 神馬是點? 難道和像素不同的麼? 那就先來普及一下先吧(這裏我是搜到的一些比較中肯的說法)~


> 點: > > * 在**iOS**和**Mac OS**中最多見的座標體系。 > * 點就像是虛擬的像素, 也被稱做邏輯像素。 > * 在標準設備上, 一個點就是一個像素, 可是在**Retina**設備上, 一個點等於**2*2**個像素。 > * **iOS**用點做爲屏幕的座標測算體系就是爲了在**Retina**設備和普通設備上能有一致的視覺效果。
> 像素: > > * 物理像素座標並不會用來屏幕布局, 可是仍然與圖片有相對關係。 > * **UIImage**是一個屏幕分辨率解決方案, 因此指定點來度量大小。 > * 可是一些底層的圖片表示如**CGImage**就會使用像素, 因此你要清楚在**Retina**設備和普通設備上, 他們表現出來了不一樣的大小。
> 單位: > > * 對於與圖片大小或是圖層邊界相關的顯示, 單位座標是一個方便的度量方式, 當大小改變的時候, 也不須要再次調整。 > * 單位座標在**OpenGL**這種紋理座標系統中用得不少,**Core Animation**中也用到了單位座標。
> 這裏咱們仍是繼續拿剛剛的工程來演示:
- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor lightGrayColor];
    
    UIView *layerView = [[UIView alloc] init];
    layerView.backgroundColor = [UIColor whiteColor];
    layerView.center = self.view.center;
    layerView.bounds = CGRectMake(0, 0, 200, 200);
    
    [self.view addSubview:layerView];
    
    
    UIImage *image = [UIImage imageNamed:@"bear"];
    
    layerView.layer.contents = (__bridge id _Nullable)(image.CGImage);
    
    layerView.layer.contentsGravity = kCAGravityCenter;
    layerView.layer.contentsScale = image.scale;
    layerView.layer.masksToBounds = YES;
    layerView.layer.contentsRect = CGRectMake(0, 0, 0.5, 0.5);
}
複製代碼

效果圖:

7

補充一些額外的知識點:

contentsRectApp當中, 還有一個更好玩的用法叫作image sprites, 若是你有遊戲開發經驗的話, 你確定對image sprites不陌生, 甚至是很是熟練的使用, 可使圖片獨立的變動在屏幕上顯示的位置.

但若是咱們拋開遊戲開發來講的話, 在咱們平常生活當中, 微博就是一個經典的表明, 把圖片拼接成一張大圖片, 而後再分享出去, 這裏使用的就是contentsRect, 這樣子的好處就是能夠減小內存的使用, 載入的時間, 還有渲染的性能等等.

這裏我就不作演示了, 感興趣的童鞋能夠 到網上找找資料.


CALayer的ContentsCenter屬性

講到這裏, 已經算是CALayer最後的一個屬性了, 它叫作contentsCenter, 它的意思比較拗口, 它是一個CGRect, 且定義了一個固定的邊框和一個圖層上可拉伸的區域.

若是你只是單單改變contentsCenter的值, 並不會影響到CALayer的顯示效果, 要同時去改變這個圖層的大小, 才能看到效果.

咱們仍是直接看代碼吧, 在咱們原先的項目上添加一個方法, 而且加多一個UIView類:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor lightGrayColor];
    
    UIView *layerView = [[UIView alloc] init];
    layerView.backgroundColor = [UIColor whiteColor];
    layerView.center = self.view.center;
    layerView.bounds = CGRectMake(0, 0, 200, 200);
    
    [self.view addSubview:layerView];
    
    
    UIImage *image = [UIImage imageNamed:@"bear"];
    
    /** * Contents */
    layerView.layer.contents = (__bridge id _Nullable)(image.CGImage);
    
    /** * ContentsGravity */
    layerView.layer.contentsGravity = kCAGravityCenter;
    
    /** * ContentsScale */
    layerView.layer.contentsScale = image.scale;
    
    /** * MasksToBounds */
    layerView.layer.masksToBounds = YES;
    
    /** * contentsRect */
    layerView.layer.contentsRect = CGRectMake(0, 0, 1.1, 1.1);
    
    
    [self addImage:[UIImage imageNamed:@"bear"] withContensRect:CGRectMake(0.25, 0.25, 0.5, 0.5)];
}

- (void)addImage:(UIImage *)image withContensRect:(CGRect)rect {
    
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    
    view.layer.contents = (__bridge id _Nullable)(image.CGImage);
    view.layer.contentsCenter = rect;
    
    [self.view addSubview:view];
}
複製代碼

效果圖:

8

補充一個知識點, 若是你是使用Storyboard或者是xib的話, 你能夠在右側的欄目看到contentsCenter

9


CALayer的Delegate

降到這裏, 基本上就已經介紹完了CALayer, 但還有一點也是須要提一提的, CALayer除了使用contents賦值CGImage來顯示圖層以後, 還可使用Core Graphics去進行繪製, 在UIView就能夠看到這個方法, 叫作**-drawRect:**.

-drawRect:方法默認沒有去實現, 由於在UIView中,backing image並非必須的, 但若是你去調用**-drawRect:方法, 那麼UIView就會給你生成一個新的backing image**, 而這個backing image的像素尺寸等於視圖大小乘以contentsScale的值.

這裏須要注意一個點, 若是你的視圖裏不須要建立一個backing image的話, 千萬不要去寫一個空的**-drawRect:**方法, 這樣子就會對CPU與內存形成浪費, 這也是蘋果官方建議的.

咱們先來解釋一下**-drawRect:**方法的實現原理:

  • 當**-drawRect:被調用,UIView會建立一個新的backing image**.
  • 會使用Core Graphicsbacking image進行描繪.
  • 而後這個描繪好的backing image會被緩存起來, 等到它須要被更新的時候, 就會去使用.

固然, 咱們本身也能夠手動去調用, 好比去調用**-setNeedsDisplay:, 那麼被從新繪製的backing image**就會立馬顯示出來了.

總而言之, -drawRect:看似是UIView的方法, 但實際上都是在內部對CALayer進行了重繪以及緩存的操做.

還有, CALayer也有一個delegate的屬性, 並且是id類型, 並實現CALayerDelegate協議, 當CALayer須要一個特定內容時, 就會從代理方法裏去請求, 因爲CALayerDelegate是一個非正式的協議, 因此並無神馬屬性給你引用, 直接調用代理方法就能夠了.

當須要被重繪的時候, CALayer就會去調用:

-(void)displayLayer:(CALayer *)layer;
複製代碼

若是你還想再重繪的時候設置一下contents的話, 那麼就要在這個方法裏去實現, 否則在別的方法裏就無法作到了.

但若是沒有實現以上的方法時, 那麼就會去調用:

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
複製代碼

在調用這個方法以前, CALayer會建立一個適合尺寸的backing image, 固然, 尺寸確定是由boundscontentsScale決定的, 還有一個Core Graphics繪製的上下文, 未繪製backing image作準備, 等的就是ctx的傳入.

說了那麼多咱們直接用代碼演示吧~

- (void)createNewLayerWithSuperView {
    
    // Background View
    UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectMake(220, 0, 100, 100)];
    backgroundView.backgroundColor = [UIColor whiteColor];
    
    [self.view addSubview:backgroundView];
    
    // Blue Layer
    CALayer *blueLayer = [CALayer layer];
    
    blueLayer.frame = CGRectMake(25, 25, 50, 50);
    blueLayer.backgroundColor = [UIColor blueColor].CGColor;
    
    // Set Layer Delegate
    blueLayer.delegate = self;
    
    // Set Layer contentsScale
    blueLayer.contentsScale = [UIScreen mainScreen].scale;
    
    [backgroundView.layer addSublayer:blueLayer];
    
    [blueLayer display];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    
    CGContextSetLineWidth(ctx, 5);
    CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
    CGContextStrokeEllipseInRect(ctx, layer.bounds);
}
複製代碼

效果圖:

10

看到效果圖代碼以後, 有兩點咱們是須要注意一下的:

  • 不一樣的CALayer在不一樣的UIView視圖中使用, 是不會自動去重載它的內容的, 因此在事例當中, 咱們用blueLayer調用了display這個方法.
  • 在事例當中, 咱們並無對blueLayer設置masksToBounds屬性, 但所繪製的那個圓仍然被裁剪了一些, 這個是由於咱們在使用CALayerDelegate的時候, 並無讓須要描繪的backing image支持超出邊界外的支持.

聊到這裏, 雖然咱們知道了CALayerDelegate, 但在實際開發當中, 咱們基本上很是很是少去接觸它, 由於當UIView建立backing image的時候, 就會默認把CALayerDelegate設置爲它本身, 同時也會提供一個**- (void)displayLayer:(CALayer *)layer;**的實現, 因此基本上不會遇到什麼問題.

當你使用有backing imageUIView時, 你也沒必要實現下面兩個方法

- (void)displayLayer:(CALayer *)layer;
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
複製代碼

由於UIView提供了一個**- (void)drawRect:(CGRect)rect;的方法, 只要你實現了這個方法, 那麼剩下的東西UIView**都是所有幫你完成.


總結一下

不得不說, 此次的內容有些多, 仍是來總結一下:

  • contents是給CALayer設置內容的一個屬性
  • ContentGravity是給CALayer設置內容的顯示, 相似UIViewcontentModel.
  • contentsScale定義圖層的像素和視圖比例大小, 默認大小爲1.0f, 而且是CGFloat類型.
  • maskToBounds是一個BOOL類型, 默認爲NO, 若是設置爲YES, 則會裁剪掉超出視圖的部分.
  • contentsRect是一個座標軸, 默認是CGRectMake(0, 0, 1, 1), 輸入對應的座標軸, 可讓CALayer顯示所輸入座標軸的區域內容.
  • cntentsCenter是用來定義一個固定的邊框和一個圖層上可拉伸的區域.
  • delegate是用來定義CALayerDelegate對象, 當UIView建立CALayer的時候, 默認就會實現, 而且提供一個**- (void)displayLayer:(CALayer *)layer;**方法的實現.

工程地址

項目地址: https://github.com/CainRun/CoreAnimation


最後

碼字很費腦, 看官賞點飯錢可好

微信

支付寶
相關文章
相關標籤/搜索