美文网首页iOS UI开发iOS
iOS动画-CALayer寄宿图与绘制原理

iOS动画-CALayer寄宿图与绘制原理

作者: 梧雨北辰 | 来源:发表于2019-04-14 15:46 被阅读99次

    核心动画Core Animation,其实是由Layer Kit这样一个名字演变而来。它实际上是一个复合引擎,可以将存储在图层树体系中的不同独立图层,尽可能快地组合成不同的可视内容呈现于屏幕上;所以做动画只是Core Animation的特性之一;

    Core Animation直接作用于CALayer上,而图层树又是形成了UIKit以及我们在iOS应用程序所能在屏幕上看见一切的基础。因此,在讨论动画之前,我们有必要对于图层这一概念进行深入的理解。

    本篇主要内容:
    1.理解视图与图层
    2.CALyer寄宿图与contents属性
    3.UIView方法绘制自定义寄宿图
    4.CALyer方法绘制自定义寄宿图

    一、理解视图与图层

    UIView我们都非常熟悉, 但它其实是对于CALayer的一层封装,我们在创建UIView时,其内部会自动创建CALayer图层对象(即UIView的关联图层),UIView调用drawRect:方法进行绘图,并且将所有的内容绘制到自己的图层上,绘制完毕后,系统会将图层拷贝到屏幕上,于是就完成了UIView显示。

    视图的的职责就是创建并管理这个图层,以确保子视图在层级关系中添加或者被移除的时候,它们的关联图层也同样对应在层级关系树当中有相同的操作。我们在访问UIView的frame,bounds等属性又或者设置动画,其实也都是在操作其关联图层CALayer的特性。

    但是,UIView因为继承了UIResponder而具备响应事件的能力;而CALayer并不清楚具体的响应者链(iOS通过视图等级关系用来传送触摸事件的机制),于是它并不能响应事件,即使它也提供一些方法来判断是否一个触点在图层的范围之内。

    最后,总结UIView(视图)与CALayer(图层)的关系:UIView = CALayer(负责绘制显示内容的功能) + 处理用户交互的功能。

    1.图层与视图的底层关系

    下面的图示很好的展示了UIView与CALayer的底层上的区别:

    图层与视图的底层关系.png

    UIView、UIColor、UIImage都定义于UIKit框架中;
    CALayer定义在QuartzCore框架中的CoreAnimation中;
    CGImageRef、CGColorRef两种数据类型是定义在Core Graphics框架中;

    QuartzCore框架和CoreGraphics框架可以跨平台使用,在iOS和Mac OS上都能使用 ,但是UIKit却只能在iOS中使用;为了保证可移植性,QuartzCore是不能直接使用UIImage和UIColor的,如果使用需要将其转化为CGImageRef、CGColorRef

    2.使用图层

    使用图层十分简单,区别在于图层必须添加到图层上,具体代码如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        CALayer *colorLayer = [CALayer new];
        colorLayer.backgroundColor = [UIColor orangeColor].CGColor;
        colorLayer.frame = CGRectMake(30, 30, kDeviceWidth -60,  200);
        [self.view.layer addSublayer:colorLayer];
    }
    

    3.图层的能力

    苹果为我们提供了简洁方便的UIView的接口,而且为UIView增加了处理触摸事件的能力,但这种简单的设计也不可避免带来灵活上的缺陷,如果我们需要在底层做一些改变,或者使用一些没有在UIView上实现的接口功能,此时就需要我们介入Core Amimation底层了。
    下面是一些UIView没有暴露出来的CALayer的功能:

    • 设置阴影、圆角、带颜色边框
    • 3D变换
    • 非矩形范围
    • 透明遮罩
    • 多级非线性动画

    二、CALyer寄宿图与contents属性

    CALayer具有和UIView一样的层级关系树,可用于显示一个矩形块。但事实上它还通过contents属性包含并显示一张图片,称之为CALayer的寄宿图。CALayer的contents属性虽被定义为id,但是真正可以被赋值的类型是CGImageRef,指向的是一个CGImage结构的指针。

    在Mac OS系统上,contents属性对于CGIamge和NSImage类型的值都起作用;而对于iOS平台,虽然UIImage的CGImage属性也返回一个CGImageRef,但如果将这个值直接赋值给CALayer的contents,却会得到一个编译错误。这是因为CGImageRef并不是一个真正的Cocoa对象,而是一个Core Foundation类型;

    具体解决方法就是使用bridged关键字,下面是用于演示的代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        UIView *colorView = [UIView new];
        colorView.backgroundColor = [UIColor orangeColor];
        colorView.frame = CGRectMake(30, 30, kDeviceWidth -60,  200);
        [self.view addSubview:colorView];
        
        UIImage *headerImage = [UIImage imageNamed:@"header"];
        colorView.layer.contents = (__bridge id)headerImage.CGImage;
    }
    

    效果图如下:

    测试CALayer寄宿图1.png

    我们没有通过UIImageView的方法,而是直接利用CALaye显示了一张图片。这似乎很酷,但惊喜之余,我们也发现了仍然存在的小缺憾,那就是此时的图片显示效果是变形的;那它是否也可以像UIImageView一样具有可设置的方法呢,答案是肯定的,我们可以使用如下的代码,将图片自适应显示:

    colorView.layer.contentsGravity = kCAGravityResizeAspect;
    

    效果图如下:


    测试CALayer寄宿图2.png

    另外,类似的对于CALayer的显示设置和UIView具有下面的对应关系(这里仅简单总结概念和用处):

    CALayer与UIView属性对应关系.png

    三、UIView方法绘制自定义寄宿图

    给contents赋值CGImage的值并不是唯一设置寄宿图的方法,我们也可以直接使用Core Graphics直接绘制寄宿图,即通过继承UIView并实现-drawRect:的方式。

    -drawRect:方法是UIView没有默认实现的方法,因为寄宿图并不是必须的;但如果UIView检测到此方法被实现了,此方法会被自动调用,然后我们就可以在其中使用Core Graphics绘制自己需要的内容了;下面的代码就演示了drawRect自定义绘制寄宿图的具体操作,实现了一个环形的绘制:

    @implementation TestLayerVC
    - (void)viewDidLoad {
        //测试drawRect自定义绘制寄宿图
        CustomCircleView *customCircleView = [CustomCircleView new];
        customCircleView.frame = CGRectMake((kDeviceWidth - 100)/2, 250, 100 , 100);
        [self.view addSubview:customCircleView];
    }
    @end
    
    @implementation CustomCircleView
    - (instancetype)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if (self) {
            //使用drawRect,默认背景色为黑色;以下两种方式解决:
            // self.opaque = NO;
            self.backgroundColor = [UIColor purpleColor];
        }
        return self;
    }
    
    - (void)drawRect:(CGRect)rect{
        //获取画布
        CGContextRef context = UIGraphicsGetCurrentContext();
        //画笔颜色
        CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
        //画笔宽度
        CGFloat lineWidth = 5;
        CGContextSetLineWidth(context, lineWidth);
        //圆点坐标
        CGFloat centerX = CGRectGetWidth(rect)/2.0;
        CGFloat centerY = CGRectGetHeight(rect)/2.0;
        CGFloat cusRadius  = self.frame.size.width/2.0 - lineWidth/2.0;
        double  PI = 3.14159265358979323846;
    
        //绘制路径:初始角度、结束角度
        CGContextAddArc(context, centerX, centerY, cusRadius, 1.5*PI, 1.5*PI + 2*PI, NO);
        CGContextDrawPath(context, kCGPathStroke);
    }
    

    绘制效果如下:


    自定义绘制寄宿图1.png

    特别注意1:如果没有自定义绘制任务不需要寄宿图,就不要在子类中写一个空的-drawRect:方法,否则会造成CPU资源和内存的浪费;
    特别注意2:如果我们将绘制过程的角度参数改为动态,并结合定时器调用-setNeedsDisplay方法,就可以实现环形动画的效果(这里就不做具体演示了);

    四、CALyer方法绘制自定义寄宿图

    虽然-drawRect:方法是实现了自定义寄宿图绘制,但事实上还是底层的CALayer重绘并保存了因此产生的图片;CALayer有一个可选的delegate属性,实现了CALayerDelegate非正式协议,当CALayer需要一个内容特定信息时,就会从协议中请求;而当需要被绘制时,CALayer会通过如下的方法来请求代理给它提供寄宿图;

    //方法1:可以直接设置contents属性;
     - (void)displayLayer:(CALayer *)layer;
     
    //方法2:在不实现方法1时,CALayer就会转而尝试调用此的方法;
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
    

    在调用方法2之前,CALayer会创建了一个合适尺寸的空寄宿图(尺寸由bounds和contentScale决定)和一个Core Graphics的绘制上下文环境,为绘制寄宿图做准备,并将其以ctx参数传入。现在我们以方法2为例,演示CALayer绘制自定义寄宿图的过程,具体代码如下:

    @implementation TestLayerVC
    - (void)viewDidLoad {
        CALayer *blueLayer = [CALayer layer];
        blueLayer.frame =CGRectMake((kDeviceWidth - 100)/2, 400, 100 , 100);
        blueLayer.backgroundColor = [UIColor purpleColor].CGColor;
        blueLayer.delegate = self;
        
        blueLayer.contentsScale = [UIScreen mainScreen].scale;
        [self.view.layer addSublayer:blueLayer];
        
        [blueLayer display];
    }
    
    
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
        CGContextSetLineWidth(ctx, 10.f);
        CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
        CGContextStrokeEllipseInRect(ctx, layer.bounds);
    }
    
    @end
    

    效果图如下:


    自定义绘制寄宿图2.png

    代码分析:
    1. 主动绘制
    我们需要显式的调用-display方法;这不同于UIView,当图层显示到屏幕上时,CALayer不会自动重绘它的内容,CALayer把重绘的决定权交给了开发者;

    2.绘制特点
    尽管没有使用masksToBounds属性,但示例中绘制的视图依然被裁剪了,这是因为通过CALayer绘制寄宿图并没有对超出边界外的内容提供绘制支持;

    3.设置代理
    CALayerDelegate不能是UIView和UIViewController,如上述代码的演示就会造成崩溃;
    UIView本身携带的layer的代理就是自己,如果将一个layer的代理设置成它,那它本身的layer就会受到影响,通常表现为野指针崩溃;而UIViewController在经历Push和Pop之后也可能被释放,造成野指针崩溃;所以,对于这个问题的解决方案是:创建继承于NSObject的类,用于实现CALayerDelegate并管理CALayer的绘制逻辑;

    使用总结:当我们需要自定义寄宿图时,其实不必实现displayLayer:和-drawLayer: inContext:方法来绘制寄宿图。通常的做法还是实现UIView的-drawRect:方法,这样UIView就会自动帮我们做完剩下的工作,包括需要重绘的时候调用-display方法;

    ---End---
    相关文章:
    iOS动画-CALayer寄宿图与绘制原理
    iOS动画-CALayer布局属性详解
    iOS动画-CALayer隐式动画原理与特性
    iOS动画-CAAnimation使用详解

    相关文章

      网友评论

        本文标题:iOS动画-CALayer寄宿图与绘制原理

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