图像Core Animation

【iOS小结】Core Animation(上)─── 图层

2017-11-30  本文已影响132人  WellsCai

一. 视图和图层的关系

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的几个属性:

以下通过例子来认识下寄宿图的各个属性:

- (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];
}
contensCenter的例子.png
设置寄宿图除了给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不仅可以简单展示图片,还能通过一些属性改变视觉效果。

- (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);
kCAFilterLinear;//双线性滤波算法(放大倍数大容易模糊不清)
kCAFilterTrilinear;//三线性滤波算法(跟双线性差不多)
kCAFilterNearest;//最近过滤算法(适用于小图,或差异小,斜线少的大图)
③ 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变换还有一个特点,变换后图层原本平行的两条线仍然会保持平行。

CGAffineTransform公式.png

这边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可以在三维空间做变换。

CATransform3D公式.png

有一点需要注意的是CGAffineTransform是属于Core Graphics框架的,而Core Graphics是2D绘图的API,所以CGAffineTransform仅对2D变换有效。而CATransform3D是属于Core Animation框架,和Core Graphics提供的函数类似,Core Animation也提供了一系列函数来做3D变换,只是多了了坐标轴的参数。

坐标轴和旋转方向.png
/*  单位矩阵(不变换)*/
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有如下优点:

- (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的底层实现。

上一篇下一篇

猜你喜欢

热点阅读