CoreAnimation3-專用圖層

CAShapeLayergit

CAShapeLayer是一個經過矢量圖形而不是bitmap來繪製的圖層子類。你指定諸如顏色和線寬等屬性,用CGPath來定義想要繪製的圖形,最後CAShapeLayer就自動渲染出來了。固然,你也能夠用Core Graphics直接向原始的CALyer的內容中繪製一個路徑,相比直下,使用CAShapeLayer有如下一些優勢:github

  • 渲染快速。CAShapeLayer使用了硬件加速,繪製同一圖形會比用Core Graphics快不少。api

  • 高效使用內存。一個CAShapeLayer不須要像普通CALayer同樣建立一個寄宿圖形,因此不管有多大,都不會佔用太多的內存。數組

  • 不會被圖層邊界剪裁掉。一個CAShapeLayer能夠在邊界以外繪製。你的圖層路徑不會像在使用Core Graphics的普通CALayer同樣被剪裁掉(如咱們在第二章所見)。安全

  • 不會出現像素化。當你給CAShapeLayer作3D變換時,它不像一個有寄宿圖的普通圖層同樣變得像素化。多線程

建立一個CGPath併發

CAShapeLayer能夠用來繪製全部可以經過CGPath來表示的形狀。這個形狀不必定要閉合,圖層路徑也不必定要不可破,事實上你能夠在一個圖層上繪製好幾個不一樣的形狀。你能夠控制一些屬性好比lineWith(線寬,用點表示單位),lineCap(線條結尾的樣子),和lineJoin(線條之間的結合點的樣子);可是在圖層層面你只有一次機會設置這些屬性。若是你想用不一樣顏色或風格來繪製多個形狀,就不得不爲每一個形狀準備一個圖層了。app

清單6.1 的代碼用一個CAShapeLayer渲染一個簡單的火柴人。CAShapeLayer屬性是CGPathRef類型,可是咱們用UIBezierPath幫助類建立了圖層路徑,這樣咱們就不用考慮人工釋放CGPath了。圖6.1是代碼運行的結果。雖然還不是很完美,可是總算知道了大意對吧!框架

清單6.1 用CAShapeLayer繪製一個火柴人dom

#import "DrawingView.h"
#import @interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
  [super viewDidLoad];
  //create path
  UIBezierPath *path = [[UIBezierPath alloc] init];
  [path moveToPoint:CGPointMake(175, 100)];
  ?
  [path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:2*M_PI clockwise:YES];
  [path moveToPoint:CGPointMake(150, 125)];
  [path addLineToPoint:CGPointMake(150, 175)];
  [path addLineToPoint:CGPointMake(125, 225)];
  [path moveToPoint:CGPointMake(150, 175)];
  [path addLineToPoint:CGPointMake(175, 225)];
  [path moveToPoint:CGPointMake(100, 150)];
  [path addLineToPoint:CGPointMake(200, 150)];
  //create shape layer
  CAShapeLayer *shapeLayer = [CAShapeLayer layer];
  shapeLayer.strokeColor = [UIColor redColor].CGColor;
  shapeLayer.fillColor = [UIColor clearColor].CGColor;
  shapeLayer.lineWidth = 5;
  shapeLayer.lineJoin = kCALineJoinRound;
  shapeLayer.lineCap = kCALineCapRound;
  shapeLayer.path = path.CGPath;
  //add it to our view
  [self.containerView.layer addSublayer:shapeLayer];
}
@end

圖6.1 用CAShapeLayer繪製一個簡單的火柴人

圓角

第二章裏面提到了CAShapeLayer爲建立圓角視圖提供了一個方法,就是CALayer的cornerRadius屬性(譯者注:實際上是在第四章提到的)。雖然使用CAShapeLayer類須要更多的工做,可是它有一個優點就是能夠單獨指定每一個角。

咱們建立圓角舉行其實就是人工繪製單獨的直線和弧度,可是事實上UIBezierPath有自動繪製圓角矩形的構造方法,下面這段代碼繪製了一個有三個圓角一個直角的矩形:

//define path parameters
CGRect rect = CGRectMake(50, 50, 100, 100);
CGSize radii = CGSizeMake(20, 20);
UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomRight | UIRectCornerBottomLeft;
//create path
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii];

咱們能夠經過這個圖層路徑繪製一個既有直角又有圓角的視圖。若是咱們想依照此圖形來剪裁視圖內容,咱們能夠把CAShapeLayer做爲視圖的宿主圖層,而不是添加一個子視圖(圖層蒙板的詳細解釋見第四章『視覺效果』)。

CATextLayer

用戶界面是沒法從一個單獨的圖片裏面構建的。一個設計良好的圖標可以很好地表現一個按鈕或控件的意圖,不過你早晚都要須要一個不錯的老式風格的文本標籤。

若是你想在一個圖層裏面顯示文字,徹底能夠藉助圖層代理直接將字符串使用Core Graphics寫入圖層的內容(這就是UILabel的精髓)。若是越過寄宿於圖層的視圖,直接在圖層上操做,那其實至關繁瑣。你要爲每個顯示文字的圖層建立一個能像圖層代理同樣工做的類,還要邏輯上判斷哪一個圖層須要顯示哪一個字符串,更別提還要記錄不一樣的字體,顏色等一系列亂七八糟的東西。

萬幸的是這些都是沒必要要的,Core Animation提供了一個CALayer的子類CATextLayer,它以圖層的形式包含了UILabel幾乎全部的繪製特性,而且額外提供了一些新的特性。

一樣,CATextLayer也要比UILabel渲染得快得多。不多有人知道在iOS 6及以前的版本,UILabel實際上是經過WebKit來實現繪製的,這樣就形成了當有不少文字的時候就會有極大的性能壓力。而CATextLayer使用了Core text,而且渲染得很是快。

讓咱們來嘗試用CATextLayer來顯示一些文字。清單6.2的代碼實現了這一功能,結果如圖6.2所示。

清單6.2 用CATextLayer來實現一個UILabel

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *labelView;
@end
@implementation ViewController
- (void)viewDidLoad
{
  [super viewDidLoad];
  //create a text layer
  CATextLayer *textLayer = [CATextLayer layer];
  textLayer.frame = self.labelView.bounds;
  [self.labelView.layer addSublayer:textLayer];
  //set text attributes
  textLayer.foregroundColor = [UIColor blackColor].CGColor;
  textLayer.alignmentMode = kCAAlignmentJustified;
  textLayer.wrapped = YES;
  //choose a font
  UIFont *font = [UIFont systemFontOfSize:15];
  //set layer font
  CFStringRef fontName = (__bridge CFStringRef)font.fontName;
  CGFontRef fontRef = CGFontCreateWithFontName(fontName);
  textLayer.font = fontRef;
  textLayer.fontSize = font.pointSize;
  CGFontRelease(fontRef);
  //choose some text
  NSString *text = @"Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis";
  //set layer text
  textLayer.string = text;
}
@end

圖6.2 用CATextLayer來顯示一個純文本標籤

若是你自習看這個文本,你會發現一個奇怪的地方:這些文本有一些像素化了。這是由於並無以Retina的方式渲染,第二章提到了這個contentScale屬性,用來決定圖層內容應該以怎樣的分辨率來渲染。contentsScale並不關心屏幕的拉伸因素而老是默認爲1.0。若是咱們想以Retina的質量來顯示文字,咱們就得手動地設置CATextLayer的contentsScale屬性,以下:

textLayer.contentsScale = [UIScreen mainScreen].scale;

圖6.3 設置contentsScale來匹配屏幕

CATextLayer的font屬性不是一個UIFont類型,而是一個CFTypeRef類型。這樣能夠根據你的具體須要來決定字體屬性應該是用CGFontRef類型仍是CTFontRef類型(Core Text字體)。同時字體大小也是用fontSize屬性單獨設置的,由於CTFontRef和CGFontRef並不像UIFont同樣包含點大小。這個例子會告訴你如何將UIFont轉換成CGFontRef。

另外,CATextLayer的string屬性並非你想象的NSString類型,而是id類型。這樣你既能夠用NSString也能夠用NSAttributedString來指定文本了(注意,NSAttributedString並非NSString的子類)。屬性化字符串是iOS用來渲染字體風格的機制,它以特定的方式來決定指定範圍內的字符串的原始信息,好比字體,顏色,字重,斜體等。

富文本

iOS 6中,Apple給UILabel和其餘UIKit文本視圖添加了直接的屬性化字符串的支持,應該說這是一個很方便的特性。不過事實上從iOS3.2開始CATextLayer就已經支持屬性化字符串了。這樣的話,若是你想要支持更低版本的iOS系統,CATextLayer無疑是你向界面中增長富文本的好辦法,並且也不用去跟複雜的Core Text打交道,也省了用UIWebView的麻煩。

讓咱們編輯一下示例使用到NSAttributedString(見清單6.3).iOS 6及以上咱們能夠用新的NSTextAttributeName實例來設置咱們的字符串屬性,可是練習的目的是爲了演示在iOS 5及如下,因此咱們用了Core Text,也就是說你須要把Core Text framework添加到你的項目中。不然,編譯器是沒法識別屬性常量的。

圖6.4是代碼運行結果(注意那個紅色的下劃線文本)

清單6.3 用NSAttributedString實現一個富文本標籤。

#import "DrawingView.h"
#import #import @interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *labelView;
@end
@implementation ViewController
- (void)viewDidLoad
{
  [super viewDidLoad];
  //create a text layer
  CATextLayer *textLayer = [CATextLayer layer];
  textLayer.frame = self.labelView.bounds;
  textLayer.contentsScale = [UIScreen mainScreen].scale;
  [self.labelView.layer addSublayer:textLayer];
  //set text attributes
  textLayer.alignmentMode = kCAAlignmentJustified;
  textLayer.wrapped = YES;
  //choose a font
  UIFont *font = [UIFont systemFontOfSize:15];
  //choose some text
  NSString *text = @"Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc \ elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis";
  ?
  //create attributed string
  NSMutableAttributedString *string = nil;
  string = [[NSMutableAttributedString alloc] initWithString:text];
  //convert UIFont to a CTFont
  CFStringRef fontName = (__bridge CFStringRef)font.fontName;
  CGFloat fontSize = font.pointSize;
  CTFontRef fontRef = CTFontCreateWithName(fontName, fontSize, NULL);
  //set text attributes
  NSDictionary *attribs = @{
    (__bridge id)kCTForegroundColorAttributeName:(__bridge id)[UIColor blackColor].CGColor,
    (__bridge id)kCTFontAttributeName: (__bridge id)fontRef
  };
  [string setAttributes:attribs range:NSMakeRange(0, [text length])];
  attribs = @{
    (__bridge id)kCTForegroundColorAttributeName: (__bridge id)[UIColor redColor].CGColor,
    (__bridge id)kCTUnderlineStyleAttributeName: @(kCTUnderlineStyleSingle),
    (__bridge id)kCTFontAttributeName: (__bridge id)fontRef
  };
  [string setAttributes:attribs range:NSMakeRange(6, 5)];
  //release the CTFont we created earlier
  CFRelease(fontRef);
  //set layer text
  textLayer.string = string;
}
@end

圖6.4 用CATextLayer實現一個富文本標籤。

行距和字距

有必要提一下的是,因爲繪製的實現機制不一樣(Core Text和WebKit),用CATextLayer渲染和用UILabel渲染出的文本行距和字距也不是不盡相同的。

兩者的差別程度(由使用的字體和字符決定)總的來講挺小,可是若是你想正確的顯示普通便籤和CATextLayer就必定要記住這一點。

UILabel的替代品

咱們已經證明了CATextLayer比UILabel有着更好的性能表現,同時還有額外的佈局選項而且在iOS 5上支持富文本。可是與通常的標籤比較而言會更加繁瑣一些。若是咱們真的在需求一個UILabel的可用替代品,最好是可以在Interface Builder上建立咱們的標籤,並且儘量地像通常的視圖同樣正常工做。

咱們應該繼承UILabel,而後添加一個子圖層CATextLayer並重寫顯示文本的方法。可是仍然會有由UILabel的-drawRect:方法建立的空寄宿圖。並且因爲CALayer不支持自動縮放和自動佈局,子視圖並非主動跟蹤視圖邊界的大小,因此每次視圖大小被更改,咱們不得不手動更新子圖層的邊界。

咱們真正想要的是一個用CATextLayer做爲宿主圖層的UILabel子類,這樣就能夠隨着視圖自動調整大小並且也沒有冗餘的寄宿圖啦。

就像咱們在第一章『圖層樹』討論的同樣,每個UIView都是寄宿在一個CALayer的示例上。這個圖層是由視圖自動建立和管理的,那咱們能夠用別的圖層類型替代它麼?一旦被建立,咱們就沒法代替這個圖層了。可是若是咱們繼承了UIView,那咱們就能夠重寫+layerClass方法使得在建立的時候能返回一個不一樣的圖層子類。UIView會在初始化的時候調用+layerClass方法,而後用它的返回類型來建立宿主圖層。

清單6.4 演示了一個UILabel子類LayerLabel用CATextLayer繪製它的問題,而不是調用通常的UILabel使用的較慢的-drawRect:方法。LayerLabel示例既能夠用代碼實現,也能夠在Interface Builder實現,只要把普通的標籤拖入視圖之中,而後設置它的類是LayerLabel就能夠了。

清單6.4 使用CATextLayer的UILabel子類:LayerLabel

#import "LayerLabel.h"
#import @implementation LayerLabel
+ (Class)layerClass
{
  //this makes our label create a CATextLayer //instead of a regular CALayer for its backing layer
  return [CATextLayer class];
}
- (CATextLayer *)textLayer
{
  return (CATextLayer *)self.layer;
}
- (void)setUp
{
  //set defaults from UILabel settings
  self.text = self.text;
  self.textColor = self.textColor;
  self.font = self.font;
  //we should really derive these from the UILabel settings too
  //but that's complicated, so for now we'll just hard-code them
  [self textLayer].alignmentMode = kCAAlignmentJustified;
  ?
  [self textLayer].wrapped = YES;
  [self.layer display];
}
- (id)initWithFrame:(CGRect)frame
{
  //called when creating label programmatically
  if (self = [super initWithFrame:frame]) {
    [self setUp];
  }
  return self;
}
- (void)awakeFromNib
{
  //called when creating label using Interface Builder
  [self setUp];
}
- (void)setText:(NSString *)text
{
  super.text = text;
  //set layer text
  [self textLayer].string = text;
}
- (void)setTextColor:(UIColor *)textColor
{
  super.textColor = textColor;
  //set layer text color
  [self textLayer].foregroundColor = textColor.CGColor;
}
- (void)setFont:(UIFont *)font
{
  super.font = font;
  //set layer font
  CFStringRef fontName = (__bridge CFStringRef)font.fontName;
  CGFontRef fontRef = CGFontCreateWithFontName(fontName);
  [self textLayer].font = fontRef;
  [self textLayer].fontSize = font.pointSize;
  ?
  CGFontRelease(fontRef);
}
@end

若是你運行代碼,你會發現文本並無像素化,而咱們也沒有設置contentsScale屬性。把CATextLayer做爲宿主圖層的另外一好處就是視圖自動設置了contentsScale屬性。

在這個簡單的例子中,咱們只是實現了UILabel的一部分風格和佈局屬性,不過稍微再改進一下咱們就能夠建立一個支持UILabel全部功能甚至更多功能的LayerLabel類(你能夠在一些線上的開源項目中找到)。

若是你打算支持iOS 6及以上,基於CATextLayer的標籤可能就有有些侷限性。可是總得來講,若是想在app裏面充分利用CALayer子類,用+layerClass來建立基於不一樣圖層的視圖是一個簡單可複用的方法。

 

CATransformLayer

當咱們在構造複雜的3D事物的時候,若是可以組織獨立元素就太方便了。好比說,你想創造一個孩子的手臂:你就須要肯定哪一部分是孩子的手腕,哪一部分是孩子的前臂,哪一部分是孩子的肘,哪一部分是孩子的上臂,哪一部分是孩子的肩膀等等。

固然是容許獨立地移動每一個區域的啦。以肘爲指點會移動前臂和手,而不是肩膀。Core Animation圖層很容易就可讓你在2D環境下作出這樣的層級體系下的變換,可是3D狀況下就不太可能,由於全部的圖層都把他的孩子都平面化到一個場景中(第五章『變換』有提到)。

CATransformLayer解決了這個問題,CATransformLayer不一樣於普通的CALayer,由於它不能顯示它本身的內容。只有當存在了一個能做用域子圖層的變換它才真正存在。CATransformLayer並不平面化它的子圖層,因此它可以用於構造一個層級的3D結構,好比個人手臂示例。

用代碼建立一個手臂須要至關多的代碼,因此我就演示得更簡單一些吧:在第五章的立方體示例,咱們將經過旋轉camara來解決圖層平面化問題而不是像立方體示例代碼中用的sublayerTransform。這是一個很是不錯的技巧,可是隻能做用域單個對象上,若是你的場景包含兩個立方體,那咱們就不能用這個技巧單獨旋轉他們了。

那麼,就讓咱們來試一試CATransformLayer吧,第一個問題就來了:在第五章,咱們是用多個視圖來構造了咱們的立方體,而不是單獨的圖層。咱們不能在不打亂已有的視圖層次的前提下在一個自己不是有寄宿圖的圖層中放置一個寄宿圖圖層。咱們能夠建立一個新的UIView子類寄宿在CATransformLayer(用+layerClass方法)之上。可是,爲了簡化案例,咱們僅僅重建了一個單獨的圖層,而不是使用視圖。這意味着咱們不能像第五章同樣在立方體表面顯示按鈕和標籤,不過咱們如今也用不到這個特性。

清單6.5就是代碼。咱們以咱們在第五章使用過的相同基本邏輯放置立方體。可是並不像之前那樣直接將立方面添加到容器視圖的宿主圖層,咱們將他們放置到一個CATransformLayer中建立一個獨立的立方體對象,而後將兩個這樣的立方體放進容器中。咱們隨機地給立方面染色以將他們區分開來,這樣就不用靠標籤或是光亮來區分他們。圖6.5是運行結果。

清單6.5 用CATransformLayer裝配一個3D圖層體系

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (CALayer *)faceWithTransform:(CATransform3D)transform
{
  //create cube face layer
  CALayer *face = [CALayer layer];
  face.frame = CGRectMake(-50, -50, 100, 100);
  //apply a random color
  CGFloat red = (rand() / (double)INT_MAX);
  CGFloat green = (rand() / (double)INT_MAX);
  CGFloat blue = (rand() / (double)INT_MAX);
  face.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
  ?//apply the transform and return
  face.transform = transform;
  return face;
}
- (CALayer *)cubeWithTransform:(CATransform3D)transform
{
  //create cube layer
  CATransformLayer *cube = [CATransformLayer layer];
  //add cube face 1
  CATransform3D ct = CATransform3DMakeTranslation(0, 0, 50);
  [cube addSublayer:[self faceWithTransform:ct]];
  //add cube face 2
  ct = CATransform3DMakeTranslation(50, 0, 0);
  ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);
  [cube addSublayer:[self faceWithTransform:ct]];
  //add cube face 3
  ct = CATransform3DMakeTranslation(0, -50, 0);
  ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);
  [cube addSublayer:[self faceWithTransform:ct]];
  //add cube face 4
  ct = CATransform3DMakeTranslation(0, 50, 0);
  ct = CATransform3DRotate(ct, -M_PI_2, 1, 0, 0);
  [cube addSublayer:[self faceWithTransform:ct]];
  //add cube face 5
  ct = CATransform3DMakeTranslation(-50, 0, 0);
  ct = CATransform3DRotate(ct, -M_PI_2, 0, 1, 0);
  [cube addSublayer:[self faceWithTransform:ct]];
  //add cube face 6
  ct = CATransform3DMakeTranslation(0, 0, -50);
  ct = CATransform3DRotate(ct, M_PI, 0, 1, 0);
  [cube addSublayer:[self faceWithTransform:ct]];
  //center the cube layer within the container
  CGSize containerSize = self.containerView.bounds.size;
  cube.position = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
  //apply the transform and return
  cube.transform = transform;
  return cube;
}
- (void)viewDidLoad
{?
  [super viewDidLoad];
  //set up the perspective transform
  CATransform3D pt = CATransform3DIdentity;
  pt.m34 = -1.0 / 500.0;
  self.containerView.layer.sublayerTransform = pt;
  //set up the transform for cube 1 and add it
  CATransform3D c1t = CATransform3DIdentity;
  c1t = CATransform3DTranslate(c1t, -100, 0, 0);
  CALayer *cube1 = [self cubeWithTransform:c1t];
  [self.containerView.layer addSublayer:cube1];
  //set up the transform for cube 2 and add it
  CATransform3D c2t = CATransform3DIdentity;
  c2t = CATransform3DTranslate(c2t, 100, 0, 0);
  c2t = CATransform3DRotate(c2t, -M_PI_4, 1, 0, 0);
  c2t = CATransform3DRotate(c2t, -M_PI_4, 0, 1, 0);
  CALayer *cube2 = [self cubeWithTransform:c2t];
  [self.containerView.layer addSublayer:cube2];
}
@end

圖6.5 同一視角下的倆不一樣變換的立方體

 

CAGradientLayer

CAGradientLayer是用來生成兩種或更多顏色平滑漸變的。用Core Graphics複製一個CAGradientLayer並將內容繪製到一個普通圖層的寄宿圖也是有可能的,可是CAGradientLayer的真正好處在於繪製使用了硬件加速。

基礎漸變

咱們將從一個簡單的紅變藍的對角線漸變開始(見清單6.6).這些漸變色彩放在一個數組中,並賦給colors屬性。這個數組成員接受CGColorRef類型的值(並非從NSObject派生而來),因此咱們要用經過bridge轉換以確保編譯正常。

CAGradientLayer也有startPoint和endPoint屬性,他們決定了漸變的方向。這兩個參數是以單位座標系進行的定義,因此左上角座標是{0, 0},右下角座標是{1, 1}。代碼運行結果如圖6.6

清單6.6 簡單的兩種顏色的對角線漸變

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
  [super viewDidLoad];
  //create gradient layer and add it to our container view
  CAGradientLayer *gradientLayer = [CAGradientLayer layer];
  gradientLayer.frame = self.containerView.bounds;
  [self.containerView.layer addSublayer:gradientLayer];
  //set gradient colors
  gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id)[UIColor blueColor].CGColor];
  //set gradient start and end points
  gradientLayer.startPoint = CGPointMake(0, 0);
  gradientLayer.endPoint = CGPointMake(1, 1);
}
@end

圖6.6 用CAGradientLayer實現簡單的兩種顏色的對角線漸變

多重漸變

若是你願意,colors屬性能夠包含不少顏色,因此建立一個彩虹同樣的多重漸變也是很簡單的。默認狀況下,這些顏色在空間上均勻地被渲染,可是咱們能夠用locations屬性來調整空間。locations屬性是一個浮點數值的數組(以NSNumber包裝)。這些浮點數定義了colors屬性中每一個不一樣顏色的位置,一樣的,也是以單位座標系進行標定。0.0表明着漸變的開始,1.0表明着結束。

locations數組並非強制要求的,可是若是你給它賦值了就必定要確保locations的數組大小和colors數組大小必定要相同,不然你將會獲得一個空白的漸變。

清單6.7展現了一個基於清單6.6的對角線漸變的代碼改造。如今變成了從紅到黃最後到綠色的漸變。locations數組指定了0.0,0.25和0.5三個數值,這樣這三個漸變就有點像擠在了左上角。(如圖6.7).

清單6.7 在漸變上使用locations

- (void)viewDidLoad {
    [super viewDidLoad];
    //create gradient layer and add it to our container view
    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:gradientLayer];
    //set gradient colors
    gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id [UIColor yellowColor].CGColor, (__bridge id)[UIColor greenColor].CGColor];
    //set locations
    gradientLayer.locations = @[@0.0, @0.25, @0.5];
    //set gradient start and end points
    gradientLayer.startPoint = CGPointMake(0, 0);
    gradientLayer.endPoint = CGPointMake(1, 1);
}

圖6.7 用locations構造偏移至左上角的三色漸變

CAReplicatorLayer

CAReplicatorLayer的目的是爲了高效生成許多類似的圖層。它會繪製一個或多個圖層的子圖層,並在每一個複製體上應用不一樣的變換。看上去演示可以更加解釋這些,咱們來寫個例子吧。

重複圖層(Repeating Layers)

清單6.8中,咱們在屏幕的中間建立了一個小白色方塊圖層,而後用CAReplicatorLayer生成十個圖層組成一個圓圈。instanceCount屬性指定了圖層須要重複多少次。instanceTransform指定了一個CATransform3D3D變換(這種狀況下,下一圖層的位移和旋轉將會移動到圓圈的下一個點)。

變換是逐步增長的,每一個實例都是相對於前一實例佈局。這就是爲何這些複製體最終不會出如今贊成位置上,圖6.8是代碼運行結果。

清單6.8 用CAReplicatorLayer重複圖層

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //create a replicator layer and add it to our view
    CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
    replicator.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:replicator];
    //configure the replicator
    replicator.instanceCount = 10;
    //apply a transform for each instance
    CATransform3D transform = CATransform3DIdentity;
    transform = CATransform3DTranslate(transform, 0, 200, 0);
    transform = CATransform3DRotate(transform, M_PI / 5.0, 0, 0, 1);
    transform = CATransform3DTranslate(transform, 0, -200, 0);
    replicator.instanceTransform = transform;
    //apply a color shift for each instance
    replicator.instanceBlueOffset = -0.1;
    replicator.instanceGreenOffset = -0.1;
    //create a sublayer and place it inside the replicator
    CALayer *layer = [CALayer layer];
    layer.frame = CGRectMake(100.0f, 100.0f, 100.0f, 100.0f);
    layer.backgroundColor = [UIColor whiteColor].CGColor;
    [replicator addSublayer:layer];
}
@end

圖6.8 用CAReplicatorLayer建立一圈圖層

注意到當圖層在重複的時候,他們的顏色也在變化:這是用instanceBlueOffset和instanceGreenOffset屬性實現的。經過逐步減小藍色和綠色通道,咱們逐漸將圖層顏色轉換成了紅色。這個複製效果看起來很酷,可是CAReplicatorLayer真正應用到實際程序上的場景好比:一個遊戲中導彈的軌跡雲,或者粒子爆炸(儘管iOS 5已經引入了CAEmitterLayer,它更適合建立任意的粒子效果)。除此以外,還有一個實際應用是:反射。

反射

使用CAReplicatorLayer並應用一個負比例變換於一個複製圖層,你就能夠建立指定視圖(或整個視圖層次)內容的鏡像圖片,這樣就建立了一個實時的『反射』效果。讓咱們來嘗試實現這個創意:指定一個繼承於UIView的ReflectionView,它會自動產生內容的反射效果。實現這個效果的代碼很簡單(見清單6.9),實際上用ReflectionView實現這個效果會更簡單,咱們只須要把ReflectionView的實例放置於Interface Builder(見圖6.9),它就會實時生成子視圖的反射,而不須要別的代碼(見圖6.10).

清單6.9 用CAReplicatorLayer自動繪製反射

#import "ReflectionView.h"
#import @implementation ReflectionView
+ (Class)layerClass
{
    return [CAReplicatorLayer class];
}
- (void)setUp
{
    //configure replicator
    CAReplicatorLayer *layer = (CAReplicatorLayer *)self.layer;
    layer.instanceCount = 2;
    //move reflection instance below original and flip vertically
    CATransform3D transform = CATransform3DIdentity;
    CGFloat verticalOffset = self.bounds.size.height + 2;
    transform = CATransform3DTranslate(transform, 0, verticalOffset, 0);
    transform = CATransform3DScale(transform, 1, -1, 0);
    layer.instanceTransform = transform;
    //reduce alpha of reflection layer
    layer.instanceAlphaOffset = -0.6;
}
?
- (id)initWithFrame:(CGRect)frame
{
    //this is called when view is created in code
    if ((self = [super initWithFrame:frame])) {
        [self setUp];
    }
    return self;
}
- (void)awakeFromNib
{
    //this is called when view is created from a nib
    [self setUp];
}
@end

圖6.9 在Interface Builder中使用ReflectionView

圖6.10 ReflectionView自動實時產生反射效果。

開源代碼ReflectionView完成了一個自適應的漸變淡出效果(用CAGradientLayer和圖層蒙板實現),代碼見 https://github.com/nicklockwood/ReflectionView

 

CAScrollLayer

對於一個未轉換的圖層,它的bounds和它的frame是同樣的,frame屬性是由bounds屬性自動計算而出的,因此更改任意一個值都會更新其餘值。

可是若是你只想顯示一個大圖層裏面的一小部分呢。好比說,你可能有一個很大的圖片,你但願用戶可以隨意滑動,或者是一個數據或文本的長列表。在一個典型的iOS應用中,你可能會用到UITableView或是UIScrollView,可是對於獨立的圖層來講,什麼會等價於剛剛提到的UITableView和UIScrollView呢?

在第二章中,咱們探索了圖層的contentsRect屬性的用法,它的確是可以解決在圖層中小地方顯示大圖片的解決方法。可是若是你的圖層包含子圖層那它就不是一個很是好的解決方案,由於,這樣作的話每次你想『滑動』可視區域的時候,你就須要手工從新計算並更新全部的子圖層位置。

這個時候就須要CAScrollLayer了。CAScrollLayer有一個-scrollToPoint:方法,它自動適應bounds的原點以便圖層內容出如今滑動的地方。注意,這就是它作的全部事情。前面提到過,Core Animation並不處理用戶輸入,因此CAScrollLayer並不負責將觸摸事件轉換爲滑動事件,既不渲染滾動條,也不實現任何iOS指定行爲例如滑動反彈(當視圖滑動超多了它的邊界的將會反彈回正確的地方)。

讓咱們來用CAScrollLayer來常見一個基本的UIScrollView替代品。咱們將會用CAScrollLayer做爲視圖的宿主圖層,並建立一個自定義的UIView,而後用UIPanGestureRecognizer實現觸摸事件響應。這段代碼見清單6.10. 圖6.11是運行效果:ScrollView顯示了一個大於它的frame的UIImageView。

清單6.10 用CAScrollLayer實現滑動視圖

#import "ScrollView.h"
#import  @implementation ScrollView
+ (Class)layerClass
{
    return [CAScrollLayer class];
}
- (void)setUp
{
    //enable clipping
    self.layer.masksToBounds = YES;
    //attach pan gesture recognizer
    UIPanGestureRecognizer *recognizer = nil;
    recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
    [self addGestureRecognizer:recognizer];
}
- (id)initWithFrame:(CGRect)frame
{
    //this is called when view is created in code
    if ((self = [super initWithFrame:frame])) {
        [self setUp];
    }
    return self;
}
- (void)awakeFromNib {
    //this is called when view is created from a nib
    [self setUp];
}
- (void)pan:(UIPanGestureRecognizer *)recognizer
{
    //get the offset by subtracting the pan gesture
    //translation from the current bounds origin
    CGPoint offset = self.bounds.origin;
    offset.x -= [recognizer translationInView:self].x;
    offset.y -= [recognizer translationInView:self].y;
    //scroll the layer
    [(CAScrollLayer *)self.layer scrollToPoint:offset];
    //reset the pan gesture translation
    [recognizer setTranslation:CGPointZero inView:self];
}
@end

圖6.11 用UIScrollView建立一個湊合的滑動視圖

不一樣於UIScrollView,咱們定製的滑動視圖類並無實現任何形式的邊界檢查(bounds checking)。圖層內容極有可能滑出視圖的邊界並沒有限滑下去。CAScrollLayer並無等同於UIScrollView中contentSize的屬性,因此當CAScrollLayer滑動的時候徹底沒有一個全局的可滑動區域的概念,也沒法自適應它的邊界原點至你指定的值。它之因此不能自適應邊界大小是由於它不須要,內容徹底能夠超過邊界。

那你必定會奇怪用CAScrollLayer的意義到底何在,由於你能夠簡單地用一個普通的CALayer而後手動適應邊界原點啊。真相其實並不複雜,UIScrollView並無用CAScrollLayer,事實上,就是簡單的經過直接操做圖層邊界來實現滑動。

CAScrollLayer有一個潛在的有用特性。若是你查看CAScrollLayer的頭文件,你就會注意到有一個擴展分類實現了一些方法和屬性:

- (void)scrollPoint:(CGPoint)p;
- (void)scrollRectToVisible:(CGRect)r;
@property(readonly) CGRect visibleRect;

看到這些方法和屬性名,你也許會覺得這些方法給每一個CALayer實例增長了滑動功能。可是事實上他們只是放置在CAScrollLayer中的圖層的實用方法。scrollPoint:方法從圖層樹中查找並找到第一個可用的CAScrollLayer,而後滑動它使得指定點成爲可視的。scrollRectToVisible:方法實現了一樣的事情只不過是做用在一個矩形上的。visibleRect屬性決定圖層(若是存在的話)的哪部分是當前的可視區域。若是你本身實現這些方法就會相對容易明白一點,可是CAScrollLayer幫你省了這些麻煩,因此當涉及到實現圖層滑動的時候就能夠用上了。

 

CATiledLayer

有些時候你可能須要繪製一個很大的圖片,常見的例子就是一個高像素的照片或者是地球表面的詳細地圖。iOS應用通暢運行在內存受限的設備上,因此讀取整個圖片到內存中是不明智的。載入大圖可能會至關地慢,那些對你看上去比較方便的作法(在主線程調用UIImage的-imageNamed:方法或者-imageWithContentsOfFile:方法)將會阻塞你的用戶界面,至少會引發動畫卡頓現象。

能高效繪製在iOS上的圖片也有一個大小限制。全部顯示在屏幕上的圖片最終都會被轉化爲OpenGL紋理,同時OpenGL有一個最大的紋理尺寸(一般是2048*2048,或4096*4096,這個取決於設備型號)。若是你想在單個紋理中顯示一個比這大的圖,即使圖片已經存在於內存中了,你仍然會遇到很大的性能問題,由於Core Animation強制用CPU處理圖片而不是更快的GPU。

CATiledLayer爲載入大圖形成的性能問題提供了一個解決方案:將大圖分解成小片而後將他們單獨按需載入。讓咱們用實驗來證實一下。

小片裁剪

這個示例中,咱們將會從一個2048*2048分辨率的雪人圖片入手。爲了可以從CATiledLayer中獲益,咱們須要把這個圖片裁切成許多小一些的圖片。你能夠經過代碼來完成這件事情,可是若是你在運行時讀入整個圖片並裁切,那CATiledLayer這些全部的性能優勢就損失殆盡了。理想狀況下來講,最好可以逐個步驟來實現。

清單6.11 演示了一個簡單的Mac OS命令行程序,它用CATiledLayer將一個圖片裁剪成小圖並存儲到不一樣的文件中。

清單6.11 裁剪圖片成小圖的終端程序

#import int main(int argc, const char * argv[])
{
    @autoreleasepool{
        ?//handle incorrect arguments
        if (argc < 2) {
            NSLog(@"TileCutter arguments: inputfile");
            return 0;
        }
        //input file
        NSString *inputFile = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];
        //tile size
        CGFloat tileSize = 256; //output path
        NSString *outputPath = [inputFile stringByDeletingPathExtension];
        //load image
        NSImage *image = [[NSImage alloc] initWithContentsOfFile:inputFile];
        NSSize size = [image size];
        NSArray *representations = [image representations];
        if ([representations count]){
            NSBitmapImageRep *representation = representations[0];
            size.width = [representation pixelsWide];
            size.height = [representation pixelsHigh];
        }
        NSRect rect = NSMakeRect(0.0, 0.0, size.width, size.height);
        CGImageRef imageRef = [image CGImageForProposedRect:&rect context:NULL hints:nil];
        //calculate rows and columns
        NSInteger rows = ceil(size.height / tileSize);
        NSInteger cols = ceil(size.width / tileSize);
        //generate tiles
        for (int y = 0; y < rows; ++y) {
            for (int x = 0; x < cols; ++x) {
            //extract tile image
            CGRect tileRect = CGRectMake(x*tileSize, y*tileSize, tileSize, tileSize);
            CGImageRef tileImage = CGImageCreateWithImageInRect(imageRef, tileRect);
            //convert to jpeg data
            NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:tileImage];
            NSData *data = [imageRep representationUsingType: NSJPEGFileType properties:nil];
            CGImageRelease(tileImage);
            //save file
            NSString *path = [outputPath stringByAppendingFormat: @"_i_i.jpg", x, y];
            [data writeToFile:path atomically:NO];
            }
        }
    }
    return 0;
}

這個程序將2048*2048分辨率的雪人圖案裁剪成了64個不一樣的256*256的小圖。(256*256是CATiledLayer的默認小圖大小,默認大小能夠經過tileSize屬性更改)。程序接受一個圖片路徑做爲命令行的第一個參數。咱們能夠在編譯的scheme將路徑參數硬編碼而後就能夠在Xcode中運行了,可是之後做用在另外一個圖片上就不方便了。因此,咱們編譯了這個程序並把它保存到敏感的地方,而後從終端調用,以下面所示:

> path/to/TileCutterApp path/to/Snowman.jpg

這個程序至關基礎,可是可以輕易地擴展支持額外的參數好比小圖大小,或者導出格式等等。運行結果是64個新圖的序列,以下面命名:

Snowman_00_00.jpg
Snowman_00_01.jpg
Snowman_00_02.jpg
...
Snowman_07_07.jpg

既然咱們有了裁切後的小圖,咱們就要讓iOS程序用到他們。CATiledLayer很好地和UIScrollView集成在一塊兒。除了設置圖層和滑動視圖邊界以適配整個圖片大小,咱們真正要作的就是實現-drawLayer:inContext:方法,當須要載入新的小圖時,CATiledLayer就會調用到這個方法。

清單6.12演示了代碼。圖6.12是代碼運行結果。

清單6.12 一個簡單的滾動CATiledLayer實現

#import "ViewController.h"
#import @interface ViewController ()
@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //add the tiled layer
    CATiledLayer *tileLayer = [CATiledLayer layer];?
    tileLayer.frame = CGRectMake(0, 0, 2048, 2048);
    tileLayer.delegate = self; [self.scrollView.layer addSublayer:tileLayer];
    //configure the scroll view
    self.scrollView.contentSize = tileLayer.frame.size;
    //draw layer
    [tileLayer setNeedsDisplay];
}
- (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx
{
    //determine tile coordinate
    CGRect bounds = CGContextGetClipBoundingBox(ctx);
    NSInteger x = floor(bounds.origin.x / layer.tileSize.width);
    NSInteger y = floor(bounds.origin.y / layer.tileSize.height);
    //load tile image
    NSString *imageName = [NSString stringWithFormat: @"Snowman_i_i, x, y];
    NSString *imagePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"jpg"];
    UIImage *tileImage = [UIImage imageWithContentsOfFile:imagePath];
    //draw tile
    UIGraphicsPushContext(ctx);
    [tileImage drawInRect:bounds];
    UIGraphicsPopContext();
}
@end

圖6.12 用UIScrollView滾動CATiledLayer

當你滑動這個圖片,你會發現當CATiledLayer載入小圖的時候,他們會淡入到界面中。這是CATiledLayer的默認行爲。(你可能已經在iOS 6以前的蘋果地圖程序中見過這個效果)你能夠用fadeDuration屬性改變淡入時長或直接禁用掉。CATiledLayer(不一樣於大部分的UIKit和Core Animation方法)支持多線程繪製,-drawLayer:inContext:方法能夠在多個線程中同時地併發調用,因此請當心謹慎地確保你在這個方法中實現的繪製代碼是線程安全的。

Retina小圖

你也許已經注意到了這些小圖並非以Retina的分辨率顯示的。爲了以屏幕的原生分辨率來渲染CATiledLayer,咱們須要設置圖層的contentsScale來匹配UIScreen的scale屬性:

tileLayer.contentsScale = [UIScreen mainScreen].scale;

有趣的是,tileSize是以像素爲單位,而不是點,因此增大了contentsScale就自動有了默認的小圖尺寸(如今它是128*128的點而不是256*256).因此,咱們不須要手工更新小圖的尺寸或是在Retina分辨率下指定一個不一樣的小圖。咱們須要作的是適應小圖渲染代碼以對應安排scale的變化,然而:

//determine tile coordinate
CGRect bounds = CGContextGetClipBoundingBox(ctx);
CGFloat scale = [UIScreen mainScreen].scale;
NSInteger x = floor(bounds.origin.x / layer.tileSize.width * scale);
NSInteger y = floor(bounds.origin.y / layer.tileSize.height * scale);

經過這個方法糾正scale也意味着咱們的雪人圖將以一半的大小渲染在Retina設備上(總尺寸是1024*1024,而不是2048*2048)。這個一般都不會影響到用CATiledLayer正常顯示的圖片類型(好比照片和地圖,他們在設計上就是要支持放大縮小,可以在不一樣的縮放條件下顯示),可是也須要在內心明白。

 

CAEmitterLayer

在iOS 5中,蘋果引入了一個新的CALayer子類叫作CAEmitterLayer。CAEmitterLayer是一個高性能的粒子引擎,被用來建立實時例子動畫如:煙霧,火,雨等等這些效果。

CAEmitterLayer看上去像是許多CAEmitterCell的容器,這些CAEmitierCell定義了一個例子效果。你將會爲不一樣的例子效果定義一個或多個CAEmitterCell做爲模版,同時CAEmitterLayer負責基於這些模版實例化一個粒子流。一個CAEmitterCell相似於一個CALayer:它有一個contents屬性能夠定義爲一個CGImage,另外還有一些可設置屬性控制着表現和行爲。咱們不會對這些屬性逐一進行詳細的描述,大家能夠在CAEmitterCell類的頭文件中找到。

咱們來舉個例子。咱們將利用在一圓中發射不一樣速度和透明度的粒子建立一個火爆炸的效果。清單6.13包含了生成爆炸的代碼。圖6.13是運行結果

清單6.13 用CAEmitterLayer建立爆炸效果

#import "ViewController.h"
#import @interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    ?
    //create particle emitter layer
    CAEmitterLayer *emitter = [CAEmitterLayer layer];
    emitter.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:emitter];
    //configure emitter
    emitter.renderMode = kCAEmitterLayerAdditive;
    emitter.emitterPosition = CGPointMake(emitter.frame.size.width / 2.0, emitter.frame.size.height / 2.0);
    //create a particle template
    CAEmitterCell *cell = [[CAEmitterCell alloc] init];
    cell.contents = (__bridge id)[UIImage imageNamed:@"Spark.png"].CGImage;
    cell.birthRate = 150;
    cell.lifetime = 5.0;
    cell.color = [UIColor colorWithRed:1 green:0.5 blue:0.1 alpha:1.0].CGColor;
    cell.alphaSpeed = -0.4;
    cell.velocity = 50;
    cell.velocityRange = 50;
    cell.emissionRange = M_PI * 2.0;
    //add particle template to emitter
    emitter.emitterCells = @[cell];
}
@end

圖6.13 火焰爆炸效果

CAEMitterCell的屬性基本上能夠分爲三種:

  • 這種粒子的某一屬性的初始值。好比,color屬性指定了一個能夠混合圖片內容顏色的混合色。在示例中,咱們將它設置爲桔色。

  • 例子某一屬性的變化範圍。好比emissionRange屬性的值是2π,這意味着例子能夠從360度任意位置反射出來。若是指定一個小一些的值,就能夠創造出一個圓錐形

  • 指定值在時間線上的變化。好比,在示例中,咱們將alphaSpeed設置爲-0.4,就是說例子的透明度每過一秒就是減小0.4,這樣就有發射出去以後逐漸小時的效果。

CAEmitterLayer的屬性它本身控制着整個例子系統的位置和形狀。一些屬性好比birthRate,lifetime和celocity,這些屬性在CAEmitterCell中也有。這些屬性會以相乘的方式做用在一塊兒,這樣你就能夠用一個值來加速或者擴大整個例子系統。其餘值得提到的屬性有如下這些:

  • preservesDepth,是否將3D例子系統平面化到一個圖層(默認值)或者能夠在3D空間中混合其餘的圖層

  • renderMode,控制着在視覺上粒子圖片是如何混合的。你可能已經注意到了示例中咱們把它設置爲kCAEmitterLayerAdditive,它實現了這樣一個效果:合併例子重疊部分的亮度使得看上去更亮。若是咱們把它設置爲默認的kCAEmitterLayerUnordered,效果就沒那麼好看了(見圖6.14).

圖6.14 禁止混色以後的火焰粒子

CAEAGLLayer

當iOS要處理高性能圖形繪製,必要時就是OpenGL。應該說它應該是最後的殺手鐗,至少對於非遊戲的應用來講是的。由於相比Core Animation和UIkit框架,它難以想象地複雜。

OpenGL提供了Core Animation的基礎,它是底層的C接口,直接和iPhone,iPad的硬件通訊,極少地抽象出來的方法。OpenGL沒有對象或是圖層的繼承概念。它只是簡單地處理三角形。OpenGL中全部東西都是3D空間中有顏色和紋理的三角形。用起來很是複雜和強大,可是用OpenGL繪製iOS用戶界面就須要不少不少的工做了。

爲了可以以高性能使用Core Animation,你須要判斷你須要繪製哪一種內容(矢量圖形,例子,文本,等等),但後選擇合適的圖層去呈現這些內容,Core Animation中只有一些類型的內容是被高度優化的;因此若是你想繪製的東西並不能找到標準的圖層類,想要獲得高性能就比較費事情了。

由於OpenGL根本不會對你的內容進行假設,它可以繪製得至關快。利用OpenGL,你能夠繪製任何你知道必要的集合信息和形狀邏輯的內容。因此不少遊戲都喜歡用OpenGL(這些狀況下,Core Animation的限制就明顯了:它優化過的內容類型並不必定能知足需求),可是這樣依賴,方便的高度抽象接口就沒了。

在iOS 5中,蘋果引入了一個新的框架叫作GLKit,它去掉了一些設置OpenGL的複雜性,提供了一個叫作CLKView的UIView的子類,幫你處理大部分的設置和繪製工做。前提是各類各樣的OpenGL繪圖緩衝的底層可配置項仍然須要你用CAEAGLLayer完成,它是CALayer的一個子類,用來顯示任意的OpenGL圖形。

大部分狀況下你都不須要手動設置CAEAGLLayer(假設用GLKView),過去的日子就不要再提了。特別的,咱們將設置一個OpenGL ES 2.0的上下文,它是現代的iOS設備的標準作法。

儘管不須要GLKit也能夠作到這一切,可是GLKit囊括了不少額外的工做,好比設置頂點和片斷着色器,這些都以類C語言叫作GLSL自包含在程序中,同時在運行時載入到圖形硬件中。編寫GLSL代碼和設置EAGLayer沒有什麼關係,因此咱們將用GLKBaseEffect類將着色邏輯抽象出來。其餘的事情,咱們仍是會有以往的方式。

在開始以前,你須要將GLKit和OpenGLES框架加入到你的項目中,而後就能夠實現清單6.14中的代碼,裏面是設置一個GAEAGLLayer的最少工做,它使用了OpenGL ES 2.0 的繪圖上下文,並渲染了一個有色三角(見圖6.15).

清單6.14 用CAEAGLLayer繪製一個三角形

#import "ViewController.h"
#import #import @interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *glView;
@property (nonatomic, strong) EAGLContext *glContext;
@property (nonatomic, strong) CAEAGLLayer *glLayer;
@property (nonatomic, assign) GLuint framebuffer;
@property (nonatomic, assign) GLuint colorRenderbuffer;
@property (nonatomic, assign) GLint framebufferWidth;
@property (nonatomic, assign) GLint framebufferHeight;
@property (nonatomic, strong) GLKBaseEffect *effect;
?
@end
@implementation ViewController
- (void)setUpBuffers
{
    //set up frame buffer
    glGenFramebuffers(1, &_framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
    //set up color render buffer
    glGenRenderbuffers(1, &_colorRenderbuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.glLayer];
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_framebufferWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_framebufferHeight);
    //check success
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        NSLog(@"Failed to make complete framebuffer object: %i", glCheckFramebufferStatus(GL_FRAMEBUFFER));
    }
}
- (void)tearDownBuffers
{
    if (_framebuffer) {
        //delete framebuffer
        glDeleteFramebuffers(1, &_framebuffer);
        _framebuffer = 0;
    }
    if (_colorRenderbuffer) {
        //delete color render buffer
        glDeleteRenderbuffers(1, &_colorRenderbuffer);
        _colorRenderbuffer = 0;
    }
}
- (void)drawFrame {
    //bind framebuffer & set viewport
    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
    glViewport(0, 0, _framebufferWidth, _framebufferHeight);
    //bind shader program
    [self.effect prepareToDraw];
    //clear the screen
    glClear(GL_COLOR_BUFFER_BIT); glClearColor(0.0, 0.0, 0.0, 1.0);
    //set up vertices
    GLfloat vertices[] = {
        -0.5f, -0.5f, -1.0f, 0.0f, 0.5f, -1.0f, 0.5f, -0.5f, -1.0f,
    };
    //set up colors
    GLfloat colors[] = {
        0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
    };
    //draw triangle
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glEnableVertexAttribArray(GLKVertexAttribColor);
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, vertices);
    glVertexAttribPointer(GLKVertexAttribColor,4, GL_FLOAT, GL_FALSE, 0, colors);
    glDrawArrays(GL_TRIANGLES, 0, 3);
    //present render buffer
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext presentRenderbuffer:GL_RENDERBUFFER];
}
- (void)viewDidLoad
{
    [super viewDidLoad];
    //set up context
    self.glContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:self.glContext];
    //set up layer
    self.glLayer = [CAEAGLLayer layer];
    self.glLayer.frame = self.glView.bounds;
    [self.glView.layer addSublayer:self.glLayer];
    self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@NO, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};
    //set up base effect
    self.effect = [[GLKBaseEffect alloc] init];
    //set up buffers
    [self setUpBuffers];
    //draw frame
    [self drawFrame];
}
- (void)viewDidUnload
{
    [self tearDownBuffers];
    [super viewDidUnload];
}
- (void)dealloc
{
    [self tearDownBuffers];
    [EAGLContext setCurrentContext:nil];
}
@end

圖6.15 用OpenGL渲染的CAEAGLLayer圖層

在一個真正的OpenGL應用中,咱們可能會用NSTimer或CADisplayLink週期性地每秒鐘調用-drawRrame方法60次,同時會將幾何圖形生成和繪製分開以便不會每次都從新生成三角形的頂點(這樣也可讓咱們繪製其餘的一些東西而不是一個三角形而已),不過上面這個例子已經足夠演示了繪圖原則了。

AVPlayerLayer

最後一個圖層類型是AVPlayerLayer。儘管它不是Core Animation框架的一部分(AV前綴看上去像),AVPlayerLayer是有別的框架(AVFoundation)提供的,它和Core Animation緊密地結合在一塊兒,提供了一個CALayer子類來顯示自定義的內容類型。

AVPlayerLayer是用來在iOS上播放視頻的。他是高級接口例如MPMoivePlayer的底層實現,提供了顯示視頻的底層控制。AVPlayerLayer的使用至關簡單:你能夠用+playerLayerWithPlayer:方法建立一個已經綁定了視頻播放器的圖層,或者你能夠先建立一個圖層,而後用player屬性綁定一個AVPlayer實例。

在咱們開始以前,咱們須要添加AVFoundation到咱們的項目中。而後,清單6.15建立了一個簡單的電影播放器,圖6.16是代碼運行結果。

清單6.15 用AVPlayerLayer播放視頻

#import "ViewController.h"
#import #import @interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView; @end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //get video URL
    NSURL *URL = [[NSBundle mainBundle] URLForResource:@"Ship" withExtension:@"mp4"];
    //create player and player layer
    AVPlayer *player = [AVPlayer playerWithURL:URL];
    AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
    //set player layer frame and attach it to our view
    playerLayer.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:playerLayer];
    //play the video
    [player play];
}
@end

圖6.16 用AVPlayerLayer圖層播放視頻的截圖

咱們用代碼建立了一個AVPlayerLayer,可是咱們仍然把它添加到了一個容器視圖中,而不是直接在controller中的主視圖上添加。這樣實際上是爲了可使用自動佈局限制使得圖層在最中間;不然,一旦設備被旋轉了咱們就要手動從新放置位置,由於Core Animation並不支持自動大小和自動佈局(見第三章『圖層幾何學』)。

固然,由於AVPlayerLayer是CALayer的子類,它繼承了父類的全部特性。咱們並不會受限於要在一個矩形中播放視頻;清單6.16演示了在3D,圓角,有色邊框,蒙板,陰影等效果(見圖6.17).

清單6.16 給視頻增長變換,邊框和圓角

- (void)viewDidLoad
{
    ...
    //set player layer frame and attach it to our view
    playerLayer.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:playerLayer];
    //transform layer
    CATransform3D transform = CATransform3DIdentity;
    transform.m34 = -1.0 / 500.0;
    transform = CATransform3DRotate(transform, M_PI_4, 1, 1, 0);
    playerLayer.transform = transform;
    ?
    //add rounded corners and border
    playerLayer.masksToBounds = YES;
    playerLayer.cornerRadius = 20.0;
    playerLayer.borderColor = [UIColor redColor].CGColor;
    playerLayer.borderWidth = 5.0;
    //play the video
    [player play];
}

圖6.17 3D視角下的邊框和圓角AVPlayerLayer

相關文章
相關標籤/搜索