圖層樹和寄宿圖--iOS核心動畫系列一
本系列文章算是一系列讀書筆記,想了解更多,請看[原文][1]
1.圖層樹
1.1 檢視
一個檢視就是在螢幕上顯示的一個矩形塊(比如圖片,文字或者視訊),它能夠攔截類似於滑鼠點選或者觸控手勢等使用者輸入。檢視在層級關係中可以互相巢狀,一個檢視可以管理它的所有子檢視的位置。在iOS中,所有的檢視都是從UIView
這個基類派生出來的。UIView
可以處理觸控時間,支援Core Graphics
繪圖,可以仿射變換等等操作。
1.2 CALayer
CALayer
平時大家也很常見,比如簡單的設定個圓角,或者邊線等操作都會用到。CALayer
類在概念上和UIView
類似,也是一些被層級關係樹管理的矩形塊,也可以包含一些內容,並且管理子檢視的位置。
和UIView
最大的區別是CALayer
不能處理使用者的操作互動
CALayer
不清楚具體的響應鏈,但是它提供了一些方法來判斷是否某個觸點在某個圖層範圍內。
1.3 平行的層級關係
每個UIView
都對應著一個CALayer
,檢視的職責是建立並管理這個圖層,以確保黨子檢視在層級關係中新增或者被移除的時候,他們對應的圖層也同樣的在對應的層級關係樹中有相同的操作。
真正用來在螢幕上顯示的是圖層(CALayer
),UIView
是對它的一個封裝,提供一些互動觸控功能,和一些Core Animation
底層的介面。
iO S提供UIView
和CALayer
兩個平行的層級關係,應該也是為了解耦,做職責分離。 以便能適應 iOS 和 Mac OS 的系統。
對於簡單的需求我們無需深入瞭解
CALayer
使用UIView
就很方便靈活了。但是有時候我們只使用UIView
還是會有些捉襟見肘的,CALayer
暴露了一些UIView
沒有提供的功能:
- 陰影、圓角、邊框
- 3D變換
- 非矩形範圍
- 透明遮罩
- 非線性動畫
2.寄宿圖
2.1 contents屬性
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;
2.1.1 示例
既然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
的顯示效果,但平常並不推薦使用這種方法。
2.2 contentGravity
上面示例的圖片有點扁,因為我們設定的frame
是個長方形,而圖片本身是一個正方形。所以被擠壓了。平時使用UIImageView
時遇到類似情況,可以設定contentMode
來解決。同樣:
layerView.contentMode = UIViewContentModeScaleAspectFill;
這樣就可以解決了。
UIView
大多數視覺相關的屬性比如contentMode
,對這些屬性的操作其實是對對應圖層的操作。CALayer
與contentMode
對應的屬性叫做contentsGravity
,這是一個NSString
型別,而UIKit
部分是列舉。contentsGravity
可選的常量值有如下:
- kCAGravityCenter
- kCAGravityTop
- kCAGravityBottom
- kCAGravityLeft
- kCAGravityRight
- kCAGravityTopLeft
- kCAGravityTopRight
- kCAGravityBottomLeft
- kCAGravityBottomRight
- kCAGravityResize
- kCAGravityResizeAspect
- kCAGravityResizeAspectFill
和contentMode
一樣, contentsGravity
目的是決定內容在圖層中怎麼對齊,將上面設定contentMode
的程式碼可以替換如下:
layerView.layer.contentsGravity = kCAGravityResizeAspectFill;
執行後的效果是一致的。
2.3 contentsScale
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)
。
2.4 maskToBounds
看上面最新的執行圖,發現圖片超出了檢視的邊界。因為預設情況下,UIView
仍會繪製超過邊界的內容,在CALayer
也不例外。UIView
有個clipsToBounds
屬性來決定是否顯示超出邊界的內容。CALayer
對應的屬性叫做maskToBounds
,把它設定成YES
就可以不顯示超出部分的圖片了。
2.5 contentsRect
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(圖片拼合)。圖片拼合後可以打包到一張大圖上一次載入,相比多次載入不同的圖片,這樣做的效能更優。
2.5.1 圖片拼接程式碼示例:
@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];
}
執行的效果如下:
本來原文是用四張不同的圖做拼接,我只是展示下這種功能實現,所以偷懶只用了一張圖片。如果有不解之處請看原文
2.6 contentsCenter
--- 未完待續