LayKit中的专用图层
Core Animation其实是一个令人误解的命名。你可能认为它只是用来做动画的,但实际上它是从一个叫做Layer Kit这么一个不怎么和动画相关的名字演变而来,所以做动画这只是Core Animation特性的冰山一角。这篇文章主要介绍LayKit的十个专用图层
CAShapeLayer
CATextLayer
CATransformLayer
CAGradientLayer
CAReplicatorLayer
CAScrollLayer
CATiledLayer
CAEmitterLayer
CAEAGLLayer
AVPlayerLayer
一、CAShapeLayer
CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类。你指定诸如颜色和线宽等属性,用CGPath来定义想要绘制的图形,最后CAShapeLayer就自动渲染出来了。当然,你也可以用Core Graphics直接向原始的CALayer的内容中绘制一个路径,相比之下,使用CAShapeLayer有以下一些优点:
-
渲染快速。。CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快的多。
-
高效使用内存。一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。
-
不会被图层边界裁剪掉。一个CAShapeLayer可以在边界之外绘制。你的图层路径不会像在使用Core Graphics的普通CALayer一样被裁剪掉。
-
不会出现像素化。当你给CAShapeLayer做3D变换时,它不像一个有寄存图的普通图层一样变得像素化。
CAShapeLayer可以用来绘制所有能够通过CGPath来表示的形状。这个形状不一定要闭合,图层路径也不一定要不可破,事实上你可以在一个图层上绘制好几个不同的形状。你可以控制一些属性比如lineWith(线宽,用点表示单位),lineCap(线条结尾的样子),和lineJoin(线条之间的结合点的样子);但是在图层层面你只有一次机会设置这些属性。如果你想用不同颜色和风格来绘制多个形状,就不得不为每个形状准备一个图层了。
案例一:用一个CAShapeLayer渲染一个简单的火柴人。CAShapeLayer属性是CGPathRef类型,但是我们用CGPathRef类型,但是我们用UIBezierPath帮助类创建了图层路径,这样我们就不用考虑人工释放CGPath了。
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(175, 100)];
[path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:M_PI*2 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)];
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;
[self.view.layer addSublayer:shapeLayer];
![](https://img.haomeiwen.com/i2460271/b089d04c2573cb3f.png)
案例二:CAShapeLayer为创建圆角视图提供了一个方法,就是CALayer的cornerRadius属性。虽然使用CAShapeLayer类需要更多的工作,但是它有一个优势就是可以单独指定每个角。
我们创建圆角矩形其实就是人工绘制单独的直线和弧度,但是事实上UIBezierPath有自动绘制圆角矩形的构造方法,下面这段代码绘制了一个有三个圆角一个直角的矩形:
// Do any additional setup after loading the view, typically from a nib.
CGRect rect = CGRectMake(50, 50, 100, 100);
CGSize radii = CGSizeMake(20, 20);
UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomRight |UIRectCornerTopLeft;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii];
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;
[self.view.layer addSublayer:shapeLayer];
我们可以通过这个图层路径绘制一个既有直角又有圆角的视图。如果我们想依照此图形来裁剪视图内容,我们可以把CAShapeLayer作为视图的宿主图层,而不是添加一个子视图。
![](https://img.haomeiwen.com/i2460271/c30be12ad298f8bf.png)
案例三:蒙版上切两个透明的圆
NSArray *hollowFrames = @[[NSValue valueWithCGPoint:CGPointMake(20.0, 120.0)], [NSValue valueWithCGPoint:CGPointMake(120.0, 120.0)]];
int radius = 15.0;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height) cornerRadius:0];
for (NSValue *point in hollowFrames) {
UIBezierPath *circlePath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(point.CGPointValue.x, point.CGPointValue.y, 2.0*radius, 2.0*radius) cornerRadius:radius];
[path appendPath:circlePath];
}
[path setUsesEvenOddFillRule:YES];
CAShapeLayer *fillLayer = [CAShapeLayer layer];
fillLayer.path = path.CGPath;
fillLayer.fillRule = kCAFillRuleEvenOdd;
fillLayer.fillColor = [UIColor blackColor].CGColor;
fillLayer.opacity = 0.5;
[self.view.layer addSublayer:fillLayer];
}
![](https://img.haomeiwen.com/i2460271/a79471bafc042407.png)
案例四:路径动画
#import "DrawView.h"
@interface DrawView ()
@property (nonatomic, strong) UIBezierPath *path;
@end
@implementation DrawView
- (UIBezierPath *)path
{
if (_path == nil) {
_path = [UIBezierPath bezierPath];
}
return _path;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
// 获取手指的触摸点
CGPoint curP = [touch locationInView:self];
[self.path moveToPoint:curP];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
// 获取手指的触摸点
CGPoint curP = [touch locationInView:self];
[self.path addLineToPoint:curP];
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
[self.path stroke];
}
// 跟随路径而动画
- (void)startAnim
{
// 图层
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = self.path.CGPath;
layer.fillColor = [UIColor whiteColor].CGColor;
layer.strokeColor = [UIColor redColor].CGColor;
layer.strokeEnd = 1;
[self.layer addSublayer:layer];
CABasicAnimation *anim = [CABasicAnimation animation];
anim.keyPath = @"strokeEnd";
anim.fromValue = @0;
anim.toValue = @1;
anim.duration = 5;
[layer addAnimation:anim forKey:nil];
// 清空画线
[self.path removeAllPoints];
[self setNeedsDisplay];
}
@end
startAnim只有在花过的时候才能调用
二、CATextLayer
用户界面是无法从一个单独的图片里面构建的。一个设计良好的图表能够很好地表现一个按钮或控件的意图,不过你迟早都要需要一个不错的老式风格的文本标签。
如果你想在一个图层里面显示文字,完全可以借助图层代理直接将字符串使用CoreGraphics写入图层的内容(这就是UILabel的精髓)。如果越过寄宿于图层的视图,直接在图层上操作,那其实相当繁琐。你要为每一个显示文字的图层创建一个能像图层代理一样工作的类,还要逻辑上判断哪个图层需要显示哪个字符串,更别提还要记录不同的字体,颜色等一系列乱七八糟的东西。
万幸的是这些都是不必要的,Core Animation提供了一个CALayer的子类CATextLayer,它以图层的形式包含了UILabel几乎所有的绘制特性,并且额外提供了一些新的特性。
同样,CATextLayer也要比UILabel渲染得快得多。很少有人知道iOS6及之前的版本,UILabel其实是通过WebKit来实现绘制的,这样就造成了当有很多文字的时候就会有极大的性能压力。而CATextLayer使用了Core text,并且渲染的非常快。
案例一:用CATextLayer来实现一个UILabel
CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = self.view.frame;
[self.view.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 = @"ni hao lalani hao lalani hao lalani hao lalani hao lalani hao lala";
textLayer.string = text;
CATextLayer的font属性不是一个UIFont类型,而是一个CFTypeRef类型。这样可以根据你的具体需要来决定字体属性应该是用CGFontRef类型还是CTFontRef类型(CoreText字体)。同时字体大小也是用fontSize属性单独设置的,同时字体大小也是用fontSize属性单独设置的,因为CTFontRef和CGFontRef并不像UIFont一样包含点大小。
另外,CATextLayer的string属性并不是你想象的NSString类型,而是id类型。这样你既可以用NSString也可以用NSAttributedString来指定文本了(注意,NSAttributedString并不是NSString的子类)。属性化字符串是iOS用来渲染字体风格的机制,它以特定的方式来决定指定范围内的字符串的原始信息,比如字体,颜色,字重,斜体等。
案例二:富文本
CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = self.view.bounds;
textLayer.contentsScale = [UIScreen mainScreen].scale;
[self.view.layer addSublayer:textLayer];
textLayer.alignmentMode = kCAAlignmentJustified;
textLayer.wrapped = YES;
UIFont *font = [UIFont systemFontOfSize:15];
NSString *text = @"woslnfowjfoejfowjefowfjoekfjlxkmclwk";
NSMutableAttributedString *string = nil;
string = [[NSMutableAttributedString alloc] initWithString:text];
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFloat fontSize = font.pointSize;
CTFontRef fontRef = CTFontCreateWithName(fontName, fontSize, nil);
NSDictionary *attribs = @{(__bridge id)kCTForegroundColorAttributeName:(__bridge id)[UIColor redColor].CGColor,(__bridge id)kCTFontAttributeName:(__bridge id)fontRef};
[string setAttributes:attribs range:NSMakeRange(0, text.length)];
NSDictionary *attribs2 = @{(__bridge id)kCTForegroundColorAttributeName:(__bridge id)[UIColor greenColor].CGColor,(__bridge id)kCTFontAttributeName:(__bridge id)fontRef};
[string setAttributes:attribs2 range:NSMakeRange(6, 5)];
CFRelease(fontRef);
textLayer.string = string;
有必要提一下的是,由于绘制的实现机制不同(Core Text和WebKit),用CATextLayer渲染和用UILabel渲染出的文本行距和字距也不是不尽相同的。
二者的差异程度(由使用的字体和字符决定)总的来说挺小,但是如果你想正确的显示普通标签和CATextLayer就一定要记住这一点。
我们已经证实了CATextLayer比UILabel有着更好的性能表现,同时还有额外的布局选项并且在iOS5上支持富文本。但是与一般的标签比较而言会更加繁琐一些。如果我们真的需求一个UILabel的可用替代品,最好能够在xib上创建我们的标签,而且尽可能的像一般的视图一样正常工作。
我们应该用CATextLayer作为宿主图层的UILabel子类,这样就可以随着视图自动调整大小而且也没有冗余的寄宿图了。
如果我们继承了UIView,那我们就可以重写+layerClass方法使得在创建的时候能返回一个不同的图层子类。UIView会在初始化的时候调用+layerClass方法,然后用它的返回类型来创建宿主图层。
案例三:自定义UILabel
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface LayerLabel : UILabel
@end
NS_ASSUME_NONNULL_END
#import "LayerLabel.h"
@implementation LayerLabel
+ (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{
[super 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
上面代码演示了一个UILabel子类LayerLabel用CATextLayer绘制它的问题,而不是调用一般的UILabel使用的较慢的-drawRect:方法。
把CATextLayer作为宿主图层的另一好处就是视图自动设置了contentsScale属性。
三、CATransformLayer
CATransformLayer能够用于构造一个层级的3D结构。
它不能显示它自己的内容,只有当存在一个能作用域子图层的变换它才真正存在。
/
// ViewController.m
// LayerKit
//
// Created by 张璠 on 2019/3/14.
// Copyright © 2019 张璠. All rights reserved.
//
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic , weak) UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(150, 300, 100, 100)];
self.containerView = containerView;
[self.view addSubview:containerView];
CATransform3D pt = CATransform3DIdentity;
pt.m34 = -1.0/500.0;
self.containerView.layer.sublayerTransform = pt;
CATransform3D c1t = CATransform3DIdentity;
c1t = CATransform3DTranslate(c1t, -100, 0, 0);
CALayer *cube1 = [self cubeWithTransform:c1t];
[self.containerView.layer addSublayer:cube1];
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];
}
- (CALayer *)faceWithTransform:(CATransform3D)transform
{
CALayer *face = [CALayer layer];
face.frame = CGRectMake(-50, -50, 100, 100);
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].CGColor;
face.transform = transform;
return face;
}
- (CALayer *)cubeWithTransform:(CATransform3D)transform{
CATransformLayer *cube = [CATransformLayer layer];
CATransform3D ct = CATransform3DMakeTranslation(0, 0, 50);
[cube addSublayer:[self faceWithTransform:ct]];
ct = CATransform3DMakeTranslation(50, 0, 0);
ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);
[cube addSublayer:[self faceWithTransform:ct]];
ct = CATransform3DMakeTranslation(0, -50, 0);
ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);
[cube addSublayer:[self faceWithTransform:ct]];
ct = CATransform3DMakeTranslation(0, 50, 0);
ct = CATransform3DRotate(ct, -M_PI_2, 1, 0, 0);
[cube addSublayer:[self faceWithTransform:ct]];
ct = CATransform3DMakeTranslation(-50, 0, 0);
ct = CATransform3DRotate(ct, -M_PI_2, 0, 1, 0);
[cube addSublayer:[self faceWithTransform:ct]];
ct = CATransform3DMakeTranslation(0, 0, -50);
ct = CATransform3DRotate(ct, M_PI,0, 1, 0);
[cube addSublayer:[self faceWithTransform:ct]];
CGSize containerSize = self.containerView.bounds.size;
cube.position = CGPointMake(containerSize.width/2, containerSize.height/2);
cube.transform = transform;
return cube;
}
@end
![](https://img.haomeiwen.com/i2460271/820488ab110855b5.png)
四、CAGradientLayer
CAGradientLayer是用来生成两种或更多颜色平滑渐变的,而且在绘制时使用了硬件加速。
案例一:基础渐变
我们将从一个简单的红变蓝的对角线渐变开始。这些渐变色彩放在一个数组中,并赋值给colors属性。这个数组成员接受CGColorRef类型的值(并不是从NSObject派生而来),所以我们要用通过bridge转换以确保编译正常。
CAGradientLayer也有startPoint和endPoint属性,他们决定了渐变的方向。这两个参数是以单位坐标系进行的定义,所以左上角坐标是{0,0},右下角坐标是{1,1}。
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic , weak) UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(150, 300, 100, 100)];
self.containerView = containerView;
[self.view addSubview:containerView];
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:gradientLayer];
gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor blueColor].CGColor];
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(1, 1);
}
@end
如果从左向右渐变endPoint变为(1,0),从上向下渐变endPoint变为(0,1)
![](https://img.haomeiwen.com/i2460271/7bb22f5f3486abda.png)
案例二:多重渐变
如果你愿意,colors属性可以包含很多颜色,所以创建一个彩虹一样的多重渐变也是很简单的。默认情况下,这些颜色在空间上均匀地被渲染,但是我们可以用loactions属性来调整空间。locations属性是一个浮点数值的数组(以NSNumber包装)。这些浮点数定义了colors属性中每个颜色的位置,同样的,也是以单位坐标系进行标定。0.0代表着渐变的开始,1.0代表着结束。
locations数组并不是强制要求的,但是如果你给它复制了就一定要确保locations的数组大小和colors数组大小一定要相同,否则你将会得到一个空白的渐变。
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic , weak) UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(150, 300, 100, 100)];
self.containerView = containerView;
[self.view addSubview:containerView];
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:gradientLayer];
gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor yellowColor].CGColor,(__bridge id)[UIColor blueColor].CGColor];
gradientLayer.locations = @[@0.0,@0.25,@0.5];
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(1, 0);
}
@end
案例三:折叠图片
UI布局
一个topView,一个bottomView,一个dragView。
topView的宽度和图片宽度一样,高度是图片宽度的一半;
bottomView和topView重合。
dragView是topView和bottomView的父控件,并且中心点与之重合,高度是其2倍,宽度与其相同。
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *topView;
@property (weak, nonatomic) IBOutlet UIImageView *bottomView;
@property (weak, nonatomic) IBOutlet UIView *dragView;
@property (nonatomic, weak) CAGradientLayer *gradientL;
@end
@implementation ViewController
// 一张图片必须要通过两个控件展示,旋转的时候,只旋转上部分控件
// 如何让一张完整的图片通过两个控件显示
// 通过layer控制图片的显示内容
// 如果快速把两个控件拼接成一个完整图片
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 通过设置contentsRect可以设置图片显示的尺寸,取值0~1
_topView.layer.contentsRect = CGRectMake(0, 0, 1, 0.5);
_topView.layer.anchorPoint = CGPointMake(0.5, 1);
_bottomView.layer.contentsRect = CGRectMake(0, 0.5, 1, 0.5);
_bottomView.layer.anchorPoint = CGPointMake(0.5, 0);
// 添加手势
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
[_dragView addGestureRecognizer:pan];
// 渐变图层
CAGradientLayer *gradientL = [CAGradientLayer layer];
// 注意图层需要设置尺寸
gradientL.frame = _bottomView.bounds;
gradientL.opacity = 0;
gradientL.colors = @[(id)[UIColor clearColor].CGColor,(id)[UIColor blackColor].CGColor];
_gradientL = gradientL;
// 设置渐变颜色
// gradientL.colors = @[(id)[UIColor redColor].CGColor,(id)[UIColor greenColor].CGColor,(id)[UIColor yellowColor].CGColor];
// 设置渐变定位点
// gradientL.locations = @[@0.1,@0.4,@0.5];
// 设置渐变开始点,取值0~1
// gradientL.startPoint = CGPointMake(0, 1);
[_bottomView.layer addSublayer:gradientL];
}
// 拖动的时候旋转上部分内容,200 M_PI
- (void)pan:(UIPanGestureRecognizer *)pan
{
// 获取偏移量
CGPoint transP = [pan translationInView:_dragView];
// 旋转角度,往下逆时针旋转
CGFloat angle = -transP.y / 200.0 * M_PI;
CATransform3D transfrom = CATransform3DIdentity;
// 增加旋转的立体感,近大远小,d:距离图层的距离
transfrom.m34 = -1 / 500.0;
transfrom = CATransform3DRotate(transfrom, angle, 1, 0, 0);
_topView.layer.transform = transfrom;
// 设置阴影效果
_gradientL.opacity = transP.y * 1 / 200.0;
if (pan.state == UIGestureRecognizerStateEnded) { // 反弹
// 弹簧效果的动画
// SpringWithDamping:弹性系数,越小,弹簧效果越明显
[UIView animateWithDuration:0.6 delay:0 usingSpringWithDamping:0.2 initialSpringVelocity:10 options:UIViewAnimationOptionCurveEaseInOut animations:^{
_topView.layer.transform = CATransform3DIdentity;
} completion:^(BOOL finished) {
// 设置阴影效果
_gradientL.opacity = 0;
}];
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
五、CAReplicatorLayer
CAReplicatorLayer的目的是为了高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换。
案例一:重复图层
我们在屏幕的中间创建了一个小白色方块图层,然后用CAReplicatorLayer生成十个图层组成一个圆圈。instanceCount属性指定了图层需要重复多少次。instanceTransform指定了一个CATransform3D 3D变换(这种情况下,下一图层的位移和旋转将会移动到圆圈的下一个点)。
变换是逐步增加的,每个实例都是相对于前一实例布局。这就是为什么这些复制体最终不是出现在同一位置上。
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic , weak) UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor yellowColor];
UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(150, 300, 100, 100)];
self.containerView = containerView;
[self.view addSubview:containerView];
CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
replicator.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:replicator];
replicator.instanceCount = 10;
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;
replicator.instanceBlueOffset = -0.1;
replicator.instanceGreenOffset = -0.1;
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(100, 100, 100, 100);
layer.backgroundColor = [UIColor whiteColor].CGColor;
[replicator addSublayer:layer];
}
@end
![](https://img.haomeiwen.com/i2460271/b42e46a91351dac2.png)
注意到当图层在重复的时候,他们的颜色也在变化:这是用instanceBlueOffset和instanceGreenOffset属性实现的。通过逐步减少蓝色和绿色通道,我们逐渐将图层颜色转换成了红色。
案例二:音量震动条
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *lightView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// CAReplicatorLayer复制图层,可以把图层里面所有子层复制
// 创建复制图层
CAReplicatorLayer *repL = [CAReplicatorLayer layer];
repL.frame = _lightView.bounds;
[_lightView.layer addSublayer:repL];
CALayer *layer = [CALayer layer];
layer.anchorPoint = CGPointMake(0.5, 1);
layer.position = CGPointMake(15, _lightView.bounds.size.height);
layer.bounds = CGRectMake(0, 0, 30, 150);
layer.backgroundColor = [UIColor whiteColor].CGColor;
[repL addSublayer:layer];
CABasicAnimation *anim = [CABasicAnimation animation];
anim.keyPath = @"transform.scale.y";
anim.toValue = @0.1;
anim.duration = 0.5;
anim.repeatCount = MAXFLOAT;
// 设置动画反转
anim.autoreverses = YES;
[layer addAnimation:anim forKey:nil];
// 复制层中子层总数
// instanceCount:表示复制层里面有多少个子层,包括原始层
repL.instanceCount = 3;
// 设置复制子层偏移量,不包括原始层,相对于原始层x偏移
repL.instanceTransform = CATransform3DMakeTranslation(45, 0, 0);
// 设置复制层动画延迟时间
repL.instanceDelay = 0.1;
// 如果设置了原始层背景色,就不需要设置这个属性
repL.instanceColor = [UIColor greenColor].CGColor;
repL.instanceGreenOffset = -0.3;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
案例三:活动指示器
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *redView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
CAReplicatorLayer *repL = [CAReplicatorLayer layer];
repL.frame = _redView.bounds;
[_redView.layer addSublayer:repL];
CALayer *layer = [CALayer layer];
layer.transform = CATransform3DMakeScale(0, 0, 0);
layer.position = CGPointMake(_redView.bounds.size.width / 2, 20);
layer.bounds = CGRectMake(0, 0, 10, 10);
layer.backgroundColor = [UIColor greenColor].CGColor;
[repL addSublayer:layer];
// 设置缩放动画
CABasicAnimation *anim = [CABasicAnimation animation];
anim.keyPath = @"transform.scale";
anim.fromValue = @1;
anim.toValue = @0;
anim.repeatCount = MAXFLOAT;
CGFloat duration = 1;
anim.duration = duration;
[layer addAnimation:anim forKey:nil];
int count = 20;
CGFloat angle = M_PI * 2 / count;
// 设置子层总数
repL.instanceCount = count;
repL.instanceTransform = CATransform3DMakeRotation(angle, 0, 0, 1);
repL.instanceDelay = duration / count;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
案例四:倒影
1.storyboard上放一个RepView,在RepView上面放一个UIImageView,大小都和图片大小相同。
2.在RepView.m文件中加入下列方法
// 设置控件主层的类型
+ (Class)layerClass
{
return [CAReplicatorLayer class];
}
3.在控制器中加入如下代码
#import "ViewController.h"
#import "RepView.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet RepView *repView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
CAReplicatorLayer *layer = (CAReplicatorLayer *)_repView.layer;
layer.instanceCount = 2;
CATransform3D transform = CATransform3DMakeTranslation(0, _repView.bounds.size.height, 0);
// 绕着X轴旋转
transform = CATransform3DRotate(transform, M_PI, 1, 0, 0);
// 往下面平移控件的高度
layer.instanceTransform = transform;
// 透明度、红绿蓝的色差
layer.instanceAlphaOffset = -0.1;
layer.instanceBlueOffset = -0.1;
layer.instanceGreenOffset = -0.1;
layer.instanceRedOffset = -0.1;
}
案例五:粒子效果
#import <UIKit/UIKit.h>
@interface DrawView : UIView
- (void)startAnim;
- (void)reDraw;
@end
#import "DrawView.h"
@interface DrawView ()
@property (nonatomic, strong) UIBezierPath *path;
@property (nonatomic, weak) CALayer *dotLayer;
@property (nonatomic, weak) CAReplicatorLayer *repL;
@end
@implementation DrawView
#pragma mark - 懒加载点层
- (CALayer *)dotLayer
{
if (_dotLayer == nil) {
// 创建图层
CALayer *layer = [CALayer layer];
CGFloat wh = 10;
layer.frame = CGRectMake(0, -1000, wh, wh);
layer.cornerRadius = wh / 2;
layer.backgroundColor = [UIColor blueColor].CGColor;
[_repL addSublayer:layer];
_dotLayer = layer;
}
return _dotLayer;
}
- (UIBezierPath *)path
{
if (_path == nil) {
_path = [UIBezierPath bezierPath];
}
return _path;
}
#pragma mark - 开始点击调用
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// 获取touch对象
UITouch *touch = [touches anyObject];
// 获取当前触摸点
CGPoint curP = [touch locationInView:self];
// 设置起点
[self.path moveToPoint:curP];
}
static int _instansCount = 0;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
// 获取touch对象
UITouch *touch = [touches anyObject];
// 获取当前触摸点
CGPoint curP = [touch locationInView:self];
// 添加线到某个点
[_path addLineToPoint:curP];
// 重绘
[self setNeedsDisplay];
_instansCount ++;
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
[_path stroke];
}
#pragma mark - 开始动画
- (void)startAnim
{
// 创建帧动画
CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
anim.keyPath = @"position";
anim.path = _path.CGPath;
anim.duration = 3;
anim.repeatCount = MAXFLOAT;
[self.dotLayer addAnimation:anim forKey:nil];
// 注意:如果复制的子层有动画,先添加动画,在复制
// 复制子层
_repL.instanceCount = _instansCount;
// 延迟图层动画
_repL.instanceDelay = 0.2;
}
#pragma mark - 加载完xib调用,创建复制层
- (void)awakeFromNib
{
// 创建复制层
CAReplicatorLayer *repL = [CAReplicatorLayer layer];
repL.frame = self.bounds;
[self.layer addSublayer:repL];
_repL = repL;
}
#pragma mark - 重绘
- (void)reDraw
{
// 清空绘图信息
_path = nil;
[self setNeedsDisplay];
// 把图层移除父控件,复制层也会移除。
[_dotLayer removeFromSuperlayer];
_dotLayer = nil;
// 清空子层总数
_instansCount = 0;
}
@end
六、CAScrollLayer
CAScrollLayer有一个-scrollToPoint:方法,它自动适应bounds的原点以便图层内容出现在滑动的地方。注意就是他所做的所有事情,Core Animation并不处理用户输入,所以CAScrollLayer并不负责将触摸事件转换为滑动事件,既不渲染滚动条,也不实现任何iOS指定行为例如滑动反弹。
案例一:自定义UIScrollView
我们创建一个自定义的UIView,用CAScrollLayer作为视图的宿主图层,然后用UIPanGestureRecognizer实现触摸时间响应。
#import "ScrollView.h"
@implementation ScrollView
+ (Class)layerClass{
return [CAScrollLayer class];
}
- (void)setup{
self.layer.masksToBounds = YES;
UIPanGestureRecognizer *recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
[self addGestureRecognizer:recognizer];
}
- (id)initWithFrame:(CGRect)frame{
if (self = [super initWithFrame:frame]) {
[self setup];
}
return self;
}
- (void)awakeFromNib{
[super awakeFromNib];
[self setup];
}
- (void)pan:(UIPanGestureRecognizer *)recognizer{
CGPoint offset = self.bounds.origin;
offset.x -= [recognizer translationInView:self].x;
offset.y -= [recognizer translationInView:self].y;
[(CAScrollLayer *)self.layer scrollToPoint:offset];
[recognizer setTranslation:CGPointZero inView:self];
}
@end
不同于UIScrollView,我们定制的滑动视图并没有实现任何形式的边界检查。图层极有可能滑出视图的边界并无限滑下去。CAScrollLayer并没有等同于UIScrollView中contentSize的属性,所以当CAScrollLayer滑动的时候完全没有一个全局的可滑动区域的概念,也无法自适应它的边界原点至你指定的值。它之所以不能自适应边界大小是因为它不需要,内容完全可以超出边界。
那你一定会奇怪用CAScrollLayer的意义到底何在,因为你可以简单地用一个普通的CALayer然后手动适应边界原点啊。UIScrollView并没有用CAScrollLayer,就是简单的通过直接操作图层边界来实现滑动。
CAScrollLayer有一个潜在的有用特性。如果你查看CAScrollLayer的头文件,你就会注意到有一个扩展分类实现了一些方法和属性:
- (void)scrollPoint:(CGPoint)p;
- (void)scrollRectToVisible:(CGRect)r;
@property(readonly) CGRect visibleRect;
scrollPoint:方法从图层树种查找并找到第一个可用的CAScrollLayer,然后滑动它使得指定点成为可视的。
scrollRectToVisible:方法实现了同样的事情只不过是作用在一个矩形上的。
visibleRect属性决定图层(如果存在的话)的哪部分是当前的可视区域。
所以,涉及到图层滑动的时候就可以用CAScrollLayer。
七、CATiledLayer
有些时候,你可能需要绘制一个很大的图片,常见的例子就是一个高像素的照片或者是地球表面的详细地图。iOS应用通畅运行在内存受限的设备上,所以读取整个图片到内存中使不明智的。载入大图可能会相当地慢,哪些对你看上去比较方便的做法(在主线程调用UIImage的-imageNamed:方法或者-imageWithContentsOfFile:方法)将会阻塞你的用户界面,至少会引起动画卡顿现象。
能高效绘制在iOS上的图片也有一个大小限制。所有显示在屏幕上的图片最终都会转化为OpenGL纹理,同事OpenGL有一个最大的纹理尺寸(通常是20482048,或40964096,这个取决于设备型号)。如果你想在单个纹理中显示一个比这大的图,即便图片已经存在于内存中了,你仍然会遇到很大的性能问题,因为Core Animation强制用CPU处理图片而不是更快的GPU。
CATiledLayer为载入大图造成的性能问题提供了一个解决方案:将大图分解成小片然后将他们单独按需载入。
案例一:小片裁剪
这个程序是一个简单的Mac OS命令行程序,将20482048分辨率的雪人图案裁剪成了64个不同的256256的小图(256*256是CATiledLayer的默认小图大小,默认大小可以通过titleSize属性更改)。
// main.m
// CATiledLayer的使用
//
// Created by 张璠 on 2019/3/17.
// Copyright © 2019 张璠. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 异常处理
if (argc < 2) {
NSLog(@"TileCutter arguments: inputfile");
return 0;
}
// 输入文件
NSString *inputFile = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];
// 每个tile 大小
CGFloat tileSize = 256; //output path
NSString *outputPath = [inputFile stringByDeletingPathExtension];
// 加载图片
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];
// 计算行数和列数
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];
NSDictionary *imageProps = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:0] forKey:NSImageCompressionFactor];
NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypeJPEG properties:imageProps];
CGImageRelease(tileImage);
//save file
NSString *path = [outputPath stringByAppendingFormat: @"_%02i_%02i.jpg", x, y];
[data writeToFile:path atomically:NO];
}
}
}
return 0;
}
在命令行中输入,回车就可以了
xcode中Products下文件的路径 图片的路径
既然我们有了裁切后的小图,我们就要让iOS程序用到他们。CATiledLayer很好地和UIScrollView集成在一起。除了设置图层和滑动视图边界以适配整个图片大小,我们真正要做的就是实现-drawLayer:inContext:方法,当需要载入新的小图时,CATiledLayer就会调用到这个方法。
#import "ViewController.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
当你滑动这个图片,你会发现当CATiledLayer载入小图的时候,他们会淡入到界面中。这是CATiledLayer的默认行为。(你可能已经在iOS 6之前的苹果地图程序中见过这个效果)你可以用fadeDuration属性改变淡入时长或直接禁用掉。CATiledLayer(不同于大部分的UIKit和Core Animation方法)支持多线程绘制,-drawLayer:inContext:方法可以在多个线程中同时地并发调用,所以请小心谨慎地确保你在这个方法中实现的绘制代码是线程安全的。
你也许已经注意到了这些小图并不是以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类的头文件中找到。
方案一:用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
CAEMitterCell的属性基本上可以分为三种:
-
这种粒子的某一属性的初始值。比如,color属性指定了一个可以混合图片内容颜色的混合色。在示例中,我们将它设置为桔色。
-
例子某一属性的变化范围。比如emissionRange属性的值是2π,这意味着例子可以从360度任意位置反射出来。如果指定一个小一些的值,就可以创造出一个圆锥形
-
指定值在时间线上的变化。比如,在示例中,我们将alphaSpeed设置为-0.4,就是说例子的透明度每过一秒就是减少0.4,这样就有发射出去之后逐渐小时的效果。
CAEmitterLayer的属性它自己控制着整个例子系统的位置和形状。一些属性比如birthRate,lifetime和celocity,这些属性在CAEmitterCell中也有。这些属性会以相乘的方式作用在一起,这样你就可以用一个值来加速或者扩大整个例子系统。其他值得提到的属性有以下这些:
-
preservesDepth,是否将3D例子系统平面化到一个图层(默认值)或者可以在3D空间中混合其他的图层
-
renderMode,控制着在视觉上粒子图片是如何混合的。你可能已经注意到了示例中我们把它设置为kCAEmitterLayerAdditive,它实现了这样一个效果:合并例子重叠部分的亮度使得看上去更亮。如果我们把它设置为默认的kCAEmitterLayerUnordered,效果就没那么好看了。
九、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).
案例一: 用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
在一个真正的OpenGL应用中,我们可能会用NSTimer或CADisplayLink周期性地每秒钟调用-drawRrame方法60次,同时会将几何图形生成和绘制分开以便不会每次都重新生成三角形的顶点(这样也可以让我们绘制其他的一些东西而不是一个三角形而已),不过上面这个例子已经足够演示了绘图原则了。
十、AVPlayerLayer
最后一个图层类型是AVPlayerLayer。尽管它不是Core Animation框架的一部分(AV前缀看上去像),AVPlayerLayer是有别的框架(AVFoundation)提供的,它和Core Animation紧密地结合在一起,提供了一个CALayer子类来显示自定义的内容类型。
AVPlayerLayer是用来在iOS上播放视频的。他是高级接口例如MPMoivePlayer的底层实现,提供了显示视频的底层控制。AVPlayerLayer的使用相当简单:你可以用+playerLayerWithPlayer:方法创建一个已经绑定了视频播放器的图层,或者你可以先创建一个图层,然后用player属性绑定一个AVPlayer实例。+
在我们开始之前,我们需要添加AVFoundation到我们的项目中。
案例一:用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
我们用代码创建了一个AVPlayerLayer,但是我们仍然把它添加到了一个容器视图中,而不是直接在controller中的主视图上添加。这样其实是为了可以使用自动布局限制使得图层在最中间;否则,一旦设备被旋转了我们就要手动重新放置位置,因为Core Animation并不支持自动大小和自动布局。
当然,因为AVPlayerLayer是CALayer的子类,它继承了父类的所有特性。我们并不会受限于要在一个矩形中播放视频;清单6.16演示了在3D,圆角,有色边框,蒙板,阴影等效果。
案例二:给视频增加变换,边框和圆角
- (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];
}