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方法;

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容