本系列文章算是一系列讀書筆記,想了解更多,請看原文html
一個視圖就是在屏幕上顯示的一個矩形塊(好比圖片,文字或者視頻),它可以攔截相似於鼠標點擊或者觸摸手勢等用戶輸入。視圖在層級關係中能夠互相嵌套,一個視圖能夠管理它的全部子視圖的位置。
在iOS中,全部的視圖都是從UIView
這個基類派生出來的。UIView
能夠處理觸摸時間,支持Core Graphics
繪圖,能夠仿射變換等等操做。ios
CALayer
平時你們也很常見,好比簡單的設置個圓角,或者邊線等操做都會用到。CALayer
類在概念上和UIView
相似,也是一些被層級關係樹管理的矩形塊,也能夠包含一些內容,而且管理子視圖的位置。git
和UIView
最大的區別是CALayer
不能處理用戶的操做交互緩存
CALayer
不清楚具體的響應鏈,可是它提供了一些方法來判斷是否某個觸點在某個圖層範圍內。性能
每一個UIView
都對應着一個CALayer
,視圖的職責是建立並管理這個圖層,以確保黨子視圖在層級關係中添加或者被移除的時候,他們對應的圖層也一樣的在對應的層級關係樹中有相同的操做。動畫
真正用來在屏幕上顯示的是圖層(CALayer
),UIView
是對它的一個封裝,提供一些交互觸摸功能,和一些Core Animation
底層的接口。atom
iO S提供UIView
和CALayer
兩個平行的層級關係,應該也是爲了解耦,作職責分離。 以便能適應 iOS 和 Mac OS 的系統。spa
對於簡單的需求咱們無需深刻了解
CALayer
使用UIView
就很方便靈活了。可是有時候咱們只使用UIView
仍是會有些捉襟見肘的,CALayer
暴露了一些UIView
沒有提供的功能:代理
- 陰影、圓角、邊框
- 3D變換
- 非矩形範圍
- 透明遮罩
- 非線性動畫
CALayer
有一個屬性叫作contents
,這個屬性是id
類型的,能夠是任何類型的對象。也便是意味着在寫代碼的時候,能夠給contents
賦任何值(顯示不顯示是另外一回事)。只有賦CGImage
的時候才能正確顯示。指針
contents
這個奇怪的表現是由 Mac OS 的歷史緣由形成的,由於在 Mac OS 系統上,這個屬性對CGImage
和NSImage
類型的值都起做用。可是在 iOS上,若是將UIImage
的值賦給它,只能獲得一個空白的圖層。
事實上,真正賦值的類型應該是CGImageRef
,這是一個指向CGImage
結構的指針。UIImage
有一個CGImage
屬性,它返回一個CGImageRef
,可是這個值不能直接賦值給CALayer
的contents
,由於CGImageRef
不是一個真正的Cocoa
對象,而是Core Foundation
類型。
Core Foundation
和Cocoa
對象是不兼容的,能夠經過bridged
轉換:layer.contents = (__bridge id)image.CGImage;
既然CALayer
的contents
能夠賦值各類類型,咱們能夠嘗試一下用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; }
運行一下,效果以下:
雖然能夠實現相似UIImageView
的顯示效果,但日常並不推薦使用這種方法。
上面示例的圖片有點扁,由於咱們設置的frame
是個長方形,而圖片自己是一個正方形。因此被擠壓了。平時使用UIImageView
時遇到相似狀況,能夠設置contentMode
來解決。一樣:
layerView.contentMode = UIViewContentModeScaleAspectFill;
這樣就能夠解決了。
UIView
大多數視覺相關的屬性好比contentMode
,對這些屬性的操做實際上是對對應圖層的操做。CALayer
與contentMode
對應的屬性叫作contentsGravity
,這是一個NSString
類型,而UIKit
部分是枚舉。contentsGravity
可選的常量值有以下:
和contentMode
同樣, contentsGravity
目的是決定內容在圖層中怎麼對齊,將上面設置contentMode
的代碼能夠替換以下:
layerView.layer.contentsGravity = kCAGravityResizeAspectFill;
運行後的效果是一致的。
contentsScale
屬性定義了寄宿圖的像素尺寸和視圖大小的比例,默認狀況下是一個1.0
的浮點數。contentsScale
並非總會對寄宿圖的效果有影響,由於contents
設置了contentsGravity
屬性,致使常常設置了contentsScale
卻沒反應。
若是單純的想放大圖層的contents
圖片,可使用圖層的transform
和affineTransform
。
contentsScale
其實屬於支持高分辨率屏幕機制的一部分,是用來判斷在繪製圖層的時候應該爲寄宿圖建立的空間大小,和須要顯示的圖片拉伸度(假設沒有設置contentsGravity
)。UIView
有一個相似可是不多用的contentScaleFactor
屬性。
若是contentsScale
設置爲1.0,將會以每一個點1個像素繪製圖片,若是2.0,則以每一個點2個像素繪製圖片(這就是Retina屏)。
修改contentsScale
並不會對咱們使用kCAGravityResizeAspectFill
有影響,由於kCAGravityResizeAspectFill
就是拉伸圖片適應圖層而已。可是若是把contentsGravity
設置成kCAGravityCenter
(這個值不會拉伸圖片),變化見下圖:
如圖所示,圖片會變的有點大,並且有像素的顆粒感。由於CGImage
和UIImage
不同,它沒有拉伸的感念。用UIImage
讀取圖片時,讀取了高質量的Retina圖片。但用CGImage
設置的時候,拉伸的概念就被丟失了,不過能夠手動設置contentsScale
來作到一樣效果:
layerView.layer.contentsScale = [UIScreen mainScreen].scale;
如今效果以下:
爲了突出layerView
的存在感,我把layerView
的frame
調整到CGRectMake(100, 200, 100, 150)
。
看上面最新的運行圖,發現圖片超出了視圖的邊界。由於默認狀況下,UIView
仍會繪製超過邊界的內容,在CALayer
也不例外。UIView
有個clipsToBounds
屬性來決定是否顯示超出邊界的內容。CALayer
對應的屬性叫作maskToBounds
,把它設置成YES
就能夠不顯示超出部分的圖片了。
CALayer
的contentsRect
屬性容許咱們在圖層邊框裏顯示寄宿圖的一個子域。和bounds
、frame
不一樣,contentsRect
不是按點來計算的。它使用單位座標。單位座標指定在0到1以前,是一個相對值(像素和點就是絕對值)。
默認的contentsRect
是{0, 0, 1, 1}
,意味着整個寄宿圖默認都是課件的。若是指定小一點的矩形,圖片就會被裁剪:
上圖設置的contentsRect
是{0, 0, 0.5, 0.5}
事實上contentsRect
設置一個負數的原點或者大於{1, 1}
的尺寸也是能夠的。這種狀況下,最外面的像素會被拉伸。
contentsRect
在 App 中最有趣的地方能夠用做 image sprites(圖片拼合)。圖片拼合後能夠打包到一張大圖上一次載入,相比屢次載入不一樣的圖片,這樣作的性能更優。
@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]; }
運行的效果以下:
原本原文是用四張不一樣的圖作拼接,我只是展現下這種功能實現,因此偷懶只用了一張圖片。若是有不解之處請看原文
contentsCenter
看名字大部分人會誤覺得是和位置有關,其實它是一個CGRect
。它定義了一個苦丁的邊框和在圖層上可拉伸的區域。
默認狀況下,contentsCenter
是{0, 0, 1, 1}
,意味着若是大小改變(contentsGravity
),寄宿圖會被均勻的拉伸。
假設咱們增長原點的值,並減少尺寸的值,例如將它變爲{0.25, 0.25, 0.5, 0.5}
將會在寄宿圖周圍留出一個邊框。以下圖:
上圖是借用原書的圖。
這效果看起來和UIImage
裏的resizableImageWithCapInsets:
很是相似,它能夠運用到任何寄宿圖,包括在Core Graphics
運行時繪製的圖形。
同一圖片使用不一樣的contentsCenter
。
contentsCenter
使用起來也很方便,能夠用代碼:
layer.contentsCenter = CGRectMake(0.25, 0.25, 0.5, 0.5);
也能夠在XIB裏面設置:
除了給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
建立了一個合適尺寸的寄宿圖(尺寸由bounds
和contentsScale
決定)和一個Core Graphics
的繪製上下文環境,並做爲ctx
傳入。
下面咱們使用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); }
- 在
blueLayer
上顯式調用了-display
。由於當圖層顯示在屏幕上時,CALayer
不會自動重繪,這和UIView
不一樣。須要手動調用。- 咱們沒有調用
masksToBounds
。可是繪製的圓仍然被裁剪了。這是由於咱們在CALayerDelegate
方法中,沒有對超出邊界歪的內容提供繪製支持。
除非建立一個單獨的圖層,咱們平時基本不會用到CALayerDelegate
。由於UIView
在建立時,會自動的吧圖層的代理設置爲本身,而後提供了一個-displayLayer:
方法實現。