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

核心动画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方法;

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值