CoreAnimation之專用圖層

復雜的組織都是專門化的
Catharine R. Stimpson

到目前為止,我們已經探討過CALayer類了,同時我們也了解到了一些非常有用的繪圖和動畫功能。但是Core Animation圖層不僅僅能作用于圖片和顏色而已。本章就會學習其他的一些圖層類,進一步擴展使用Core Animation繪圖的能力

  • CAShapeLayer

在第四章『視覺效果』我們學習到了不使用圖片的情況下用CGPath去構造任意形狀的陰影。如果我們能用同樣的方式創建相同形狀的圖層就好了。
CAShapeLayer是一個通過矢量圖形而不是bitmap來繪制的圖層子類。你指定諸如顏色和線寬等屬性,用CGPath來定義想要繪制的圖形,最后CAShapeLayer就自動渲染出來了。當然,你也可以用Core Graphics直接向原始的CALyer的內容中繪制一個路徑,相比直下,使用CAShapeLayer有以下一些優點:

  • 渲染快速。CAShapeLayer使用了硬件加速,繪制同一圖形會比用Core Graphics快很多。
  • 高效使用內存。一個CAShapeLayer不需要像普通CALayer一樣創建一個寄宿圖形,所以無論有多大,都不會占用太多的內存。
  • 不會被圖層邊界剪裁掉。一個CAShapeLayer可以在邊界之外繪制。你的圖層路徑不會像在使用Core Graphics的普通CALayer一樣被剪裁掉(如我們在第二章所見)。
  • 不會出現像素化。當你給CAShapeLayer做3D變換時,它不像一個有寄宿圖的普通圖層一樣變得像素化。

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

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

清單6.1 用CAShapeLayer繪制一個火柴人
#import "DrawingView.h"
#import <QuartzCore/QuartzCore.h>
    
    @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.png](http://upload-images.jianshu.io/upload_images/1694376-7d3000c3c6b9099d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
**圓角**
 第二章里面提到了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](http://lib.csdn.net/base/1) 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.png](http://upload-images.jianshu.io/upload_images/1694376-3d0e66a31fbaa91b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
如果你仔細看這個文本,你會發現一個奇怪的地方:這些文本有一些像素化了。這是因為并沒有以Retina的方式渲染,第二章提到了這個contentScale屬性,用來決定圖層內容應該以怎樣的分辨率來渲染。contentsScale并不關心屏幕的拉伸因素而總是默認為1.0。如果我們想以Retina的質量來顯示文字,我們就得手動地設置CATextLayer的contentsScale屬性,如下:
`textLayer.contentsScale = [UIScreen mainScreen].scale;`

![6.3.png](http://upload-images.jianshu.io/upload_images/1694376-d646738dc883b658.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

         設置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 <QuartzCore/QuartzCore.h>

import <CoreText/CoreText.h>

@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.png

行距和字距

有必要提一下的是,由于繪制的實現機制不同(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 <QuartzCore/QuartzCore.h>
    
    @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
 
    - (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
 
    - (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.png
  • *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.png](http://upload-images.jianshu.io/upload_images/1694376-9d6f27a9846fb2b1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

####多重漸變
 如果你愿意,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.png
  • 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.png

注意到當圖層在重復的時候,他們的顏色也在變化:這是用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 <QuartzCore/QuartzCore.h>
    
    @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.png
6.10.png
  • *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 <QuartzCore/QuartzCore.h> @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

不同于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有一個最大的紋理尺寸(通常是20482048,或40964096,這個取決于設備型號)。如果你想在單個紋理中顯示一個比這大的圖,即便圖片已經存在于內存中了,你仍然會遇到很大的性能問題,因為Core Animation強制用CPU處理圖片而不是更快的GPU(見第12章『速度的曲調』,和第13章『高效繪圖』,它更加詳細地解釋了軟件繪制和硬件繪制)。

CATiledLayer為載入大圖造成的性能問題提供了一個解決方案:將大圖分解成小片然后將他們單獨按需載入。讓我們用實驗來證明一下。

小片裁剪

這個示例中,我們將會從一個2048*2048分辨率的雪人圖片入手。為了能夠從CATiledLayer中獲益,我們需要把這個圖片裁切成許多小一些的圖片。你可以通過代碼來完成這件事情,但是如果你在運行時讀入整個圖片并裁切,那CATiledLayer這些所有的性能優點就損失殆盡了。理想情況下來說,最好能夠逐個步驟來實現。

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

清單6.11 裁剪圖片成小圖的終端程序
#import <AppKit/AppKit.h>
    
    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: @"_%02i_%02i.jpg", x, y];
                    [data writeToFile:path atomically:NO];
                }
            }
        }
        return 0;
    }

這個程序將20482048分辨率的雪人圖案裁剪成了64個不同的256256的小圖。(256*256是CATiledLayer的默認小圖大小,默認大小可以通過tileSize屬性更改)。程序接受一個圖片路徑作為命令行的第一個參數。我們可以在編譯的scheme將路徑參數硬編碼然后就可以在Xcode中運行了,但是以后作用在另一個圖片上就不方便了。所以,我們編譯了這個程序并把它保存到敏感的地方,然后從終端調用,如下面所示:
> path/to/TileCutterApp path/to/Snowman.jpg

The app is very basic, but could easily be extended to support additional arguments such as tile size, or to export images in formats other than JPEG. The result of running it is a sequence of 64 new images, named as follows:

這個程序相當基礎,但是能夠輕易地擴展支持額外的參數比如小圖大小,或者導出格式等等。運行結果是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 <QuartzCore/QuartzCore.h>
    
    @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_%02i_%02i", 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.png

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

Retina小圖

你也許已經注意到了這些小圖并不是以Retina的分辨率顯示的。為了以屏幕的原生分辨率來渲染CATiledLayer
,我們需要設置圖層的contentsScale來匹配UIScreen的scale
屬性:
tileLayer.contentsScale = [UIScreen mainScreen].scale;

有趣的是,tileSize是以像素為單位,而不是點,所以增大了contentsScale就自動有了默認的小圖尺寸(現在它是128128的點而不是256256).所以,我們不需要手工更新小圖的尺寸或是在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設備上(總尺寸是10241024,而不是20482048)。這個通常都不會影響到用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 <QuartzCore/QuartzCore.h>
    
    @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

CAEMitterCell的屬性基本上可以分為三種:

  • 這種粒子的某一屬性的初始值。比如,color屬性指定了一個可以混合圖片內容顏色的混合色。在示例中,我們將它設置為桔色。

  • 例子某一屬性的變化范圍。比如emissionRange屬性的值是2π,這意味著例子可以從360度任意位置反射出來。如果指定一個小一些的值,就可以創造出一個圓錐形

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

CAEmitterLayer的屬性它自己控制著整個例子系統的位置和形狀。一些屬性比如birthRate,lifetime和celocity,這些屬性在CAEmitterCell中也有。這些屬性會以相乘的方式作用在一起,這樣你就可以用一個值來加速或者擴大整個例子系統。其他值得提到的屬性有以下這些:

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

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

6.14.png
  • 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 <QuartzCore/QuartzCore.h>
#import <GLKit/GLKit.h>
    
    @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.png

在一個真正的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 <QuartzCore/QuartzCore.h>
#import <AVFoundation/AVFoundation.h>
    
    @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.png

我們用代碼創建了一個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];
    }
總結

這一章我們簡要概述了一些專用圖層以及用他們實現的一些效果,我們只是了解到這些圖層的皮毛,像CATiledLayer和CAEMitterLayer這些類可以單獨寫一章的。但是,重點是記住CALayer是用處很大的,而且它并沒有為所有可能的場景進行優化。為了獲得Core Animation最好的性能,你需要為你的工作選對正確的工具,希望你能夠挖掘這些不同的CALayer子類的功能。 這一章我們通過CAEmitterLayer和AVPlayerLayer類簡單地接觸到了一些動畫,在第二章,我們將繼續深入研究動畫,就從隱式動畫開始。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容