一. 视图和图层的关系
Core Animation不仅仅是我们认识的“核心动画”,它是一个复合引擎,它的职责是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的图层,存储在图层树的体系中。
我们可能对视图的概念比较熟悉,一个视图就是在屏幕上显示的一个矩形块(UIView,UIButton等)。视图在层级关系中可以互相嵌套,一个视图可以管理它的所有子视图的位置。在iOS中,所有的视图都是继承UIView而来的,它可以处理触摸手势,支持基于Core Graphics绘图,可以做仿射变换(例如旋转、缩放等),简单的动画。
图层(CALayer)也是一个被层级关系树管理的矩形块,也可以包含图片、文字等内容,管理子视图的位置,它们也可以做动画和变换。和UIView最大的不同是,CALayer不处理用户的交互。
那CALayer和UIView有什么关系呢?
每一个UIView都有一个CALayer实例的图层属性,视图的职责就是创建并管理这个图层,以确保子视图在层级关系中添加或被移除时,他们关联的图层同样在层级关系树中有相同的操作。UIView本质就是对图层的封装,提供处理用户交互的功能,以及动画的高级接口。实际上,这些UIView背后关联的图层才是真正用来在屏幕上显示和做动画的。
一个视图只有一个相关联的图层(系统自动创建),同时支持添加其他子图层,当然这在开发中似乎没什么意义。如果不是一些特殊的情况(比如使用CALayer的子类,或对性能有高要求),我们都会直接使用图层关联的视图,因为你不仅能使用CALayer的底层特性,也可以使用UIView的高级API(自动排版,布局,事件处理)。
//创建一个宽高100的图层添加到self.view的图层中
- (void)createSimpleLayer{
CALayer *blueLayer = [[CALayer alloc] init];
blueLayer.frame = CGRectMake(50, 50, 100, 100);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.view.layer addSublayer:blueLayer];
}
图层不能像视图那样处理触摸事件,但是它的很多功能是视图不具备的:
① 阴影,圆角,边框的颜色和宽度
② 3D变换
③ 非矩形范围
④ 透明遮罩
⑤ 复杂动画
二. 图层的认识和应用
CALayer需要认识的属性很多,我们分成三部分来介绍:
① 寄宿图(图层内部)
② 显示效果(图层外表)
③ 变换(位置相关)
① CALayer的寄宿图
CALayer不仅可以有背景色,也可以包含一张图片,而这张图层中的图片就是CALayer的寄宿图。
这边我们先了解CALayer的几个属性:
contents
该类型定义为id是因为对CGImage和NSImage的值都起作用,事实上你要赋值的类型是CGImageRef,一个指向CGImage结构体的指针。contentsGravity
决定伸缩样式,这是和UIView中contentMode对应的属性,只是它是NSString类型(kCAGravity开头),不是枚举。contentsScale
定义寄宿图的像素尺寸和视图大小的比例,默认1.0。不过很多时候都被contentsGravity的样式限制了。masksToBounds
决定是否显示超出的内容,这是和UIView中clipsToBounds对应的属性。contentsRect
设置图层中显示寄宿图的子域,通俗讲就是要显示寄宿图的哪一部分,默认值是{0, 0, 1, 1},即全部显示,要显示左上部分则设为{0, 0, 0.5, 0.5}contentsCenter
定义一个固定边框和一个在图层上可拉伸的区域,默认值是{0, 0, 1, 1},即均匀拉伸,效果和UIImage 的resizableImageWithCapInsets:是一样的,还可以在可视化界面直接设置
以下通过例子来认识下寄宿图的各个属性:
- (void)createImageLayer{
CALayer *imageLayer = [[CALayer alloc] init];
imageLayer.frame = CGRectMake(50, 50, 200, 200);
imageLayer.backgroundColor = [UIColor blueColor].CGColor;
//设置寄宿图
UIImage *image = [UIImage imageNamed:@"person"];
imageLayer.contents = (__bridge id)image.CGImage;
//设置伸缩方式
imageLayer.contentsGravity = kCAGravityResizeAspectFill;
//为防止在Retina设备显示不正常,要这样设置contentsScale
imageLayer.contentsScale = [UIScreen mainScreen].scale;
//裁剪超出部分
imageLayer.masksToBounds = YES;
//显示图片的左上部分
imageLayer.contentsRect = CGRectMake(0, 0, 0.5, 0.5);
//设置图片拉伸区域
imageLayer.contentsCenter = CGRectMake(0.5, 0.5, 0.5, 0.5);
[self.view.layer addSublayer:imageLayer];
}
设置寄宿图除了给CALayer的contens赋值,也可以用UIView的drawRect:方法自定义绘制。
drawRect:方法没有默认的实现,因为寄宿图不是必须的,如果UIView检测到drawRect:方法被调用了,它会为视图分配一个像素尺度等于视图大小乘以contentsScale的寄宿图。如果不需要寄宿图,就不要创建这个方法(即使是空的),也会造成CPU资源和内存的浪费。(会创建一个图形上下文,该上下文内存公式:图层宽 x 图层高 x 4 字节)
drawRect:方法的本质也是底层的layer安排重绘的工作和保存因此产生的图片。获取的上下文就是底层layer的上下文,在渲染的时候,就是把图形渲染到对应的layer上。在执行渲染操作时,本质上它的内部相当于执行的 [self.layer drawInContext:ctx]。
#import "TestView.h"
@implementation TestView
- (void)drawRect:(CGRect)rect {
// 获取图形上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(ctx, 10.0f);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
//渲染时本质是调用[self.layer drawInContext:ctx]
CGContextStrokeEllipseInRect(ctx, self.bounds);
}
@end
当创建一个单独的CALayer时,也可以通过设置CALayer的delegate,实现 CALayerDelegate 协议来绘图。
当需要被重绘时,CALayer 请它的代理给它寄宿图来显示。首先会调用displayLayer:来直接设置contens,如果没实现该方法就会去调用drawLayer:inContext:进行绘图(实现了displayLayer:就不会实现drawLayer:inContext:)。它会为CALayer分配一个尺寸合适的空寄宿图(尺寸由bounds和contentsScale决定)和一个Core Graphics绘制的上下文环境,为绘制寄宿图做准备。
实际上,UIView中的layer的delegate是设置为它自己。当UIView需要重绘时,会调用drawLayer:inContext:方法。而UIView在drawLayer:inContext:方法中又会调用自己的drawRect:方法。在drawRect:中完成的所有绘图都会填入layer的CGContextRef中,然后被拷贝至屏幕。
#import "ViewController.h"
#import "TestView.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self drawContent];
}
- (void)drawContent{
CALayer *contentlayer = [[CALayer alloc] init];
contentlayer.frame = CGRectMake(50, 50, 100, 100);
contentlayer.backgroundColor = [UIColor blueColor].CGColor;
[self.view.layer addSublayer:contentlayer];
//设置代理(CALayerDelegate包含在NSObject中,所以不用显式写出来)
contentlayer.delegate = self;
//需要显式设置display(或setNeedDisplay)才会去调用CALayerDelegate代理方法
[contentlayer display];
}
#pragma mark - <CALayerDelegate>
//用来设置layer的contens
- (void)displayLayer:(CALayer *)layer{
UIImage *image = [UIImage imageNamed:@"person"];
layer.contents = (__bridge id)image.CGImage;
}
//用来绘图
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
CGContextSetLineWidth(ctx, 10.0f);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextStrokeEllipseInRect(ctx, layer.bounds);
}
@end
实际上,无论是CALayer还是UIView,创建寄宿图的本质都一样。只不过CALayer不会自动重绘,需要我们显式调用display,而且使用单独的CALayer还得实现CALayerDelegate。所以通常做法就是实现UIView的drawRect:,UIView会帮你做完剩下的工作。
② CALayer的显示效果
CALayer不仅可以简单展示图片,还能通过一些属性改变视觉效果。
圆角(conrnerRadius)
该属性控制图层角的曲率,这个曲率值只影响背景颜色,不影响背景图片或子图层,所以要把masksToBounds设为YES。图层边框(borderWidth&borderColor)
这两个属性控制图层边框,有点需要注意的是边框是在图层边界里面的,而且在所有内容包括子图层之前。阴影(shadowOpacity、shadowPath等)
shadowOpacity控制阴影的透明度,shadowColor控制阴影的颜色,shadowOffset控制阴影的方向和距离,shadowRadius控制阴影的模糊度。
和图层边框不同的是,阴影是根据内容的外形来决定的,所以会把寄宿图或子图层考虑在内。所以使用masksToBounds时要注意。shadowPath是控制阴影形状,根据内容计算阴影也是很耗费资源的,所以知道阴影形状直接指定会提高性能。
- (void)createSimpleLayer{
CALayer *blueLayer = [[CALayer alloc] init];
blueLayer.frame = CGRectMake(50, 50, 100, 100);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.view.layer addSublayer:blueLayer];
//设置阴影
blueLayer.shadowColor = [UIColor redColor].CGColor;//默认黑色
blueLayer.shadowOpacity = 0.5;//默认是0(完全透明,不显示),1是不透明
blueLayer.shadowOffset = CGSizeMake(0, 3);//默认是(0.-3)望y的负方向偏移
blueLayer.shadowRadius = 10;//默认0(边界清晰),值越来越模糊,所以要设为非0才自然
//自定义阴影形状
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, blueLayer.bounds);
blueLayer.shadowPath = path;
CGPathRelease(path);
- 图层蒙板(mask)
如果想展示的内容不是一个矩形或者圆角矩形,而是一个特殊形状的图形,就要用到图层蒙板mask。mask也是CALayer类型,它相当于一个子图层,不同于那种绘制在父图层的子图层,mask是定义父图层的可见区域。mask图层重要的是轮廓(边界),它就像做饼干的模子。 - 拉伸(minificationFilter&magnificationFilter)
当图片显示不同大小时,是有个拉伸过滤算法在起作用。CALayer提供了三种拉伸过滤方法。minificationFilter(缩小图片)和magnificationFilter(方法图片)默认值都是kCAFilterLinear。
kCAFilterLinear;//双线性滤波算法(放大倍数大容易模糊不清)
kCAFilterTrilinear;//三线性滤波算法(跟双线性差不多)
kCAFilterNearest;//最近过滤算法(适用于小图,或差异小,斜线少的大图)
- 透明(opacity)
CALayer的opacity(对应UIView的alpha)都是会影响子层级的。透明度的叠加原理是这样,假如一个50%透明度的图层,图层的每一个像素都会一般显示自己的颜色,一般显示图层下面的颜色。
单独图层还好,要是图层中有子图层就会出现问题(两个图层计算出来的透明度其实不一致)。所以,要想这个图层所在的图层树像一个整体一样的透明效果,就可以设置shouldRasterize属性为YES,在应用透明度之前,图层和子图层会被合成一个整体的图片。不过设置shouldRasterize也要注意rasterizationScale(默认1)去适配屏幕(设为[UIScreen mainScreen].scale),防止在Retina屏幕像素化。
③ CALayer的变换
在谈变换前,我们先来了解CALayer布局的属性。
UIView有三个重要的布局属性:frame、bounds、center,分别对应CALayer的frame、bounds、position。
为什么会有center和position呢?主要是因为CALayer有个anchorPoint(锚点),可以解释为把手(比如旋转时就是绕着该点旋转),默认值是(0.5,0.5)就在中心点。UIView没有anchorPoint可以设置,所以固定在中心点,CALayer通过设置可以在任何地方,包括图层外面(>1的情况)。
另外,与UIView的二维坐标不同,CALayer是三维坐标。所以有另外两个属性:zPosition和anchorPointZ。zPosition除了做仿射变换,最常见的就是改变图层显示顺序,通过提高zPosition(提高很小的值就可以,如0.0001)让该图层前置,不过一点需要注意的是不能改变事件传递的顺序。
说到事件传递,CALayer并不关心任何响应链事件,所以不能直接处理触摸事件和手势,但它有一系列方法帮你处理事件containsPoint:和hitTest:。
2D变换(仿射变换)
说起变换,我们最常用的就是UIView的transform属性,UIView的transform属性是一个CGAffineTransform类型,用于在二维空间旋转、缩放、平移。以下公式的意思是用图层的每一个CGPoint和CGAffineTransform矩阵的每一行对应元素相乘再相加,就形成一个新的位置。2D变换还有一个特点,变换后图层原本平行的两条线仍然会保持平行。
这边Core Graphics提供了一系列函数,让我们实现简单的仿射变换。
/* 单位矩阵(不变换)*/
CGAffineTransformIdentity;
/* 简单变换 */
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy);//缩放
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty);//平移
CGAffineTransformMakeRotation(CGFloat angle);//旋转
/* 混合变换 */
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy);
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty);
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle);
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);
3D变换
CALayer也有个transform属性,它的类型是CATransform3D。CATransform3D和CGAffineTransform类似,也是一个矩阵,不过CATransform3D可以在三维空间做变换。
有一点需要注意的是CGAffineTransform是属于Core Graphics框架的,而Core Graphics是2D绘图的API,所以CGAffineTransform仅对2D变换有效。而CATransform3D是属于Core Animation框架,和Core Graphics提供的函数类似,Core Animation也提供了一系列函数来做3D变换,只是多了了坐标轴的参数。
/* 单位矩阵(不变换)*/
CATransform3DIdentity;
/* 简单变换 */
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz);//缩放
CATransform3DMakeTranslation(CGFloat tx, CGFloat ty, CGFloat tz);//平移
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz);//旋转
/* 混合变换 */
CATransform3DScale(CATransform3D t, CGFloat sx, CGFloat sy, CGFloat sz);
CATransform3DTranslate(CATransform3D t, CGFloat tx, CGFloat ty, CGFloat tz);
CATransform3DRotate(CATransform3D t, CGFloat angle, CGFloat x, CGFloat y, CGFloat z);
CATransform3DConcat(CATransform3D a, CATransform3D b);
在一些变换时(比如旋转),要先有透视效果(近大远小),需要设置CATransform3D矩阵中的m34,m34用于用于按比例缩放X和Y的值来计算到底离视角多远。m34默认值是0,我们可以设置m34为-1.0/d来应用透视效果,d通常取500-1000。当在透视效果绘图时,物体会越远越小,当远离到某个极限距离,就会缩成一点,该点就是灭点。为了模拟现实效果,Core Animation定义了该点位于变换图层的anchorPoint。所以当改变了图层的position,你也改变了它的灭点,做3D变换时这一点要特别注意。
当在有多个视图或图层,如果要做3D变换,就需要用到CALayer的sublayerTransform,它也是CATransform3D类型,但和对一个图层的变换不同,它影响到所有的图层。
- (void)createTransform3D{
CALayer *blueLayer = [[CALayer alloc] init];
blueLayer.frame = CGRectMake(50, 200, 100, 100);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;
blueLayer.transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
[self.view.layer addSublayer:blueLayer];
CALayer *redLayer = [[CALayer alloc] init];
redLayer.frame = CGRectMake(200, 200, 100, 100);
redLayer.backgroundColor = [UIColor redColor].CGColor;
redLayer.transform = CATransform3DMakeRotation(- M_PI_4, 0, 1, 0);
[self.view.layer addSublayer:redLayer];
//设置统一的灭点
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0/500;
self.view.layer.sublayerTransform = transform;
}
假如旋转到可以看到背面,我们可以看到背面是一个对称的图形。CALayer有个doubleSided的属性来控制图层背面是否要被绘制,默认值是YES。为了防止浪费资源,应该设置为NO,这样图层正面将要从相机视角消失时,即背面要出来时,背面才不会被绘制。
还有一个比较重要的知识点,每个图层的3D场景其实是扁平化的,即3D场仅仅是绘制在图层表面,而不是你所想象的那个3D。
三. 专用图层(CALayer的子类)
CAShapeLayer
绘制图形,我们可以用Core Graphics在原始的CALayer上绘制,也可以用CAShapeLayer。CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类。指定线宽,颜色等属性,用CGPath定义要绘制的图形,CAShapeLayer就会自动渲染出来。所以对比Core Graphics绘制,CAShapeLayer有如下优点:
- 渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比Core Graphics快很多。
- 高效使用内存。一个CAShapeLayer不需要像普通的CALayer一样创建寄存图,所以无论多大,都不会占用太多内存。
- 不会被图层边界裁剪。CAShapeLayer可以再边界外绘制,而使用Core Graphics在普通CALayer会被裁剪。
- 不会出现像素化。(因为没有寄存图)
- (void)createCAShapeLayer{
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.strokeColor = [UIColor redColor].CGColor;//描线颜色
shapeLayer.fillColor = [UIColor blueColor].CGColor;//填充颜色
shapeLayer.lineWidth = 10;//线宽
shapeLayer.lineCap = kCALineCapRound;//线条结尾样式
shapeLayer.lineJoin = kCALineJoinRound;//线条结合处样式
//CAShapeLayer还有个好处,就是通过设置path来设置单个倒角
UIRectCorner *corner = UIRectCornerTopLeft|UIRectCornerTopRight;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(100, 100, 160, 140) byRoundingCorners:*corner cornerRadii:CGSizeMake(30, 30)];
shapeLayer.path = path.CGPath;//路径
[self.view.layer addSublayer:shapeLayer];
}
CAGradientLayer
CAGradientLayer是用来生成两种或多种颜色平滑渐变的。它的优点是绘制时使用了硬件加速。
CATextLayer
如果想在一个图层显示文字,可以像UILabel一样借助图层代理将字符串使用Core Graphics写入图层的内容。想越过代理,直接在图层上操作是十分复杂,除了使用CATextLayer。CATextLayer以图层的形式包含了UILabel几乎所有的绘制特性,并且有一些额外新特性。
CATransformLayer
CATransformLayer不同于普通的CALayer,因为它不能显示自己的内容。它是作为一个容器,把子图层包装成一个整体变换(比如正方体的旋转)。
CAReplicatorLayer
CAReplicatorLayer的目的是为了高效生成多个相似的图层。还有一个常见的用途就是复制出一个负比例的图层来实现反射效果。(可以自定义一个View,通过layerClass方法将普通的CALayer换成CAReplicatorLayer,然后进行设置。)
CAScrollLayer
CAScrollLayer可以实现像UIScrollView一样的功能,可以理解为图层中的ScrollView。
CATiledLayer
CATiledLayer为载入大图出现的性能问题提供一个解决方案,将大图分解的小图按需单独载入。
CAEmitterLayer
CAEmitterLayer是一个高性能的粒子引擎,被用来创建实时粒子动画(比如烟雾,火,雨)。
CAEAGLLayer
CAEAGLLayer用来显示任意的OpenGL图形。
AVPlayerLayer
AVPlayerLayer不属于Core Animation框架,是属于AVFountation框架,是用来播放视频的,是高级接口如MPMoivePlayer的底层实现。