博客> Core Animation(上)───图层
Core Animation(上)───图层
2018-09-22 01:13 评论:0 阅读:1004 稻香里的守望者
ios animation Core CALayer 图层

原文地址(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的几个属性:

  • 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];
}

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不仅可以简单展示图片,还能通过一些属性改变视觉效果。

  • 圆角(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变换还有一个特点,变换后图层原本平行的两条线仍然会保持平行。

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有如下优点:

  • 渲染快速。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的底层实现。
收藏
1
sina weixin mail 回到顶部