玩轉iOS開發:6.《Core Animation》CALayer的Specialized Layers

CainLuo發表於2019-03-04

文章分享至我的個人部落格: https://cainluo.github.io/14790557329421.html


作者感言

在上一篇文章《Core Animation》CALayer的Transforms中, 我們瞭解了二維空間和三維空間的一些佈局, 還有就是最簡單的旋轉, 平移之類的, 再來一些就是混合使用的, 這次我們來換個話題.

**最後:**
**如果你有更好的建議或者對這篇文章有不滿的地方, 請聯絡我, 我會參考你們的意見再進行修改, 聯絡我時, 請備註**`Core Animation`**如果覺得好的話, 希望大家也可以打賞一下~嘻嘻~祝大家學習愉快~謝謝~**


簡介

Specialized Layers講得是一些專用的一些圖層類, 而不是之前所說的一些用於圖片, 顏色之類的, 下面讓我們來看看吧~


CAShapeLayer

在之前的文章裡, 我們使用過陰影效果, 並且是不使用CGPath情況下去構建形狀不同的陰影, 在CALayer中, 有一個子類叫做CAShapeLayer, 它也是可以做到對應的效果.

CAShapeLayer是一個通過向量圖形來進行繪製的圖層子類, 而並不是使用Bitmap, 當我們指定對應的顏色, 線寬等屬性, 就可以使用CGPath來繪製我們想要的形狀, 最後CAShapeLayer就自動渲染出來了, 當然你也可以使用Core Graphics直接對一個CALayer進行繪製, 但CAShapeLayer要比Core Graphics直接操作CALayer要好一些, 比如:

  • CAShapeLayer使用了硬體加速, 繪製同一圖形時會比Core Graphics渲染的快.
  • CAShapeLayer不需要像普通CALayer一樣建立一個寄宿圖形, 所以無論有多大, 都不會佔用太多的記憶體.
  • CAShapeLayerCore Graphics不一樣, 它並不會被圖層邊界給裁剪掉.
  • CAShapeLayer不會出現畫素化, 這可以提現在, 用CAShapeLayer做3D變換的時候, 不會和普通的圖層一樣出現畫素化.

建立一個CGPath

剛剛說了, CAShapeLayer可以通過CGPath來繪製任意圖形, 並且可以設定一些屬性, 比如lineWith, lineCap, lineJoin.

我們繪製這個圖形的時候, 不一定要閉合, 圖層路徑也不是絕對, 可以在一個圖層上繪製多個不同的圖形, 當然, 如果你要想用不同的顏色風格來繪製N個圖形, 那你就要準備好多個Layer了.

CAShapeLayer是屬於CGPathRef型別, 但在實際開發中, 我們是用UIBezierPath來建立圖層路徑的, 這樣子我們就不用考慮人工釋放CGPath了, 下面讓我們來看Demo吧:

- (void)createPath {
    
    UIBezierPath *path = [[UIBezierPath alloc] init];
    
    [path moveToPoint:CGPointMake(175, 100)];
    [path addQuadCurveToPoint:CGPointMake(100, 500)
                 controlPoint:CGPointMake(250, 600)];
    
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.strokeColor = [UIColor blueColor].CGColor;
    shapeLayer.fillColor = [UIColor clearColor].CGColor;
    shapeLayer.lineWidth = 10;
    shapeLayer.lineJoin = kCALineJoinRound;
    shapeLayer.lineCap = kCALineCapRound;
    shapeLayer.path = path.CGPath;
    
    [self.view.layer addSublayer:shapeLayer];
}
複製程式碼
1
2

圓角

之前我們在之前的文章裡, 有提到過把一個檢視剪下成圓角, 用的就是CALayercornerRadius屬性, 而CAShapeLayer類也可以提供同樣的功能, 雖然程式碼多了一些, 但也多了一些靈活, 它可以指定單獨的指定每個角.

我們建立圓角矩形其實就是人工繪製單獨的直線和弧度, 但在UIBezierPath中有提供自動繪製圓角矩形的方法, 直接看程式碼:

- (void)viewRoundedCorners {
    
    CGRect rect = CGRectMake(130, 130, 100, 100);
    CGSize radii = CGSizeMake(10, 10);
    
    UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomRight | UIRectCornerBottomLeft | UIRectCornerTopLeft;
    
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect
                                               byRoundingCorners:corners
                                                     cornerRadii:radii];
    
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    
    shapeLayer.path = path.CGPath;
    
    [self.view.layer addSublayer:shapeLayer];
}
複製程式碼
3
4

CATextLayer

如果我們想在一個圖層裡顯示文字, 我們可以藉助和UILabel一樣的方式, 使用Core Graphics在圖層上寫入內容, 但如果要越過UILabel這些控制元件, 直接在圖層上顯示文字的話, 我們就要為每個顯示文字的圖層建立一個圖層代理的類, 並且判斷哪個圖層需要顯示哪個字串, 如果再加一些字型, 顏色一些亂七八糟的東西, 那就蛋疼的不要不要的.

好在CALayer裡有一個子類, 叫做CATextLayer, 它幾乎都包含了UILabel的所有繪製特性, 而且還額外提供了一些新特性, 並且在渲染的速度上, 要比UILabel快的多, 偷偷說個事, 在iOS 6之前, UILabel其實是通過WebKit來實現繪製的, 所以那時候iOS在渲染文字的時候會有非常大的效能問題, 但CATextLayer使用的是Core Text, 兩者之前完全不同一個概念.

說那麼多廢話, 直接上程式碼吧:

- (void)catextLayer {
    
    UIView *labelView = [[UIView alloc] initWithFrame:CGRectMake(30, 100, 300, 300)];
    
    labelView.backgroundColor = [UIColor redColor];
    
    [self.view addSubview:labelView];
    
    CATextLayer *textLayer = [CATextLayer layer];
    textLayer.frame = labelView.bounds;
    
    [labelView.layer addSublayer:textLayer];
    
    textLayer.foregroundColor = [UIColor blackColor].CGColor;
    textLayer.alignmentMode = kCAAlignmentJustified;
    textLayer.wrapped = YES;
    
    UIFont *font = [UIFont systemFontOfSize:15];
    
    CFStringRef fontName = (__bridge CFStringRef)font.fontName;
    CGFontRef fontRef = CGFontCreateWithFontName(fontName);
    
    textLayer.font = fontRef;
    textLayer.fontSize = font.pointSize;
    
    CGFontRelease(fontRef);
    
    NSString *text = @"這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字";
    
    textLayer.string = text;
}
複製程式碼

這裡還要多說一句, 如果你發現文字顯示的時候出現畫素化的時候, 只要加上以下這段程式碼, 就哦了, 它會以Retina模式來渲染:

    textLayer.contentsScale = [UIScreen mainScreen].scale;
複製程式碼
5
6

contentsScale並且並不關心螢幕的拉伸, 因為預設都是1.0f, 所以我們要高清, 那就設定它吧.

CATextLayer裡的font屬性, 其實並不是一個真正的UIFont型別, 而是一個CFTypeRef型別, 這樣子就可以根據我們的需求來決定字型的屬性到底是用CGFontRef型別還是用Core Text裡的CTFontRef型別了, 同時字型大小也是用fontSize屬性單獨設定的, 因為CTFontRefCGFontRefUIFont完全是兩回事, 在程式碼中我們也知道了如何將UIFont轉成CGFontRef.

當然CATextLayer裡的string屬性是id型別, 並不是我們想象中的NSString型別, 因為這樣子我們就可以用NSString也可以用NSAttributedString來指定要顯示的文字, 比如指定某段文字的字型, 顏色, 字重, 斜體等等.

Rich Text

其實在iOS 6的時候, Apple就已經給了UILabel和其他的UIKit文字檢視新增直接的屬性, 但事實上, 在iOS 3.2的時候, CATextLayer就已經支援屬性化字串了, 如果你想支援更低版本的iOS那麼你可以使用CATextLayer, 不需要和更復雜的Core Text打交道, 也省略了使用其他的方法, 但現在又會有哪家公司支援低版本的iOS呢? 但不能夠說在新版本的iOSD昂中CATextLayer就無用功了, 這個得看我們的需求來確定了.

這次我們把Core Text, CATextLayer, NSAttributedString三者混在一起使用一下~

- (void)attributedString {
    
    UIView *labelView = [[UIView alloc] initWithFrame:CGRectMake(0, 200, self.view.frame.size.width, 400)];
    
    [self.view addSubview:labelView];
    
    CATextLayer *textLayer = [CATextLayer layer];
    
    textLayer.frame = labelView.bounds;
    textLayer.contentsScale = [UIScreen mainScreen].scale;
    
    [labelView.layer addSublayer:textLayer];
    
    textLayer.alignmentMode = kCAAlignmentJustified; textLayer.wrapped = YES;
    
    UIFont *font = [UIFont systemFontOfSize:15];

    NSString *text = @"這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字, 這是一段測試的文字";
    
    NSMutableAttributedString *string = nil;
    string = [[NSMutableAttributedString alloc] initWithString:text];
    
    CFStringRef fontName = (__bridge CFStringRef)font.fontName; CGFloat fontSize = font.pointSize;
    CTFontRef fontRef = CTFontCreateWithName(fontName, fontSize, NULL);
    
    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, 20)];
    
    CFRelease(fontRef);
    
    textLayer.string = string;
}
複製程式碼
7
8

Leading and Kerning

這裡有個點, 由於Core TextWebKit的內部實現機制不同, 用CATextLayer渲染或者是用UILabel渲染文字行距和字距也是不一樣的, 這個是由使用字型和字元來決定的, 所以大家如果要使用普通的UILabelCATextLayer, 就要好好注意一下了.

A UILabel Replacement

這次我們就自己建立一個屬於我們自己的UILabel, 代替系統的UILabel, 雖然這個類也是繼承於UILabel, 但比系統的UILabel的**-drawRect:**方法要快, 來看看程式碼吧~

#import "CLLabel.h"
#import <QuartzCore/QuartzCore.h>

@implementation CLLabel

+ (Class)layerClass {
    
    return [CATextLayer class];
}

- (CATextLayer *)textLayer {
    
    return (CATextLayer *)self.layer;
}

- (void)setUp {
    
    self.text = self.text;
    self.textColor = self.textColor;
    self.font = self.font;
    
    [self textLayer].wrapped = YES;
    [self.layer display];
}

- (id)initWithFrame:(CGRect)frame {
    
    if (self = [super initWithFrame:frame]) {
        
        [self setUp];
    }
    
    return self;
}

- (void)awakeFromNib {
    
    [self setUp];
}

- (void)setText:(NSString *)text {
    super.text = text;
    
    [self textLayer].string = text;
}

- (void)setTextColor:(UIColor *)textColor {
    super.textColor = textColor;
    
    [self textLayer].foregroundColor = textColor.CGColor;
}

- (void)setFont:(UIFont *)font {
    super.font = font;
    
    CFStringRef fontName = (__bridge CFStringRef)font.fontName;
    CGFontRef fontRef = CGFontCreateWithFontName(fontName);
    
    [self textLayer].font = fontRef;
    [self textLayer].fontSize = font.pointSize;
    
    CGFontRelease(fontRef);
}

@end
複製程式碼

使用這個自定義的CLLabel, 我們看看效果

- (void)createCLLabel {
    
    CLLabel *label = [[CLLabel alloc] initWithFrame:CGRectMake(20, 50, 200, 200)];
    
    label.text = @"這是一段很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長的測試文字";
    label.textColor = [UIColor blackColor];
    
    [self.view addSubview:label];
}
複製程式碼
9
10
11

CATransformLayer

在我們日常開發當中, 如果需要用到3D Layer, 可以用到之前我們說到的3D Transforms, 但是那樣子太麻煩了, 要算一堆東西, 如果有一種Layer可以像玩積木一樣, 一個一個的組合成一個3D形狀的話, 那該多好~

其實Apple早就想到了這個問題, 它們提供了CATransformLayer, 就是專門用來給Layer做一個容器, 然後讓拼接成一個看起來像3D的一樣圖形.

我們來看程式碼:

- (void)transformLayer {
    
    self.view.backgroundColor = [UIColor grayColor];
    
    CATransform3D transform3DOne = CATransform3DIdentity;
    
    transform3DOne.m34 = -1.0 / 500.0;
    
    self.view.layer.sublayerTransform = transform3DOne;
    
    CATransform3D transform3DTwo = CATransform3DIdentity;
    
    transform3DTwo = CATransform3DTranslate(transform3DTwo, -100, 0, 0);
    
    CALayer *cubeOne = [self cubeWithTransform:transform3DTwo];
    
    [self.view.layer addSublayer:cubeOne];
    
    CATransform3D transform3DThree = CATransform3DIdentity;
    
    transform3DThree = CATransform3DTranslate(transform3DThree, 100, 0, 0);
    transform3DThree = CATransform3DRotate(transform3DThree, -M_PI_4, 1, 0, 0);
    transform3DThree = CATransform3DRotate(transform3DThree, -M_PI_4, 0, 1, 0);
    
    CALayer *cubeTwo = [self cubeWithTransform:transform3DThree];
    
    [self.view.layer addSublayer:cubeTwo];
}

- (CALayer *)layerWithTransform:(CATransform3D)transform {
    
    CALayer *layer = [CALayer layer];
    
    layer.frame = CGRectMake(-50, -50, 100, 100);
    
    CGFloat red = (rand() / (double)INT_MAX);
    CGFloat green = (100000 / (double)INT_MAX);
    CGFloat blue = (rand() / (double)INT_MAX);
    
    layer.backgroundColor = [UIColor colorWithRed:red
                                            green:green
                                             blue:blue
                                            alpha:1.0f].CGColor;
    layer.transform = transform;
    
    return layer;
}

- (CALayer *)cubeWithTransform:(CATransform3D)transform {
    
    // cube
    CATransformLayer *cube = [CATransformLayer layer];
    
    // layer one
    CATransform3D transform3D = CATransform3DMakeTranslation(0, 0, 50);
    [cube addSublayer:[self layerWithTransform:transform3D]];
    
    // layer two
    transform3D = CATransform3DMakeTranslation(50, 0, 0);
    transform3D = CATransform3DRotate(transform3D, M_PI_2, 0, 1, 0);
    [cube addSublayer:[self layerWithTransform:transform3D]];
    
    // layer three
    transform3D = CATransform3DMakeTranslation(0, -50, 0);
    transform3D = CATransform3DRotate(transform3D, M_PI_2, 1, 0, 0);
    [cube addSublayer:[self layerWithTransform:transform3D]];

    // layer five
    transform3D = CATransform3DMakeTranslation(-50, 0, 0);
    transform3D = CATransform3DRotate(transform3D, -M_PI_2, 0, 1, 0);
    [cube addSublayer:[self layerWithTransform:transform3D]];

    // layer six
    transform3D = CATransform3DMakeTranslation(0, 0, -50);
    transform3D = CATransform3DRotate(transform3D, M_PI, 0, 1, 0);
    [cube addSublayer:[self layerWithTransform:transform3D]];

    CGSize containerSize = self.view.bounds.size;
    
    cube.position = CGPointMake(containerSize.width / 2.0,
                                containerSize.height / 2.0);
    
    cube.transform = transform;
    
    return cube;
}
複製程式碼
12
13
14

CAGradientLayer

Layer中, 有一種顏色平滑漸變的子類, 叫做CAGradientLayer, 雖然用Core Graphics也可以通過一些技巧做到和CAGradientLayer一樣的效果, 但CAGradieLayer真正好, 是好在它是用硬體加速來繪製的, 直接來看程式碼吧:

- (void)gradientLayer {
    
    UIView *view = [[UIView alloc] init];
    
    view.bounds = CGRectMake(0, 0, 200, 200);
    view.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
    
    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = view.bounds;
    
    // 設定漸變的顏色, 理論上來講是無限新增的
    gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor,
                             (__bridge id)[UIColor greenColor].CGColor];
    
    gradientLayer.startPoint = CGPointMake(0, 0); // 開始漸變的點
    gradientLayer.endPoint = CGPointMake(1, 1); // 結束漸變的點
    
    gradientLayer.locations = @[@0.0, @0.2]; // 設定漸變的區域
    
    [view.layer addSublayer:gradientLayer];
    
    [self.view addSubview:view];
}
複製程式碼
15
16

CAReplicatorLayer

CALayer的子類當中還有一個叫做CAReplicatorLayer, 它是用來複制重複的圖層, 並且, 你可以給這些複製的圖層進行一些屬性上的操作, 比如漸變色, 漸變透明, 形狀, 還可以加動畫效果, 來看看程式碼吧:

#pragma mark - CAReplicatorLayer
- (void)replicatorLayer {
    
    UIView *view = [[UIView alloc] init];
    
    view.bounds = CGRectMake(0, 0, 100, 100);
    view.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 4.5);
    
    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);

    CAReplicatorLayer *replicatorLayer = [CAReplicatorLayer layer];
    
    replicatorLayer.frame = view.bounds;
    replicatorLayer.instanceCount = 10;  // 複製圖層個數
    replicatorLayer.instanceBlueOffset = -1.0f; // 設定每一個圖層的逐漸藍色偏移
    replicatorLayer.instanceRedOffset = -1.0f;  // 設定每一個圖層的逐漸紅色偏移
    replicatorLayer.instanceAlphaOffset = -0.1f;
    replicatorLayer.instanceDelay = 0.33f;  // 設定每個圖層延遲0.33f
    replicatorLayer.instanceTransform = transform;
    
    CALayer *layer = [CALayer layer];
    
    layer.frame = CGRectMake(0, 0, 100, 100);
    layer.backgroundColor = [UIColor whiteColor].CGColor;
    
    [replicatorLayer addSublayer:layer];
    
    self.view.backgroundColor = [UIColor grayColor];
    
    [view.layer addSublayer:replicatorLayer];
    
    [self addLayerAnimation:layer];
    
    [self.view addSubview:view];
}

- (void)addLayerAnimation:(CALayer *)layer {
    
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position.y"];
    
    animation.toValue =  @(layer.position.y - 25.0);
    animation.duration = 0.5;
    animation.autoreverses = true;
    animation.repeatCount = CGFLOAT_MAX;
    
    [layer addAnimation:animation forKey:nil];
}
複製程式碼
17
18

Reflections

CAReplicatorLayer其實還有一個更加實用的功能, 就是做一個鏡面反射的效果, 我們可以自己封裝一個UIView的類, 也可以自己寫一個簡單的, 這裡我就寫個簡單點的吧, 大家也可以去GitHub裡面搜搜, 我在網上搜到一個, 雖然這個庫已經2年多沒更新了, 但還是值得看看的ReflectionView.

- (void)reflectionsLayer {
    
    CAReplicatorLayer *replicatorLayer = [CAReplicatorLayer layer];
    
    replicatorLayer.instanceCount = 2;
    replicatorLayer.frame = CGRectMake(50, 100, 100, 100);

    CALayer *layer = [CALayer layer];
    
    layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"github"].CGImage);
    layer.frame = replicatorLayer.bounds;

    CATransform3D transform = CATransform3DIdentity;
    
    transform = CATransform3DTranslate(transform, 0, layer.bounds.size.height, 0);
    transform = CATransform3DScale(transform, 1, -1, 0);
    
    replicatorLayer.instanceTransform = transform;
    replicatorLayer.instanceAlphaOffset = -0.6;
    
    [replicatorLayer addSublayer:layer];
    
    [self.view.layer addSublayer:replicatorLayer];
    self.view.backgroundColor = [UIColor grayColor];
}
複製程式碼
19
20

CAScrollLayer

CALayer的子類當中, 還有一個CAScrollLayer, 它可以被稱為UIScrollView的代替品, 但有一個問題, 我們都知道Core Animation是不能處理使用者輸入, 所以CAScrollLayer也不能處理滑動事件, 也不能實現UIScrollView那種滑動反彈效果, 但這裡加了一個滑動手勢就可以實現了滑動效果了.

@interface ViewController ()

@property (nonatomic, strong) CAScrollLayer *scrollLayer;

@end

#pragma mark - CAScrollLayer
- (void)addScrollLayer {
    
    CALayer *layer = [CALayer layer];
    
    layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"github"].CGImage);
    layer.frame = CGRectMake(0, 0, 300, 300);
    
    self.scrollLayer = [CAScrollLayer layer];
    self.scrollLayer.frame = CGRectMake(50, 100, 150, 150);
    self.scrollLayer.scrollMode = kCAScrollBoth;
    self.scrollLayer.backgroundColor = [UIColor grayColor].CGColor;
    
    [self.scrollLayer addSublayer:layer];
    
    [self.view.layer addSublayer:self.scrollLayer];
    
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGesture:)];
    
    [self.view addGestureRecognizer:pan];
}

- (void)panGesture:(UIPanGestureRecognizer *)pan {
    
    CGPoint translocation = [pan translationInView:self.view];
    CGPoint origin = self.scrollLayer.bounds.origin;
    
    origin = CGPointMake(origin.x - translocation.x, origin.y - translocation.y);
    
    [self.scrollLayer scrollToPoint:origin];
    
    [pan setTranslation:CGPointZero inView:self.view];
}
複製程式碼

CATiledLayer

在我們開發當中, 有時候我們會需要載入一張超大的圖片, 比如神馬4K高清圖, 或者是世界地圖等等之類的, 但是在iOS當中是有記憶體限制的, 並不像其他系統一樣4G, 6G有超大記憶體, 如果我們要把超大的圖片載入到記憶體當中, 那很明顯, 直接會撐爆, 或者是加速速度慢得感人, 如果你是在主執行緒中使用UIImage的**+ (nullable UIImage )imageNamed:(NSString )name;或者是– (nullable instancetype)initWithContentsOfFile:(NSString )path方法來載入圖片的話, 那你會驚喜的發現, 卡執行緒了~~

在iOS當中, 能夠高效的繪製並且載入到介面的圖片是有一個大小限制的, 因為在iOS當中所有顯示在螢幕上的圖片最終都會被轉化為OpenGL的紋理, 同時OpenGL是有一個最大紋理尺寸的限制, 根據裝置的型號來決定, 通常是20482048或者40964096*, 如果我們想在單個紋理中顯示一個比這個限制尺寸還要大的圖, 哪怕圖片已經存在於記憶體當中, 我們也會遇到非常大的效能問題, 因為Core Animation是強制用CPU處理圖片, 而不是GPU, 蘋果為了解決這個問題, 於是乎有了CATiledLayer, 下面我們來看看Demo:

由於我不懂怎麼把大圖分解成小圖, 這裡就找張小一點的圖用用

- (void)addCATileLayer {
    
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.width)];
    
    [self.view addSubview:scrollView];
    
    CATiledLayer *tiledLayer = [CATiledLayer layer];
    
    tiledLayer.frame = CGRectMake(0, 0, 2048, 2048);
    tiledLayer.delegate = self;
    tiledLayer.contentsScale = [UIScreen mainScreen].scale;
    
    [scrollView.layer addSublayer:tiledLayer];
    
    scrollView.contentSize = tiledLayer.frame.size;
    
    [tiledLayer setNeedsDisplay];
}

- (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx {
    
    CGRect bounds = CGContextGetClipBoundingBox(ctx);
    
    NSInteger x = floor(bounds.origin.x / layer.tileSize.width);
    NSInteger y = floor(bounds.origin.y / layer.tileSize.height);
    
    NSString *imageName = [NSString stringWithFormat:@"image%02zd_%02zd", x, y];
    
    UIImage *tileImage = [UIImage imageNamed:imageName];
    
    UIGraphicsPushContext(ctx);
    
    [tileImage drawInRect:bounds];
    
    UIGraphicsPopContext();
}
複製程式碼
21

這裡我們注意到, 我預設是用Retina模式去顯示圖片的, 所以我們看起來這些圖片會比較小, 如果你不想用Retina模式去顯示, 你可以把程式碼中的一句程式碼刪除即可:

    tiledLayer.contentsScale = [UIScreen mainScreen].scale;
複製程式碼

如果我們要做到像地圖那樣子放大縮小的話, 那就要自己頭腦風暴一下, 然後想著如何去實現了~~


CAEmitterLayer

iOS 5版本中, 蘋果加入了一個新的CALayer子類, 叫做CAEmitterLayer, 它是一個高效能的粒子引擎, 常用於製作實時效果的動畫, 比如煙霧, 火, 雨等等之類的.

其實仔細想想, CAEmitterLayer看起來更像是一個容器, 裡面裝載著很多的CAEmitterCell, 這些CAEmitterCell定義了一個粒子效果, 然後在CAEmitterLayer的裝載中顯示出來.

CAEmitterCell類似於一個普通的CALayer, 它有一個contents的屬性, 可以定義為一個CGImage, 但不同於普通的CALayer的是它有一些課設定屬性控制著表現和行為, 想了解更多的話, 大家可以自行去CAEmitterCell的標頭檔案找找, 現在我們來看看Demo:

- (void)addCAEmitterLayer {
    
    UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 100,
                                                                   self.view.frame.size.width,
                                                                   self.view.frame.size.width)];
    
    [self.view addSubview:contentView];
    
    CAEmitterLayer *emitterLayer = [CAEmitterLayer layer];
    
    emitterLayer.frame = contentView.bounds;
    emitterLayer.renderMode = kCAEmitterLayerAdditive;
    emitterLayer.emitterPosition = CGPointMake(emitterLayer.frame.size.width / 2,
                                               emitterLayer.frame.size.height / 2);
    
    [contentView.layer addSublayer:emitterLayer];
    
    CAEmitterCell *cell = [[CAEmitterCell alloc] init];
    
    cell.contents = (__bridge id _Nullable)([UIImage imageNamed:@"fire"].CGImage);
    cell.birthRate = 150;
    cell.lifetime = 5.0;
    cell.color = [UIColor colorWithRed:1.f
                                 green:0.5f
                                  blue:0.1f
                                 alpha:1.0f].CGColor;
    cell.alphaSpeed = -0.4f;
    cell.velocity = 50.f;
    cell.velocityRange = 50.f;
    cell.emissionRange = M_PI * 2.0f;
    
    emitterLayer.emitterCells = @[cell];
}
複製程式碼
22

這裡補充一下知識點, CAEMitterCell基本上可以分為三種:

  • 粒子的某一屬性的初始值, 比如:color屬性指定了一個圖片的混合色, 在Demo當中我們就設定了某個顏色.

  • 粒子某一屬性的變化範圍, 比如:emissionRange, 在Demo當中, 我們設定為M_PI * 2.0f, 這意味著粒子可以從360°的任意位置反射出來.

  • 粒子在指定值的時間線上的變化, 比如: alphaSpeed, 子啊Demo中, 我們設定為**-0.4f**, 這意味著, 每過一秒, 粒子的透明度就減少0.4, 這樣子就有漸漸消失的效果啦.

    CAEmitterLayer它是控制著整個粒子系統的位置和形狀, 比如birthRate, lifetimecelocity, 當然, CAEMitterCell也有這些屬性, 整個粒子系統都是這些屬性以相乘的方式作用在一起, 這樣子我們就可以用一個值來加速或者擴大整個粒子系統.

    我們還需要知道另外兩個比較重要的屬性:

  • preservesDepth: 是否將一個3D的粒子系統平面化到一個圖層, 或者可以在3D空間中混合其他圖層.

  • renderMode: 控制著粒子圖片在視覺上是如何混合的, 在Demo當中, 我們設定為kCAEmitterLayerAdditive效果, 預設值為kCAEmitterLayerUnordered, 在開發當中需要什麼樣的效果, 還是得根據需求的來~


CAEAGLLayer

iOS當中, 如果我們需要高效能的圖形繪製, 那肯定是少不了去了解OpenGL, 這裡說的是非遊戲類的應用哈, 畢竟遊戲有屬於自己的一套渲染庫, 說起OpenGL, 肯定有很多人覺得這個框架很厲害, 的確是的, 因為OpenGL是用C來寫的, 直接和硬體進行通訊, 但是呢, 也因為是用C所寫的, 幾乎有沒有抽象出來的介面, 如果你要直接使用OpenGL來把圖形顯示在螢幕上, 那你就需要寫非常多的複雜程式碼, 雖然OpenGL是非常強大的神器, 因為OpenGLCore AnimationUIKit的基礎.

OpenGL中, 是沒有物件和圖層繼承的概念, 它只是非常簡單的去處理三角形, 在OpenGL中, 所有東西都是3D空間中有顏色和紋理的三角形, 感覺灰常的牛逼~

如果我們要高效的時候Core Animation, 那麼我們就需要判斷我們需要繪製哪些內容, 比如(向量圖形, 粒子, 文字等等), 但即使是我們選擇了合適的圖層去呈現這些內容, Core Animation中也不是每個型別的內容都被高度優化過, 所以要想得到高效能的去繪製, 那就比較蛋疼了.

iOS 5中, 蘋果為了解決這些蛋疼的問題, 加入了一個叫做GLKit的庫, 它在一定層度上減少了使用OpenGL的複雜度, 提供了一個叫做GLKViewUIView子類, 幫我們處理大部分的設定內容和繪製工作, 有需要了解GLKit的朋友們可以去翻翻官方文件.

即使是如此, 我們還是需要使用到一個叫做CAEAGLLayerCALayer子類, 醬紫我們才可以用來顯示OpenGL的圖形.

這裡還需要提到一點, 雖然在大部分情況下, 我們不需要手動設定CAEAGLLayer(如果是用GLKView的話), 我們可以設定一個OpenGL ES 2.0的上下文, 這是大多數的用法, GLKit為我們提供許多便捷的方法, 比如設定頂點和片段的著色器之類的, 這些都是以類C語言叫做GLSL自包含在程式中, 同事在執行時載入到圖形硬體中, 當然, GLSL的程式碼和設定CAEAGLLayer是一毛錢關係都沒, 所以我們會用GLKBaseEffect類, 將著色的邏輯抽象出來就完事, 其他的事情, 還是和平常使用一樣就哦了, 下面讓我們來看看Demo:

- (void)addCAEAGLLayer {
    
    UIView *glView = [[UIView alloc] initWithFrame:CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.width)];
    
    [self.view addSubview:glView];
    
    // 設定Context
    self.glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    
    [EAGLContext setCurrentContext:self.glContext];
    
    // 設定顯示的Layer
    self.glLayer = [CAEAGLLayer layer];
    self.glLayer.frame = glView.bounds;
    [glView.layer addSublayer:self.glLayer];
    self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking : @NO,
                                        kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8};
    
    self.effect = [[GLKBaseEffect alloc] init];
    
    [self setUpBuffers];
    [self drawFrame];
}

- (void)setUpBuffers {
    
    // 設定Frame
    glGenFramebuffers(1, &_framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
    
    // 設定顏色
    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);
    
    // 檢查是否成功
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        
        NSLog(@"%i", glCheckFramebufferStatus(GL_FRAMEBUFFER));
    }
}

- (void)tearDownBuffers {
    
    if (_framebuffer) {
        
        glDeleteFramebuffers(1, &_framebuffer);
        _framebuffer = 0;
    }
    
    if (_colorRenderbuffer) {
        
        glDeleteRenderbuffers(1, &_colorRenderbuffer);
        _colorRenderbuffer = 0;
    }
}

- (void)drawFrame {
    
    // 繫結緩衝區
    glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
    glViewport(0, 0, _framebufferWidth, _framebufferHeight);
    
    [self.effect prepareToDraw];
    
    // 清空螢幕
    glClear(GL_COLOR_BUFFER_BIT);
    glClearColor(0.0, 0.0, 0.0, 1.0);
    
    // 設定頂點
    GLfloat vertices[] = {
        -0.5f, -0.5f, -1.0f,
        0.0f, 0.5f, -1.0f,
        0.5f, -0.5f, -1.0f};
    
    // 設定顏色值
    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};
    
    // 開始畫三角形
    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);
    
    // 渲染
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
    [self.glContext presentRenderbuffer:GL_RENDERBUFFER];
}

- (void)dealloc {
    [self tearDownBuffers];
    
    [EAGLContext setCurrentContext:nil];
}
複製程式碼
23

如果我們要做一個真正的OpenGL應用,


AVPlayerLayer

最後一個圖層型別叫做AVPlayerLayer, 看名字就知道它並不屬於Core Animation裡的一個部分, 它是由AVFoundation所提供, 但它和Core Animation緊密的結合在一起, 並且是CALayer的子類, 可以用來顯示自定義內容.

實際上AVPlayerLayer是用來在iOS上播放視訊的, 是屬於MPMoivePlayer的底層實現, 提供了顯示視訊的底層支援.

AVPlayerLayer使用起來比較簡單, 我們可以直接來看看Demo:

- (void)addAVPlayerLayer {
    
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"demo0"
                                         withExtension:@"m4v"];
    
    AVPlayer *player = [AVPlayer playerWithURL:url];
    AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
    
    playerLayer.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
    
    [self.view.layer addSublayer:playerLayer];
    
    [player play];
}
複製程式碼
24

我們知道了AVPlayerLayerCALayer的子類, 那麼它應當也有父類的所有特性, 比如3D, 圓角, 有色邊框, 蒙版, 陰影等等效果都有, 我們再原來的基礎上再改改~

- (void)addAVPlayerLayerTwo {
    
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"demo0"
                                         withExtension:@"m4v"];
    
    AVPlayer *player = [AVPlayer playerWithURL:url];
    AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
    
    playerLayer.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
    
    [self.view.layer addSublayer:playerLayer];
    
    CATransform3D transform = CATransform3DIdentity;
    transform.m34 = -1.0 / 500.0;
    transform = CATransform3DRotate(transform, M_PI_4, 1, 1, 0);
    
    playerLayer.transform = transform;
    playerLayer.masksToBounds = YES;
    playerLayer.cornerRadius = 30.f;
    playerLayer.borderColor = [UIColor blueColor].CGColor;
    playerLayer.borderWidth = 10.f;
    
    [player play];
}
複製程式碼
25

總結

好了, 這次我們講到這裡了, 在這章裡, 我們認識CALayer的一些子類, 以及它們的一些特性, 方便我們在開發當中實現我們想要的效果時提供了多一些的參考, 但是呢, 這還遠遠不夠, 我們只是初步的去了解這些CALayer子類的皮毛, 單單CATiledLayerCAEMitterLayer兩個子類我們都可以單獨抽出來寫一長串的東東, 這個還是後面再說吧, 重點是, 我們要記住, CALayer的用處非常之大, 雖然有一些CALayer的子類並沒有為所有可能出現的場景進行優化, 這個就要靠我們自己的頭腦風暴去思考如何才能更好的去優化了.


工程地址

專案地址: https://github.com/CainRun/CoreAnimation


最後

碼字很費腦, 看官賞點飯錢可好
微信
支付寶

相關文章