圖層樹和寄宿圖 -- iOS Core Animation 系列一

本系列文章算是一系列讀書筆記,想了解更多,請看原文html

1.圖層樹

1.1 視圖

一個視圖就是在屏幕上顯示的一個矩形塊(好比圖片,文字或者視頻),它可以攔截相似於鼠標點擊或者觸摸手勢等用戶輸入。視圖在層級關係中能夠互相嵌套,一個視圖能夠管理它的全部子視圖的位置。
在iOS中,全部的視圖都是從UIView這個基類派生出來的。UIView能夠處理觸摸時間,支持Core Graphics繪圖,能夠仿射變換等等操做。ios

1.2 CALayer

CALayer平時你們也很常見,好比簡單的設置個圓角,或者邊線等操做都會用到。CALayer類在概念上和UIView相似,也是一些被層級關係樹管理的矩形塊,也能夠包含一些內容,而且管理子視圖的位置。git

UIView最大的區別是CALayer不能處理用戶的操做交互緩存

CALayer不清楚具體的響應鏈,可是它提供了一些方法來判斷是否某個觸點在某個圖層範圍內。性能

1.3 平行的層級關係

每一個UIView都對應着一個CALayer,視圖的職責是建立並管理這個圖層,以確保黨子視圖在層級關係中添加或者被移除的時候,他們對應的圖層也一樣的在對應的層級關係樹中有相同的操做。動畫

真正用來在屏幕上顯示的是圖層(CALayer),UIView是對它的一個封裝,提供一些交互觸摸功能,和一些Core Animation底層的接口。atom

iO S提供UIViewCALayer兩個平行的層級關係,應該也是爲了解耦,作職責分離。 以便能適應 iOS 和 Mac OS 的系統。spa

對於簡單的需求咱們無需深刻了解CALayer使用UIView就很方便靈活了。可是有時候咱們只使用UIView仍是會有些捉襟見肘的,CALayer暴露了一些UIView沒有提供的功能:代理

  • 陰影、圓角、邊框
  • 3D變換
  • 非矩形範圍
  • 透明遮罩
  • 非線性動畫

2.寄宿圖

2.1 contents屬性

CALayer有一個屬性叫作contents,這個屬性是id類型的,能夠是任何類型的對象。也便是意味着在寫代碼的時候,能夠給contents賦任何值(顯示不顯示是另外一回事)。只有賦CGImage的時候才能正確顯示。指針

contents 這個奇怪的表現是由 Mac OS 的歷史緣由形成的,由於在 Mac OS 系統上,這個屬性對 CGImageNSImage 類型的值都起做用。可是在 iOS上,若是將 UIImage 的值賦給它,只能獲得一個空白的圖層。

事實上,真正賦值的類型應該是CGImageRef,這是一個指向CGImage結構的指針。UIImage有一個CGImage屬性,它返回一個CGImageRef,可是這個值不能直接賦值給CALayercontents,由於CGImageRef不是一個真正的Cocoa對象,而是Core Foundation類型。

Core FoundationCocoa對象是不兼容的,能夠經過bridged轉換:

layer.contents = (__bridge id)image.CGImage;
2.1.1 示例

既然CALayercontents能夠賦值各類類型,咱們能夠嘗試一下用CALayer實現UIImageView的效果。代碼以下:

@implementation ViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  self.view.backgroundColor = [UIColor lightGrayColor];
  
  UIView *layerView = [[UIView alloc] initWithFrame:CGRectMake(100, 200, 50, 100)];
  layerView.backgroundColor = [UIColor whiteColor];
  [self.view addSubview:layerView];
  
  
  UIImage *image = [UIImage imageNamed:@"test"];
  layerView.layer.contents = (__bridge id)image.CGImage;
}

運行一下,效果以下:

clipboard.png

雖然能夠實現相似UIImageView的顯示效果,但日常並不推薦使用這種方法。

2.1.2 contentGravity

上面示例的圖片有點扁,由於咱們設置的frame是個長方形,而圖片自己是一個正方形。因此被擠壓了。平時使用UIImageView時遇到相似狀況,能夠設置contentMode來解決。一樣:

layerView.contentMode = UIViewContentModeScaleAspectFill;

這樣就能夠解決了。

UIView大多數視覺相關的屬性好比contentMode,對這些屬性的操做實際上是對對應圖層的操做。
CALayercontentMode對應的屬性叫作contentsGravity,這是一個NSString類型,而UIKit部分是枚舉。contentsGravity可選的常量值有以下:

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

contentMode同樣, contentsGravity目的是決定內容在圖層中怎麼對齊,將上面設置contentMode的代碼能夠替換以下:

layerView.layer.contentsGravity = kCAGravityResizeAspectFill;

運行後的效果是一致的。

2.1.3 contentsScale

contentsScale屬性定義了寄宿圖的像素尺寸和視圖大小的比例,默認狀況下是一個1.0的浮點數。
contentsScale並非總會對寄宿圖的效果有影響,由於contents設置了contentsGravity屬性,致使常常設置了contentsScale卻沒反應。

若是單純的想放大圖層的 contents圖片,可使用圖層的 transformaffineTransform

contentsScale其實屬於支持高分辨率屏幕機制的一部分,是用來判斷在繪製圖層的時候應該爲寄宿圖建立的空間大小,和須要顯示的圖片拉伸度(假設沒有設置contentsGravity)。UIView有一個相似可是不多用的contentScaleFactor屬性。
若是contentsScale設置爲1.0,將會以每一個點1個像素繪製圖片,若是2.0,則以每一個點2個像素繪製圖片(這就是Retina屏)。
修改contentsScale並不會對咱們使用kCAGravityResizeAspectFill有影響,由於kCAGravityResizeAspectFill就是拉伸圖片適應圖層而已。可是若是把contentsGravity設置成kCAGravityCenter(這個值不會拉伸圖片),變化見下圖:

clipboard.png

如圖所示,圖片會變的有點大,並且有像素的顆粒感。由於CGImageUIImage不同,它沒有拉伸的感念。用UIImage讀取圖片時,讀取了高質量的Retina圖片。但用CGImage設置的時候,拉伸的概念就被丟失了,不過能夠手動設置contentsScale來作到一樣效果:

layerView.layer.contentsScale = [UIScreen mainScreen].scale;

如今效果以下:

clipboard.png
爲了突出layerView的存在感,我把layerViewframe調整到CGRectMake(100, 200, 100, 150)

2.1.4 maskToBounds

看上面最新的運行圖,發現圖片超出了視圖的邊界。由於默認狀況下,UIView仍會繪製超過邊界的內容,在CALayer也不例外。
UIView有個clipsToBounds屬性來決定是否顯示超出邊界的內容。CALayer對應的屬性叫作maskToBounds,把它設置成YES就能夠不顯示超出部分的圖片了。

2.1.5 contentsRect

CALayercontentsRect屬性容許咱們在圖層邊框裏顯示寄宿圖的一個子域。和boundsframe不一樣,contentsRect不是按點來計算的。它使用單位座標。單位座標指定在0到1以前,是一個相對值(像素和點就是絕對值)。

默認的contentsRect{0, 0, 1, 1},意味着整個寄宿圖默認都是課件的。若是指定小一點的矩形,圖片就會被裁剪:

clipboard.png

上圖設置的 contentsRect{0, 0, 0.5, 0.5}

事實上contentsRect設置一個負數的原點或者大於{1, 1}的尺寸也是能夠的。這種狀況下,最外面的像素會被拉伸。

contentsRect在 App 中最有趣的地方能夠用做 image sprites(圖片拼合)。圖片拼合後能夠打包到一張大圖上一次載入,相比屢次載入不一樣的圖片,這樣作的性能更優。

2.1.6 圖片拼接代碼示例:
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *view1;
@property (weak, nonatomic) IBOutlet UIView *view2;
@property (weak, nonatomic) IBOutlet UIView *view3;
@property (weak, nonatomic) IBOutlet UIView *view4;


@end

@implementation ViewController

- (void)addSpriteImage:(UIImage *)image withContentRect:(CGRect)rect toLayer:(CALayer *)layer //set image
{
  layer.contents = (__bridge id)image.CGImage;
  
  //scale contents to fit
  layer.contentsGravity = kCAGravityResizeAspect;
  
  //set contentsRect
  layer.contentsRect = rect;
}

- (void)viewDidLoad {
  [super viewDidLoad];
  
  UIImage *image = [UIImage imageNamed:@"test_1"];
  [self addSpriteImage:image withContentRect:CGRectMake(0, 0, 0.5, 0.5) toLayer:self.view1.layer];

  [self addSpriteImage:image withContentRect:CGRectMake(0.5, 0, 0.5, 0.5) toLayer:self.view2.layer];

  [self addSpriteImage:image withContentRect:CGRectMake(0, 0.5, 0.5, 0.5) toLayer:self.view3.layer];

  [self addSpriteImage:image withContentRect:CGRectMake(0.5, 0.5, 0.5, 0.5) toLayer:self.view4.layer];
}

運行的效果以下:

clipboard.png

原本原文是用四張不一樣的圖作拼接,我只是展現下這種功能實現,因此偷懶只用了一張圖片。若是有不解之處請看原文

2.1.7 contentsCenter

contentsCenter看名字大部分人會誤覺得是和位置有關,其實它是一個CGRect。它定義了一個苦丁的邊框和在圖層上可拉伸的區域。
默認狀況下,contentsCenter{0, 0, 1, 1},意味着若是大小改變(contentsGravity),寄宿圖會被均勻的拉伸。
假設咱們增長原點的值,並減少尺寸的值,例如將它變爲{0.25, 0.25, 0.5, 0.5}將會在寄宿圖周圍留出一個邊框。以下圖:

clipboard.png
上圖是借用原書的圖

這效果看起來和UIImage裏的resizableImageWithCapInsets:很是相似,它能夠運用到任何寄宿圖,包括在Core Graphics運行時繪製的圖形。

clipboard.png
同一圖片使用不一樣的 contentsCenter

contentsCenter使用起來也很方便,能夠用代碼:

layer.contentsCenter = CGRectMake(0.25, 0.25, 0.5, 0.5);

也能夠在XIB裏面設置:

clipboard.png

2.2 Custom Drawing

除了給contents賦值CGImage來設置寄宿圖以外,還能夠直接用Core Graphics來繪製寄宿圖。

-drawRect: 經過繼承UIView來實現此方法進行自定義繪製。這個方法默認是沒有被實現的。由於對於UIView來講,寄宿圖不是必須的。若是UIView檢測到-drawRect:被調用,會自動給視圖分配一個寄宿圖。這個寄宿圖的像素尺寸等於視圖大小乘以contentsScale

若是你不須要寄宿圖,不要寫這個方法,會形成資源浪費,詳細部分見 《內存惡鬼drawRect》

視圖在屏幕上出現的時候-drawRect:會自動被調用。-drawRect:方法裏面的代碼利用Core Graphics繪製一個寄宿圖,而後被緩存起來直到須要被更顯(通常是調用了- setNeedDisplay方法)。

CALayer有一個可選的delegate屬性<CALayerDelegate>,當CALayer須要內容的時候,會從這個delegate裏面查詢。
當須要被重繪時,CALayer會從下面這個代理方法請求一個寄宿圖來展現:

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

若是這個方法沒有被實現,CALayer會嘗試下面這個:

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

drawLayer:被調用以前,CALayer建立了一個合適尺寸的寄宿圖(尺寸由boundscontentsScale決定)和一個Core Graphics的繪製上下文環境,並做爲ctx傳入。

2.2.1示例:

下面咱們使用CALayerDelegate是作個示例。

- (void)viewDidLoad {
  [super viewDidLoad];
  
  UIView *layerView = [[UIView alloc] initWithFrame:CGRectMake(100, 200, 150, 150)];
  layerView.backgroundColor = [UIColor lightGrayColor];
  [self.view addSubview:layerView];
  
  CALayer *blueLayer = [CALayer layer];
  blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
  blueLayer.backgroundColor = [UIColor blueColor].CGColor;
  
  blueLayer.delegate = self;
  
  blueLayer.contentsScale = [UIScreen mainScreen].scale;
  [layerView.layer addSublayer:blueLayer];
  //
  [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);
}

clipboard.png

  • blueLayer上顯式調用了-display。由於當圖層顯示在屏幕上時,CALayer不會自動重繪,這和UIView不一樣。須要手動調用。
  • 咱們沒有調用masksToBounds。可是繪製的圓仍然被裁剪了。這是由於咱們在CALayerDelegate方法中,沒有對超出邊界歪的內容提供繪製支持。

除非建立一個單獨的圖層,咱們平時基本不會用到CALayerDelegate。由於UIView在建立時,會自動的吧圖層的代理設置爲本身,而後提供了一個-displayLayer:方法實現。


- 系列一完 -

相關文章
相關標籤/搜索