图层

作者: 一个栗 | 来源:发表于2021-01-01 21:24 被阅读0次

    1. UIView和CALayer的区别

    1. CALayer无法响应用户事件
      NSObject -> CALayer
      NSObject -> UIResponder -> UIView -> UIWindow
    2. 分工不同
      UIView侧重于对显示内容的管理和整体布局
      CALayer侧重于显示内容的绘制、显示和动画。
    3. 所属框架不同
      UIView属于UIKit框架,UIKit框架主要用来构建用户界面的。
      CALayer属于QuartzCore框架,而且CALayer是作为一个低级的,可以承载绘制内容的底层对象出现在该框架的。

    2.什么是Layer层对象

    用来显示绘制内容的一种数据对象,常见的几个自身具有绘制功能的专用Layer有:CATextLayer、CAShapeLayer、CAGradientLayer

    2.1 CATextLayer

    用来实现更加灵活的文字布局和渲染的,几乎包含了UILabel的所有特性并在此基础上增加了更强大的功能,包括字体、尺寸、颜色和下划线等文字效果,同时CATextLayer的渲染效果明显高于UILabel。
    通过CATextLayer实现一个UILabel的示例代码如下:

        // 创建一个字符承载视图
        UIView *textView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 280, 50)];
        CATextLayer *textLayer = [CATextLayer layer];
        textLayer.frame = textView.bounds;
        textLayer.string = @"CATextLayer";
        // 文字前景色和背景色
        textLayer.foregroundColor = [UIColor whiteColor].CGColor;
        textLayer.backgroundColor = [UIColor grayColor].CGColor;
        // 文字超出视图边界裁剪
        textLayer.wrapped = YES;
        textLayer.font = (__bridge CFTypeRef)[UIFont systemFontOfSize:18].fontName;
        textLayer.alignmentMode = kCAAlignmentCenter;
        // 适应屏幕retain分辨率,防止像素化导致模糊
        textLayer.contentsScale = [[UIScreen mainScreen] scale];
        [textView.layer addSublayer:textLayer];
        [self.view addSubview:textView];
    

    显示效果如下:


    截屏2021-01-0120.55.29.png

    2.2 CAShapeLayer

    专门用来绘制矢量图形的图形子类,例如可以指定线宽和颜色等利用CGPath绘制图形路径,可以实现图形的3D变换效果,渲染效率比Core Graphics高得多,而且可以在超出视图边界之外绘制,即不会被边界裁减掉。
    圆形的绘制代码如下:

        // 创建圆形路径
        UIBezierPath *path = [[UIBezierPath alloc] init];
        // 起点要在圆心水平右侧半径长度处
        [path moveToPoint:CGPointMake(200, 100)];
        // 添加圆弧路径
        [path addArcWithCenter:CGPointMake(150, 100) radius:50 startAngle:0 endAngle:2 * M_PI clockwise:YES];
        
        // 创建图形层
        CAShapeLayer *shapeLayer = [CAShapeLayer layer];
        // 路径线的颜色
        shapeLayer.strokeColor = [UIColor cyanColor].CGColor;
        // 闭合图形填充色,此处设置透明
        shapeLayer.fillColor = [UIColor clearColor].CGColor;
        // 线宽
        shapeLayer.lineWidth = 10;
        // 线的样式,端点、叫点
        shapeLayer.lineCap = kCALineCapRound;
        shapeLayer.lineJoin = kCALineJoinRound;
        // 设置图形路径
        shapeLayer.path = path.CGPath;
        [self.view.layer addSublayer:shapeLayer];
    

    效果如下:


    截屏2021-01-0121.09.58.png

    2.3 CAGradientLayer

    是一个硬件加速的高性能绘制图层,主要用来实现多种颜色的平滑渐变效果。以下是一个3种颜色从正方形左上角到右下角的渐变效果示例代码:

        // 创建leyer承载视图
        UIView *containView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
        CAGradientLayer *gradientLayer = [CAGradientLayer layer];
        gradientLayer.frame = containView.bounds;
        // 依次设置渐变颜色数组
        gradientLayer.colors = @[(__bridge id)[UIColor greenColor].CGColor,(__bridge id)[UIColor yellowColor].CGColor,(__bridge id)[UIColor orangeColor].CGColor];
        // 颜色从起点到终点按比例分段位置
        gradientLayer.locations = @[@0.0, @0.3, @0.5];
        // 颜色渐变的起点和终点,(0, 0)~(1, 1)表示左上角到右下角
        gradientLayer.startPoint = CGPointMake(0, 0);
        gradientLayer.endPoint = CGPointMake(1, 1);
        [containView.layer addSublayer:gradientLayer];
        [self.view addSubview:containView];
    

    效果如下:


    截屏2021-01-0121.22.44.png

    3.如何使用CAShapeLayer绘制图层

    CAShapeLayer是一个通过矢量图形来进行绘制的图层子类。开发者通过指定诸如颜色和线宽等属性,用CGPath来定义指定形状的图形,最后CAShapeLayer对象就会自动渲染出来了。与直接使用Core Graphics在原始的CAL ayer对象中绘制的方式相比,使用CAShapeLayer有以下优点:
    1.渲染速度更快。CAShapeLayer使用了硬件加速的方式绘制图形,绘制速度比Core Graphics快很多。
    2.内存使用更高效。普通的CALayer对象需要创建一个寄宿图,而CAShapeLayer不需要,这样就节约了内存。
    3.不会被边界涂层裁减掉。一个CAShapeLayer可以在图层边界绘制。而CAShapeLayer的图层路径不会像在使用Core Graphics的普通CALayer一样被裁减掉。
    通常通过指定CAShapeLayer对象的path属性来绘制图层。示例代码如下:

    @property (nullable) CGPathRef path;
    

    此外还可以通过一些属性来描绘图形的线条,但在图层层面只有一次机会设置这些属性,如果想用不同的颜色或者风格来绘制多个形状,那么就不得不为每一个形状准备一个图层。下面示例用一个CAShapeLayer渲染一个简单的多边形,由于CAShapeLayer对象的path属性是CGPathRef类型,所以很容易使用UIBezierPath类帮助创建图层路径,这样就不需要人工释放CGPath了。
    代码如下:

        CAShapeLayer *shapeLayer = [CAShapeLayer layer];
        // 设置线宽
        shapeLayer.lineWidth = 3;
        shapeLayer.lineCap = kCALineJoinRound;
        shapeLayer.lineJoin = kCALineJoinRound;
        // 描边颜色
        shapeLayer.strokeColor = [UIColor redColor].CGColor;
        // 填充颜色
        shapeLayer.fillColor = [UIColor whiteColor].CGColor;
        // 创建UIBezierPath对象
        UIBezierPath *bezierPath = [UIBezierPath bezierPath];
        [bezierPath moveToPoint:CGPointMake(160, 100)];
        [bezierPath addLineToPoint:CGPointMake(100, 160)];
        [bezierPath addLineToPoint:CGPointMake(100, 220)];
        [bezierPath addLineToPoint:CGPointMake(160, 280)];
        [bezierPath addLineToPoint:CGPointMake(220, 220)];
        [bezierPath addLineToPoint:CGPointMake(220, 160)];
        [bezierPath closePath];
        shapeLayer.path = bezierPath.CGPath;
        [self.view.layer addSublayer:shapeLayer];
    

    效果如下:


    截屏2021-01-0419.53.08.png

    4.iOS中如何为UIImageView添加圆角

    按照渲染方式,实现UIView及其子类的圆角效果有2种方法:一种是直接设置layer圆角属性,为离屏渲染,另一种是自定义圆角绘制方法,实现当前屏幕渲染。
    当前屏幕渲染:指GPU直接在当前屏幕缓冲区进行图形渲染,不需要提前另开缓冲区也就不需要缓冲区的切换,因此性能高。
    离屏渲染:提前另开一个缓冲区进行图形渲染,由于需要和当前屏幕缓冲区进行切换,所以很耗性能。通常圆角、遮罩、不透明度、阴影、渐变、光栅化和抗锯齿等设置都会触发离屏渲染。
    1.离屏渲染实现圆角

    view.layer.masksToBounds = YES;
    view.layer.cornerRadius = 5;
    

    2.当前屏幕渲染实现圆角
    直接在当前屏幕绘制,提高性能。
    为UIImage类扩展一个实例方法:

    - (UIImage *)imageWithCornerRadius:(CGFloat)radius ofSize:(CGSize)size {
        // 当前UIImage可见绘制区域
        CGRect rect = (CGRect){0.f,0.f,size};
        // 创建基于位图的上下文
        UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
        // 在当前位图上下文添加圆角绘制路径
        CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
        // 当前绘制路径和原绘制路径相交得到最终裁剪绘制路径
        CGContextClip(UIGraphicsGetCurrentContext());
        // 绘制
        [view drawRect:rect];
        // 取得裁剪后的image
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        // 关闭当前位图上下文
        UIGraphicsEndImageContext();
        return image;
    }
    

    5.contentsScale属性有什么作用

    图层的contentsScale属性属于支持高分辨率屏幕(如Retina屏幕)机制的一部分,它定义了图层content中图像的像素尺寸与视图大小的比例,它也被用来判断在绘制图层时允许为content属性创建的空间大小,以及需要显示的图片的拉伸度。
    默认情况下,contentsScale值为1.0,也就是说图层的绘制系统将会以每一个点对应一个像素来绘制图片,如果将其设置为2.0,那么会以每个点对应2个像素绘制图片,即所谓的Retina屏幕。
    在开发中,可设置contentsScale为合适的值,如下:

    layer.contentsScale = [UIScreen mainScreen].scale;
    

    问:像素和点有什么关系
    1.点是iOS中标准的坐标体系,他是IOS中的虚拟像素,也就是逻辑像素。标准设备中1个点就是1个像素,Retina中1个点是2个像素。iOS中用点作为屏幕的坐标测算就是为了在Retina设备和普通设备上有一致的效果。
    2.像素是屏幕分辨率的尺寸单位,物理像素坐标并不适用于屏幕布局,但是仍然和图片有相对关系。UIImage是一个屏幕分辨率解决办法,它是用来衡量点大小的,但是,一些底层的CGImage类型的图片会使用像素,所以必须清楚在Retina设备和普通设备上,点和像素代表不同的大小。

    6.如何理解anchorPoint和position作用

    在UIView中有3个重要的属性:frame、bounds、center,分别对应CALayer中的frame、bounds、position。虽然图层使用了position而视图使用了center,但这2个属性代表了同样的值。也就是说,当前图层的anchorPoint相当于父图层的位置。
    anchorPoint被称为“锚点”,图层的anchorPoint属性通过影响position的值来控制它的frame,可以将anchorPoint比作控制图层移动的“支点”。anchorPoint是使用单位坐标来描述的,也就是图层的相对坐标。左上角是{0,0},右下角是{1,1}。anchorPoint默认的坐标是{0.5,0.5},即图层的中心点。
    position等价于视图中的center,代表了anchorPoint点在父图层(superLayer)的位置,因此可以说,position点是相对于父图层坐标系的,而anchorPoint是相对于当前图层的,2者是相对于不同坐标系的一个重合点。
    事实上,position和anchorPoint是相互不影响的,修改其中任何一个值,另一个不会改变,改变的只是当前图层的frame。对于视图或者图层来说,frame并不是非常清晰的一个属性,当其中任何一个值发生改变,frame都会变化,相反,改变frame的值,同样也会影响他们的值。
    frame、position和anchorPoint关系如下:

    frame.origin.x = position.x -  anchorPoint.x * bounds.size.width;
    frame.origin.y = position.y -  anchorPoint.y * bounds.size.height;
    

    在制作动画时,视图默认是围绕中心点进行旋转或者平移的,这是因为默认UIView的rootLayer的anchorPoint就是图层的中心。在实际开发中,可以改变anchorPoint以达到其他的效果。

    7.如何理解drawRect方法

    iOS的绘图操作是发生在UIView的drawRect方法中的。如果想在UIView中绘图,那么可以通过继承UIView类并实现drawRect方法,在drawRect方法中获取当前环境的上下文进行绘制。drawRect方法被定义在UIView的UIViewRendering类别中:

    - (void)drawRect:(CGRect)rect;
    

    drawRect:里面的代码利用Core Graphics在指定的rect中绘制图形,然后内容就会被缓存起来直到它需要被更新。事实上,苹果公司不建议开发者主动调用drawRect,当然如果直接强制调用,也是没有效果的,因为系统此时不会自动创建View相关联的上下文。而且当没有自定义绘制任务时,就不要在子类中写一个空的drawRect,否则会造成CPU资源和内存的浪费。
    当视图在屏幕中出现的时候,drawRect会被自动调用。具体是,drawRect方法的第一次调用是在控制器中loadView和viewDidLoad两方法之后。所以不必担心控制器初始化之前,drawRect会被执行。虽然drawRect是UIView中的方法,但事实上都是底层的CALayer对象安排了重绘工作并保存了绘制好的内容。
    另外,可以调用setNeedsDisplay方法将绘制好的图形更新到视图上。setNeedsDisplay就是在receiver上设置一个需要被重绘的标记,在下一个绘制周期自动进行重绘,一般iPhone的刷新频率是60HZ,也就是说1/60秒后重绘。示例代码如下:

    - (void)drawRect:(CGRect)rect {
       // 获取上下文
        CGContextRef ctx = UIGraphicsGetCurrentContext();
        // 绘制一个圆形
        CGContextAddEllipseInRect(ctx, CGRectMake(0, 0, 200, 200));
        // 设置颜色
        [[UIColor redColor] set];
        // 填充
        CGContextFillPath(ctx);
    }
    

    8.如何使用mask属性实现图层蒙版功能

    在实际开发中,通常使用mask属性来实现图层蒙版的功能,这个属性本身就是CALayer类型,和其他图层一样有绘制和布局的属性,不同于那些绘制在父图层中的子图层,mask定义了父图层的部分可见区域。
    mask图层中最重要的是图层的轮廓。在父图层中,与mask图层相重叠的部分会被保留,其他部分将会被遮盖,也就是说,父图层提供内容,mask图层提供形状。mask不仅仅局限于静态图,还可以用代码或者动画实时生成。

    CGSize size = [UIScreen mainScreen].bounds.size;
    // 设置寄宿图
    self.view.layer.contents = (id)[UIImage imageNamed:@"海水"].CGImage;
    // 设置寄宿图的显示方式,等价于UIView的contentMode
    self.view.layer.contentsGravity = @"resizeAspect";
    // 设置mask层
    UIImageView *searchImageView = [[UIImageView alloc] initWithFrame:CGRectMake((size.width - 150) * 0.5, (size.height - 150) * 0.5, 150, 150)];
    searchImageView.image = [UIImage imageNamed:@"马"];
    self.view.layer.mask = searchImageView.layer;
    

    9.如何解决maskToBounds离屏渲染带来的性能损耗

    在开发中常用CALayer的cornerRadius属性来设置图层的圆角曲率,默认情况下,cornerRadius只影响背景颜色而不影响背景图片或者子图层。但是将CALayer的masksToBounds设置为YES时,图层内容就会被截取:

    imageView.layer.masksToBounds = YES;
    imageView.layer.cornerRadius = 5;
    

    这样的渲染机制是GPU在当前屏幕缓冲区外新开辟一个缓冲区进行工作,也就是所谓的离屏渲染,所以会有额外的性能损耗。如果在某一时刻大量使用这种方式设置圆角,那么就会触发缓冲区频繁合并和上下文之间频繁切换,应用程序就可能出现掉帧和卡顿。具体可以用Instruments检测。
    为了防止离屏渲染,可以不使用上面方法,而是将图片处理的权利交给CPU,虽然CPU对图形的处理能力不如GPU,但是设置圆角的难度不大,且代价远小于上下文切换,以下是常用的2种方法:
    1.使用CALayer的shouldRasterize属性
    shouldRasterize属性是设置光栅化,可以使离屏渲染的结果缓存到内存中存为位图,当下次使用的时候可以直接使用内存缓存,这样就节省了一直离屏渲染的性能损耗。为了使用shouldRasterize属性,还需要设置rasterizationScale属性去适配屏幕,以防止Retina屏幕像素化的问题。

    imageView.layer.shouldRasterize = YES;
    imageView.layer.rasterizationScale = [UIScreen mainScreen].scale;
    

    这种方法虽然在一定程度上优化性能,但是如果layer及sublayers经常改变,那么就会不停地渲染及设置缓存,这种情况下也是很耗性能的。
    2.使用Core Graphics绘制出圆角图片
    可以使用UIKit中对Core Graphics有一定封装的应用层类UIBezierPath,对图片进行重新剪切。代码如下:

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    imageView.image = [UIImage imageNamed:@"1"];
    // 开启上下文,开始对imageView画图
    UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
    // 使用贝塞尔曲线画出一个圆形图
    [[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
    [imageView drawRect:imageView.bounds];
    imageView.image = UIGraphicsGetImageFromCurrentImageContext();
    // 关闭上下文,结束画图
    UIGraphicsEndImageContext();
    [self.view addSubview:imageView];
    

    10. QuartzCore和Core Graphics有什么区别

    Core Graphics是iOS系统中的底层绘图框架,平时使用最频繁的point、size、rect等视图属性都被定义在这个框架中,包含的API都是以CG开头,提供的都是C语言的函数接口。
    QuartzCore框架从其头文件可以发现,其实就是CoreAnimation。也就是说,QuartzCore专指CoreAnimation用到的动画相关的库、API和类。
    以下是QuartzCore头文件:

    #ifndef QUARTZCORE_H
    #define QUARTZCORE_H
    #include <QuartzCore/CoreAnimation.h>
    #endif /* QUARTZCORE_H */
    

    Core Graphics和QuartzCore都是跨iOS和Mac OS平台的,这点区别于UIKit(只适用于iOS平台)。
    QuartzCore大量使用了Core Graphics中的类,因为动画的产生必然要用到图形库中的东西。

    相关文章

      网友评论

          本文标题:图层

          本文链接:https://www.haomeiwen.com/subject/iuiroktx.html