一、CAShapelayer
我们知道可以不使用图片情况下利用CGpath去构建任意形状的阴影。其实我们也可以用同样的方式创建图层的。
CAShapelayer是一个通过矢量图形而不是bitmap来绘制的图层子类。我们可以定义颜色和线宽等属性,用CGPath来定义想要绘制的图形,最后CAShapelayer就自动渲染出来了。当然也可以用CoreGraphics之间想原始的CAlayer中绘制一个路径。CAShapelayer就是对CoreGraphics的一个使用的封装,更方便使用,优点:
1)渲染快速。CAShapelayer使用了硬件加速,绘制同一个图形比直接用CoreGraphics快很多。
2)高效使用内存。一个CAShapelayer不需要像普通CAlayer一样创建一个寄宿图层,所以无论多大占用内存比较少。
3)不会被图层边界裁剪掉。一个CAShapelayer可以在边界之外绘制。不会像普通的CAlayer被边界裁掉。
4)不会出现像素化。当你给CAShapeLayer做3D变换时,它是矢量图,不会像有寄宿图的图层那样变的像素化。
创建一个路径
CAShapelayer可以用来绘制所有能通过CGPath来表示的形状。这个形状不一定要闭合,图层路径也不一定要不可破,事实上你可以在一个图层上绘制好几个不同的形状。我们可以控制一些属性linewidth:线宽,linecap:线结尾的样子,和lineJoin:线条之间结合点的样子;但这些属性在同一个图层中只能一次机会设置,如果想要不同的风格绘制多个形状,就要为每个形状准备一个图层。
这里我们用CAShapelayer渲染一个火柴人。CAShapelayer属性是CGPathRef类型,但是我们可以用UIBezierPath创建一个路径对象CGPath,可以与CGPathRef互相转换,这样我们就不用手动释放path对象了。
@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
圆角
在“视图与图层关系”中我们知道CAlayer的cornerRadius属性可以绘制圆角。虽然使用CAShapeLayer类需要更多工作,但是它有一个优势就是可以单独制定每一个角。
我们创建圆角矩形其实就是人工绘制单独的直线和弧线,UIBezierPath有自动绘制圆角矩形的构造方法,下面代码绘制了一个三个圆角一个直角的矩形。
//define path parameters
CGRect rect = CGRectMake(50, 50, 100, 100);//矩形的frame
CGSize radii = CGSizeMake(20, 20);
UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomRight | UIRectCornerBottomLeft;
//create path
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii];
我们可以通过这个图层路径绘制一个既有直角又有圆角的视图。如果想要按照这个路径来裁剪视图内容,我们可以吧CAlShapelayer作为视图的宿主图层,而不是当作一个子图层添加进去。
二、CATextLayer
用户界面为了更好的表达作者的意图,只用图片是不行的,我们还需要一个文本标签。UIKit中的UIlable是我们最熟悉的文本对象。但UIlable不是唯一的工具,这里我们学习CoreAnimation为我们提供一个图层类:CATextLayer,它以图层的形式包含了UILable的所有绘制特性,并提供一些额外的特性,比UIlable更灵活。
CATextLayer比UILable渲染快的多。据说在iOS6之前的版本,UILable其实是通过webkit来实现绘制的,这样在文字很多的时候就会有极大的性能压力。CATextLayer使用的是CoreText,更底层,疗效更好。
这里给一个简单的demo实现这个CATextLayer显示一些文字的功能。
@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 不能直接用UIFont,需要得到一个CGFontRef对象
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
如果仔细看这个文本,发现这些文本又点儿像素化。这是因为没有以Retina的方式渲染,在之前的学习中提到图层的contentScale属性,用来决定图层内容应该以怎样的分辨率来渲染。contentsScale并不关心屏幕的拉伸因素,总是默认1.0。如果想以Retina的质量来显示文本,我们就要手动设置:
textLayer.contentsScale = [UIScreen mainScreen].scale;
CATextLayer的font属性不是一个UIFont类型,而是一个CFTypeRef类型。这样可以根据具体需求来决定应该用CGFontRef还是CTFontRef(coreText字体)。同时字体大小也是fontSize属性单独设置。demo中给出了UIFont和CGFontRef的转换。
CATextLayer的string属性并不是NSString类型,是id类型。这样就既可以用NSString也可以用NSAttributedString来指定文本了。
富文本
iOS6中,Apple给UIlable和其他UIKit文本视图添加了直接的属性化字符串的支持,应该说这是一个方便的特性。不过iOS3.2开始CATextLayer就已经支持属性化字符串了。这样的话如果你想要支持更低版本的iOS系统,CATextLayer无疑是向界面中增加富文本的好办法,也不用和更复杂的CoreText打交道,也省了用UIWebView。
这里我们用coreText方式实现一段富文本。
上例子demo中的UIFont转化成了CGFontRef,这个是CoreGraphics实现。这里用到CoreText,那就要要用CTFontRef类型了,所以我们从font处改变。
//convert UIFont to a CTFont
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFloat fontSize = font.pointSize;
CTFontRef fontRef = CTFontCreateWithName(fontName, fontSize, NULL);
//create attributed string
NSMutableAttributedString *string = nil;
string = [[NSMutableAttributedString alloc] initWithString:text];//text是富文本的内容
//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)];//特定范围内文本为红色
行距和字距
由于绘制实现的机制不同CoreText和webkit,CoreGraphics,所以CATextlayer和UIlabel渲染出的文本行距和字距也是不同的。总的来说挺小的。不过要注意调整。
我们已经证实CATextlayer比UILabel有着更好的性能表现,但是使用要更麻烦一些,如果我们需要一个UILabel的替代品。我们可能会集成UILabel,然后添加一个子图层CATextLayer 并重写显示文本的方法。但是UILabel的-drawRect:方法创建一个空寄宿图。而且由于CAlayer不支持自动缩放和自动布局,子视图并不是主动跟踪视图宾级的大小,所以我不得不通过CAlayerDelegate处理子图层的边界。我们真正想要的是用一个CATextLayer作为宿主图层的UILabel字类,这样就省去跟随视图调整大小了。
我们在图层树中已经知道,每一个UIView都是寄宿在一个CAlayer的实例上。这个图层是有视图自动创建和管理的。一旦被创建我们就没法替代这个图层了,但是如果我们集成UIView,那我们可以重写+layerClass方法使得在创建时候能返回一个不同图层字类。UIView会在初始化的时候调用+layerClass,然后用它的类型来创建宿主图层。
@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
上面的demo创建了一个UIlabel的子类,它使用了CATextlayer作为宿主图层,比drawRect:CoreGraphics渲染快多了。如果我们使用这个label,发现文本并没有像素化,而我们也没有设置contentsScale属性。把CATextLayer作为宿主layer,视图自动就设置了contentsScale属性。当然可以扩展更多的功能。
三、CATransformLayer
当我们在构造复杂的3D事物的时候,如果能够组织独立元素就太方便了。比如创建一个孩子的手臂:我们就可以确定这个手臂的各部分:手腕、前臂、肘、上臂等等。
我们要独立的移动每个区域,以肘为支点能够移动前臂和手而不是肩膀。CoreAnimation图层比较容易就可以在2D环境下做成这样层级体系下的变换,但是3D情况下就太可能了,因为所有的图层都把它们的子图层平面化道一个场景,就是扁平化。
CATransformLayer解决了这个问题,CATransformLayer不同于普通的图层,因为它不能显示它自己的内容。只有当存在了一个能作用于子图层的变换他才能真正存在。CATransformLayer并不平面化它的子图层,所以它能够创建一个3D结构。
在“用图层创建一个立体对象”中我们通过旋转camara:视角来解决图层扁平化的问题而不是sublayerTransform。这是一个非常不错的技巧,但是只能作用于单个容器,如果场景包含两个立方体,我们就不能用这个技巧单独旋转它们了。
那我们试试CATransformlayer吧,在“用图层创建一个立体对象”中,我们是用多个视图来构建立方体,而不是单个图层。我们不能在不打乱已有的视图层次的前提下在一个本身不是有寄宿图的图层中放置一个寄宿图层。我们可以创建一个新的UIView子类寄宿在CATransformLayer用+layerClass。为了简化案例,我们仅仅重建了一个单独的图层,不使用视图。
我们以“创建一个立体对象”中使用过的相同基本逻辑放置立方体。但是我们不像以前那样直接将立方体面添加到容器视图的宿主图层,我们将它们放置到一个CATransformlayer中创建一个独立的立方体对象,然后将两个这样的立方体放进容器中。随机给力方面染色将它们区分开。
@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
更复杂的比如多部分的立体构图,不同TransformLayer之间的关系,这需要慢慢去琢磨了,总是这是一个成全其他图层的图层,幕后好人啊。
四、CAGradientLayer
CAGradientLayer使用来生成两种或者更多中颜色平滑渐变的。用CoreGraphics复制一个CAGradientLayer并将内容绘制到一个普通图层的寄宿图也是有可能的,但是CAGradientLayer的真正好吃在于绘制使用了硬件加速。
基础渐变
我们从一个简单的红变蓝的对角线渐变开始。渐变色彩放在一个数组中,并赋值给colors,数组内容是CGColorRef类型,需要我们用bridge转换。CAGradientLayer也有startPoint和endPoint属性,它们决定了渐变的方向。这个是单位坐标系,左上角(0,0)右下角(1,1);
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
- (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
多重渐变
如果愿意,colors可以包含很多颜色,就像彩虹一样。默认情况下这个颜色都是均匀的被渲染,但是我们可以用location属性来调整空间。locations属性是一个浮点数组,用NSNumber包装。这些浮点数定义了colors中不同颜色的位置,同样也是单位坐标。0.0代表渐变开始,1.0代表结束。需要注意的是,locatiions数组不是必需的,但是一旦要给它赋值就一定要确保和colors数组大小相同,否则会得到空白的渐变。
//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];
都挤在了左上角的三变色
至于做一条彩虹,就有时间再琢磨吧。
CAReplicatorLayer
CAReplicatorLayer 的目的是为了高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换。解释太饶,上demo。
重复图层
我们在屏幕的中间创建了一个小白色方块图层,然后用CAReplicatorLayer 生成十个图层组成一个圆圈。instanceCount属性指定了图层需要重复多少次。instanceTransform 指定了一个CATransform3D变换(下一个图层的位移和旋转将会移动到圆圈的下一个点)。
变换是是逐步增加的,每个实例都是相对于前一个实例布局。这就是为什么这些复制体最终不会出现在同一个位置上。
@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
图层在重复的时候,他们的颜色也在变化:instanceBlueOffset和instanceGreenoffset属性实现,通过逐步减少蓝色和绿色通道,我们逐渐将图层颜色转化成红色。这个复制效果看起很酷,但是CAReplicatorlayer真正应用到实际程序上的场景比如:一个游戏中导弹的轨迹云,或者粒子爆炸。还有一个实际的用处反射。
反射
使用CAReplicatorLayer 并应用一个负比例变换于一个复制图层,你就可以创建指定视图,或者整个视图层次内容的镜像图片,这样就创建了一个实时的反射效果。让我们来实现这个创意:指定一个继承于UIView的ReflectionView,他会自动产生内容的反射效果。
#import "ReflectionView.h"#import@implementation ReflectionView
+ (Class)layerClass
{
return [CAReplicatorLayer class];
}
- (void)setUp
{
//configure replicator
CAReplicatorLayer *layer = (CAReplicatorLayer *)self.layer;
layer.instanceCount = 2;
//move reflection instance below original and flip vertically
CATransform3D transform = CATransform3DIdentity;
CGFloat verticalOffset = self.bounds.size.height + 2;
transform = CATransform3DTranslate(transform, 0, verticalOffset, 0);
transform = CATransform3DScale(transform, 1, -1, 0);
layer.instanceTransform = transform;
//reduce alpha of reflection layer
layer.instanceAlphaOffset = -0.6;
}
- (id)initWithFrame:(CGRect)frame
{
//this is called when view is created in code
if ((self = [super initWithFrame:frame])) {
[self setUp];
}
return self;
}
- (void)awakeFromNib
{
//this is called when view is created from a nib
[self setUp];
}
@end
一个更丰富的对象在:https://github.com/nicklockwood/ReflectionView功能更完善。
六、CAScrollLayer
对于一个未转换的图层,它的bounds和它的frame是一样的,frame属性是由bounds属性计算而出的,所以更改任意一个值都会更新其他值。如果我们只想显示大图层里面的一小部分呢,比如说,一个很大的图,希望用户能够随意滑动,或者一个文本的长列表。在一个典型的iOS应用中,你可能会用刀UITableView或者UIScrollview,不过这都是视图层级的,但是对于独立的图层来说怎么办呢?我们知道图层的contentsRect 属性的用法,它确实能解决一部分需求。如果这个图层包含了子图层那就不好解决了,因为每次想滑动可视区域,需要手工重新计算所有子图层的位置。这时候就需要CAScrollayer了。
CAScrollayer有一个-scrollToPoint:方法,它自动适应bounds的原点以便图层内容出现在滑动的地方。前面提到过CoreAnimation并不处理用户的输入,所以CAScrollLayer并不负责触摸事件转换为滑动事件,既不渲染滚动条,也不实现任何滑动反弹。
我们可以用CAScrollLayer来实现一个UIScrollview。我们将会用CAScrollLayer作为视图的宿主图层,并创建一个自定义的UIView,然后用UIPanGestureRecognizer实现触摸事件响应。
#import "ScrollView.h"#import@implementation ScrollView
+ (Class)layerClass
{
return [CAScrollLayer class];
}
- (void)setUp
{
//enable clipping
self.layer.masksToBounds = YES;
//attach pan gesture recognizer
UIPanGestureRecognizer *recognizer = nil;
recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
[self addGestureRecognizer:recognizer];
}
- (id)initWithFrame:(CGRect)frame
{
//this is called when view is created in code
if ((self = [super initWithFrame:frame])) {
[self setUp];
}
return self;
}
- (void)awakeFromNib {
//this is called when view is created from a nib
[self setUp];
}
- (void)pan:(UIPanGestureRecognizer *)recognizer
{
//get the offset by subtracting the pan gesture
//translation from the current bounds origin
CGPoint offset = self.bounds.origin;
offset.x -= [recognizer translationInView:self].x;
offset.y -= [recognizer translationInView:self].y;
//scroll the layer
[(CAScrollLayer *)self.layer scrollToPoint:offset];
//reset the pan gesture translation
[recognizer setTranslation:CGPointZero inView:self];
}
@end
不同于UIScrollView,我们定制的滑动视图并没有实现任何形式的边界检查bounds checking。图层内容极有可能滑出边界,并无限滑下去。CAScrollLayer 并没有等同于UIScrollview中的contentSize属性,所以当CAScrollLayer滑动时候没有一个可滑动区域的概念。这样就会怀疑CAScrollLayer存在的意义了。
CAScrollLayer有一个潜在的有用特性。在头文件我们看到一个扩展分类实现了一些方法和属性:
- (void)scrollPoint:(CGPoint)p;
- (void)scrollRectToVisible:(CGRect)r;
@property(readonly) CGRect visibleRect;
-scrollPoint:方法从图层树中查找到第一个可用的CASrollLayer,然后滑动它使得指点成为可视。-scrollRectToVisible:方法实现了同样的事情,只不过作用在一个矩形上。visibleRect属性决定图层的哪部分是当前可视区域。
七CATiledLayer:瓦片图层
有时候我们可能需要绘制一个很大的图片,常见就是一个高像素的照片或者地球表面的详细地图。iOS应用要想流畅地运行在内存受限的设备上,读取整个图片到内存中是不明智的。我们常用图片加载方法,UImage的-imageNamed:或者imageWithContentsofFile:方法,将会阻塞你的用户界面,引起卡顿。
能高效的绘制在iOS上的图片也有一个大小的限制,所有显示在屏幕上的图片都是通过OpenGL纹理来渲染的。同时openGL有一个最大的纹理尺寸。通常(2048*2048,或者4096*4096,设备型号差异)。如果你想在单个纹理中显示一个比这个大的图,即便图片已经在内存中了,你仍然会遇到很大的性能问题,因为CoreAnimation强制用CPU处理图片而不是更快的GPU。CATiledLayer为载入大图造成的性能问题提供了一个解决方案:将大图分解成小片,然后将他们按照需要展示。
示例中,我们将从一个2048*2048分辨率的图片入手。为了利用CATiledLayer,我们需要把这个图片裁切成许多小一些的图片。如果在运行时,整体载入图片再裁切,那CATiledLayer的这些性能优点就损失殆尽了。理想情况下来说,最好能够一块一块儿的实现。
把bundle中存在的这张图片,先切成很多256*256的小块,当然大小可以自定义。
NSString *inputfile = [[NSBundle mainBundle] pathForResource:@"snowPeople" ofType:@"PNG"];
CGFloat tileSize = 256;
NSString *outputPath = [inputfile stringByDeletingPathExtension];//?去掉.png;.zip这类的后缀
//load image
UIImage *image = [[UIImage alloc]initWithContentsOfFile:inputfile];
CGSize size = [image size];
CGImageRef imageRef = image.CGImage;
//calculate rows and columns
NSInteger rows = ceil(size.height / tileSize);
NSInteger cols = ceil(size.width / tileSize);
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 tileImageRef = CGImageCreateWithImageInRect(imageRef, tileRect);
UIImage *tileImage = [UIImage imageWithCGImage:tileImageRef];
NSData *tileImagedata = UIImagePNGRepresentation(tileImage);
NSString *path = [outputPath stringByAppendingFormat: @"_%02i_%02i.jpg", x, y];
[tileImagedata writeToFile:path atomically:NO];存在本地
}
}
切好了64个不同的小图,怎么使用它们呢?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();
}
当你滑动这个图片,你会发现当CATiledLayer载入小图时候,它们会淡入界面中。这个CATiledLayer的默认行为。你可以用fadeDuration属性改变淡入的时长或者不用。CATiledLayer不同于大部分的UIKit和CoreAnimation,支持多线程绘制,-drawLayer:incontext:方法可以在多个线程中同地并发调用,所以注意线程安全与否。
这些小图注意到不是以Retina的分辨率显示的。为了以屏幕的原生分辨率来渲染CATiledLayer,我们需要设置layer的contentsScale来匹配UIScreen的scale属性。
tileLayer.contentsScale = [UIScreen mainScreen].scale;
有趣的是,tileSize是以像素为单位,而不是点,所以增大了contentsScale就自动有了默认的小图尺寸(现在它是128*128的点而不是256*256).所以,我们不需要手工更新小图的尺寸或是在Retina分辨率下指定一个不同的小图。我们需要做的是适应小图渲染代码以对应安排scale的变化,然而
//determine tile coordinate
CGRect bounds = CGContextGetClipBoundingBox(ctx);
CGFloat scale = [UIScreen mainScreen].scale;
NSInteger x = floor(bounds.origin.x / layer.tileSize.width * scale);
NSInteger y = floor(bounds.origin.y / layer.tileSize.height * scale);
通过这个方法纠正scale也意味着我们的雪人图将以一半的大小渲染在Retina设备上(总尺寸是1024*1024,而不是2048*2048)。这个通常都不会影响到用CATiledLayer正常显示的图片类型(比如照片和地图,他们在设计上就是要支持放大缩小,能够在不同的缩放条件下显示),但是也需要在心里明白。
八、CAEmitterLayer
在iOS5中,Apple引入了一个新的CAlayer子类是CAEmitterLayer。CAEmitterLayer是一个高性能的粒子引擎,被用来创建实时粒子动画:烟雾,火、雨等效果。
CAEmitterLayer看上去像是许多CAEmitterCell的容器,这些CAEmitterCell定义了一个粒子效果。我们会为不同的粒子效果穿件一个活着多个CAEmittercell,同时CAEmitterLayer负责基于这些粒子对象实例化一个粒子流。CAEmitterCell类似一个CAlayer:它有一个contents属性可以定义为一个CGImage,另外还有一些属性可以在CAEmittercell的头文件中找到。
举个栗子。我们将利用在一个圆中发射不同速度和透明度的粒子创建一个活爆炸的效果。
#import "ViewController.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的属性基本可以分为三种:
1)、这种粒子的某一属性的初始值。比如,color属性指定了可以混合图片内容的颜色的混合色。例子中设置为桔色。
2)、例子中某一属性的变化范围。比如emissionRange的属性是2π,这意味着例子可以从360度任意位置反射出来。如果小一些,就可以弄出一个圆锥型。
3)、指定值在时间线上的变化。比如,在例子中,我们将alphaSpeep设置为-0.4,粒子的透明度每过一秒就减少0.4,这样就发射出去逐渐消失的效果。
CAEmitterLayer的属性,它控制着整个粒子系统的位置和形状。一些属性,比如birthRat,lifetime和celocity,这些属性在CAEmitterCell中也有。这些属性会以相乘的方式左右在一起,这样就可以用一个值来加速活着扩大整个粒子系统。
1)、preservesDepth,是否将3D粒子系统平面化到一个图层,或者可以在3D空间混合其他图层。
2)、控制着粒子图片是如何混合的。例子中我们用kCAEmitterLayerAdditive,它实现了这样一个效果:合并粒子重叠部分使得看上去更亮。更逼真了。
九、CAEAGLLayer
当iOS要处理高性能的图形绘制,必要时候就的用OpenGL。对于非游戏应用来说这就是杀手锏了。因为相对于CoreAnimation和UIKIt,它TTM复杂了。
OpenGL提供了CoreAnimation的基础,它是底层的C接口,直接和iPhone、iPad的硬件通信,极少抽象出来方法。OpenGL没有对象或者是图层的概念,它只是简单的处理三角形。OpenGL中所有的东西都是3D空间中有颜色和纹理的三角形。用起来非常复杂和强大,但是用OpenGL绘制iOS用户界面就需要很多很多工作。
为了能够高性能使用CoreAnimation,你需要判断你需要绘制哪种内容(矢量图、粒子、文本等等),然后再用合适的图层去呈现这些内容,CoreAnimation中只有一些类型的内容是被高度优化的;所以如果你想绘制的东西并不能找到标准的图层类,想要得到高性能就比较费事儿了。
因为OpenGL根本不会对内容进行假设,它能够绘制地相当快。利用OpenGL,你可以绘制任何你想的集合信息和形状逻辑的内容。所有游戏开发喜欢用OpenGL(这些情况下,CoreAnimation的限制就明显了:它优化过的图层并不一定能满足需求),但是这样就没有了发给你的高度抽象的接口了。
在iOS5中,Apple引入了一个新的框架GLKit,它去掉了一些设置OpenGL的复杂性,提供了一个叫做CLKView的UIView的子类,帮我们处理大部分的设置和绘制工作。前提是各种各样的OpenGL绘图缓冲的底层配置项仍然需要用CAEAGLLayer完成,它是CALayer的一个字类,用来显示任意的OpenGL图形。
如果用GLKView,大部分情况下都不需要手动设置CAEAGLLayer。特别的我们将设置一个OpenGL ES2.0的上下文,它是iOS设备的标准做法。
尽管不需要GLKit也可以做到这一切,但是GLKit包括了很多额外的工作,比如设置定点和片段的着色器,这些都以C语音叫做GLSL自包含在程序中,同时在运行时载入到图形硬件中。编写GLSL代码和设置EAGLayer没有什么关系,所以我们将用GLKBaseEffect类将着色逻辑抽象出来。
在开始之前需要讲GLKIt和OpenGLES框架加入到项目中,例子中设置了一个CAEAGLLayer,它使用了OpenGL ES2.0的绘图上下文,渲染一个有色三角。
@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)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
- (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];
}
挺复杂的配置图层的效果。
在一个真正OpenGL的应用中,我们可能会用NSTimer或者CADisplayLink周期性地每秒钟调用-drawRrame方法60次,同时将集合图形生成和绘制的分开。
十、AVPlayerLayer
AVPlayerlayer不是CoreAnimation框架的一部分,AVPlayerlayer是AVFoundatiion提供的,它和CoreAnimation紧密结合在一起,提供了一个CALayer字类来显示自定义的内容类型。
AVPlayerlayer是用来在iOS上播放视频的。它是高级接口例如MPMoivePlayer的底层实现,提供了显示视频的底层控制。AVPlayerlayer的使用相当简单:我们可以用+playerLayerWithPlayer:方法创建一个已经绑定了视频播放器的图层,或者可以先创建一个图层,然后用player属性绑定一个AVPlayer实例。
在开始之前我们需要添加AVFoundation到我们的项目中。然后我们创建一个简单的电影播放器。
@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中的主视图上添加。为了避免自动布局时候图层大小位置发生变化。因为CoreAnimation并不支持自动大小和自动布局。
当然,因为AVPlayerLayer是CAlayer的子类,它继承了父类的所有特性。所以可添加3D,圆角,有色边框,蒙版、阴影等效果。
//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;
总结
本文我们简要的学习了一些专用的图层以及用他们实现的一些效果,我们只是理解了这些图层的少许特性。像CATiledLayer和CAEMitterLayer都是很强大,可以实现很多复杂效果的,可以扩展。我们初步地了解了这些专用图层的作用,希望在工作中能选择好合适的工具来更好的完成任务。
此文是一个读书笔记,内容来自网络:https://zsisme.gitbooks.io/ios-/content/chapter6.html
文中部分内容是笔者自己的理解修改,希望能看到这篇文章的同学不能照搬demo,一切知识的得来都要自己去验证。