1. 程式人生 > >圖層樹和寄宿圖--iOS核心動畫系列一

圖層樹和寄宿圖--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提供UIViewCALayer兩個平行的層級關係,應該也是為了解耦,做職責分離。 以便能適應 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,但是這個值不能直接賦值給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.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.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.4 maskToBounds

看上面最新的執行圖,發現圖片超出了檢視的邊界。因為預設情況下,UIView仍會繪製超過邊界的內容,在CALayer也不例外。UIView有個clipsToBounds屬性來決定是否顯示超出邊界的內容。CALayer對應的屬性叫做maskToBounds,把它設定成YES就可以不顯示超出部分的圖片了。

2.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.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];
}

執行的效果如下:

clipboard.png

本來原文是用四張不同的圖做拼接,我只是展示下這種功能實現,所以偷懶只用了一張圖片。如果有不解之處請看原文

2.6 contentsCenter

--- 未完待續