iOS核心动画1——认识图层CALayer

前言

通常我们一提到iOS中的动画框架,就会想起Core Animation来,这
本没错,但该框架却不止能做做动画而已......最近几天在看《iOS Core Animation Advanced Techinques》这本书,想对Core Animation做一个系统的学习,大概会将其整理成三四篇读书笔记。本篇是第一篇,先认识图层CALayer。


认识图层CALayer

图层CALayer和视图UIView非常类似,都是可以显示内容的矩形块,并且也有继承体系,均可设置一些属性值来改变其性质、状态等。与UIView不同的是CALayer没有“响应链机制”,不能响应事件。它仅能判断某点是否在该图层内。我们知道,每个UIView默认对应一个CALayer,并且是作为视图的一个属性layer的。事实上与视图关联的图层才是真正用来在屏幕上显示和做动画的,可以这么理解:UIView是对CALayer的封装,使其能显示并表现文本图像等内容,并且提供了一些一些高级接口,使其更方便地操作图层或其他底层的东西。
虽然视图是对图层的封装,使其接口更简单优雅,但是这也不可避免的带来一些灵活上的缺陷。因此,有些事是图层可为,但视图不可为的,比如圆角、阴影、边框、3D变换、非矩形返回、透明遮罩、复杂动画等。此时,我们就不得不使用图层了。但是,需要说明的是,能用视图实现的我们还是用视图比较好一点,虽然图层更轻量,但是它也有明显的不足,比如不能响应事件,对自动布局不友好等。


几个比较重要的属性:

contents属性:

意为“内容”,在此指图层的“寄宿图”。可为图层添加图片,使图层像UIImageView那样显示图片。
该属性被定义为id类型,似乎意为任何类型对象均可。然而事实上,若为其赋的值不是CGImage型,你可以编译通过,但是图层显示出来却是空白的。也就是说该属性其实必需赋值为CGImage型。该属性类型之所以被定义为id类型,究其原因其实是Mac OS的历史原因:在Mac OS上对CGImageUIImage均起作用。

myLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"ym01.jpg"].CGImage);
contentsGravity属性:

contentsGravity和UIView中的contentMode比较类似,都是表示内容的位置或者对齐方式。与contentMode类型为枚举不同的是该属性值为NSString类型的,可选常量值有多种。
这篇博文对此属性不同常量值的含义解释的比较清楚:CoreAnimation编程指南(五)图层内容
为了弄清楚几个属性值的具体含义,我自己也在代码里试了试,结果如下图:

屏幕快照 2016-08-24 22.54.32.png

myLayer.contentsGravity = kCAGravityResizeAspectFill;
contentsScale属性:

contentsScale属性值为浮点类型,代表了寄宿图的像素尺寸和视图大小的比例,默认情况下为1.0,表示正常屏幕。若值为2.0,则代表会以每个点2个像素绘制图片,即我们熟知的Retina屏幕。并不是每次设置contentsScale值都会生效,若我们已设置contentsGravity属性,即为已依靠合适的方式拉伸或伸缩,再设置contentsScale属性值将是无效的。

myLayer.contentsScale = [UIScreen mainScreen].scale;
contentsRect属性:

contentsRect允许我们在图层边框里显示寄宿图的一个子域,使其显示原本的部分。需要注意的是它并不是以像素或点来表示的,它是相对值,用0~1来表示寄宿图的两端。比如,下面代码我们使其只显示寄宿图的左上角部分:

myLayer.contentsRect = CGRectMake(0, 0, 0.5, 0.5);
屏幕快照 2016-08-24 23.23.02.png
contentsRect属性:

contentsRect属性意为“全面拉伸”,指在寄宿图中圈定出一块区域,图片拉伸时这块区域会纵向横向全面的拉伸,而外围边框处的只做单向的拉伸或不拉伸。
关于该属性,这篇文章解释得很清楚:关于CALayer的contentsCenter属性

** 自定义绘制:** 给图层的contentsCGImage的值不是唯一的设置寄宿图的方法,我们还可以直接用Core Graphics直接绘制寄宿图。
CALayer可实现CALayerDelegate协议,将其代理设为图层所属的视图view,并且实现代理方法displayLayer:或者drawLayer:inContext:,在代理方法里自己通过Core Graphics绘制。需要注意的是,CALayer不会自动重绘其内容,需要显示地调用display方法。也因此,更常见的做法是我们可以重写UIViewdrawRect:方法,在该方法内进行绘制,UIView会在需要重绘的时候帮你绘制。比如下面代码,我们定义了一个CustomView继承自UIView,然后重写其drawRect:方法完成绘制。

- (void)drawRect:(CGRect)rect
{
    UIImage *img = [UIImage imageNamed:@"kungfu.jpg"];
    [img drawInRect:CGRectMake(0, 0, rect.size.width, rect.size.height)];
}
anchorPoint属性:

anchorPoint属性,意为“锚点”,默认锚点是点(0.5,0.5)。即为图层的中心点。可以将其理解为移动、旋转图层的把柄。若锚点为图层的中心,则旋转图层时以中心点为中心旋转;若锚点为(0,0),则旋转时则以图层的左上角为中心旋转。比如,我们要做一个钟表的效果,那表针旋转时锚点则不能为中心点,因为表针不是以表针这个图层的中心为中心旋转的,它是以几乎接近一端的位置为中心旋转的,它的锚点大概是(0.5,0.9)。

Clock.png
zPosition属性:

zPositionanchorPointZ属性都是表示三维空间布局的属性。zPosition可以理解为图层在z坐标轴的位置。该属性一般用于图层在三维空间的形变(CATransform3D)。
除此外,最常用的用途是改变图层的显示顺序。图层的显示顺序默认是从下往上的,后添加的覆盖在原来的视图之上。就像画家在画板上画画,后画的内容总会覆盖之前画的东西。此时,你可以将被覆盖的图层的zPosition属性稍微调大点,便可以将其调整到上面来了。(该属性默认是0,只需调大一两个像素就行,但是浮点型四舍五入的计算有时会造成不便的麻烦)。

关于坐标系转换:

和视图一样,图层坐标位置是以其父类为参照的。若父图层的位置变了,那子图层的位置也会相应的跟着变动。但是,有时候我们可能需要知道一个图层的绝对位置,或者相对于另一个指定图层的位置,而不是默认的,相对于其父图层的位置。

- (CGPoint)convertPoint:(CGPoint)p fromLayer:(nullable CALayer *)l;
- (CGPoint)convertPoint:(CGPoint)p toLayer:(nullable CALayer *)l;
- (CGRect)convertRect:(CGRect)r fromLayer:(nullable CALayer *)l;
- (CGRect)convertRect:(CGRect)r toLayer:(nullable CALayer *)l;
Hit Testing:
- (BOOL)containsPoint:(CGPoint)p;  // 判断某点是否在该图层内
- (CALayer *)hitTest:(CGPoint)p;

图层不能响应触摸事件或者手势,但是以上两个CALayer的方法可以供你判断,你所触摸的点是否在图层之内。hitTest:方法传入一个point返回被触摸的图层。返回的layer有可能是图层本身,也有可能是某个subLayer

CALayer的自动布局:

UIViewUIViewAutoresizingMaskNSLayoutConstraint来实现自动布局。但是CALayer并不支持自动布局。若遇到需要调整子图层重新布局问题,则需要手动在CALayer的代理方法里手动调整其布局。该代理方法被触发的时机是layerbounds改变,或者调用了setNeedsLayout方法。

- (void)layoutSublayersOfLayer:(CALayer *)layer;

因为CALayer对自动布局不友好,且无法处理触摸事件,所以能用UIView处理的需求,还是用UIView处理比较好。

圆角:
    myLayer.cornerRadius = 20.f;
    myLayer.masksToBounds = YES;
屏幕快照 2016-08-25 00.42.47.png
边框:
    myLayer.borderColor = [UIColor lightGrayColor].CGColor;
    myLayer.borderWidth = 5.f;
屏幕快照 2016-08-25 00.46.04.png

需要注意的是图层的边框是依附在图层的外围的,而不是图层内容的外围。如下这种情况,图层的内容超出了图层界限,但是边框还是显示在图层的边界上。

    myLayer.borderColor = [UIColor lightGrayColor].CGColor;
    myLayer.borderWidth = 5.f;
    myLayer.contentsGravity = kCAGravityCenter;
屏幕快照 2016-08-25 00.49.42.png
阴影:

有关阴影可以设置的属性有:
shadowOpacityshadowColorshadowOffsetshadowRadius(模糊程度)shadowPath(阴影路径)shadowOffset(偏移量)

    myLayer.shadowOpacity = 0.7;
    myLayer.shadowColor = [UIColor blackColor].CGColor;
    myLayer.shadowOffset = CGSizeMake(5.f, -5.f);
    myLayer.shadowRadius = 15.f;
屏幕快照 2016-08-25 00.57.44.png

需要注意的是在一个图层上裁剪圆角和添加阴影不能共同实现。因为阴影就是图层之外的东西,会被裁剪掉。那我们既要圆角又要阴影时怎么办呢?解决办法就是创建两个图层重叠在一起,其中一个实现圆角裁剪,一个实现添加阴影。

另外,和边框只会添加在图层的边界不同,阴影却是添加在图层内容的外围的。也就说虽然图层是个矩形,但若图层的内容为一个多边形或奇奇怪怪的多边图形,阴影是显示在多边形的边上的,即图层的内容。
也就是说图层在添加阴影时需要自己先计算出内容边界的模样,以待稍后绘制阴影,如此是比较耗费资源的。其实我们可以主动的告知阴影的路径,减少其资源消耗。这便是shadowPath的作用。

mask(图层蒙板):

图层是个矩形,内容显示出来一般都在一个矩形内。我们可以通过设置圆角,将矩形变为圆角矩形甚至圆形。但有时候我们希望我们要展现的内容是在一个不规则范围内时,我们可以使用“图层蒙板”来实现。下面的代码表示我们给UIImageView对应的图层设置一个不规则形状的蒙板maskmask本身就是个图层,如此依赖,UIImageView的图片只会展现出mask内容轮廓内的部分。

    UIImageView *imgV =[[UIImageView alloc] initWithFrame:CGRectMake(50, 100, 200, 200)];
    imgV.image = [UIImage imageNamed:@"ym01.jpg"];
    [self.view addSubview:imgV];
    
    CALayer *maskLayer = [CALayer layer];
    maskLayer.frame = imgV.bounds;
    UIImage *image = [UIImage imageNamed:@"dacuxiao"];
    maskLayer.contents = (__bridge id _Nullable)(image.CGImage);
    imgV.layer.mask = maskLayer;  // 将maskLayer设置为imgV对应图层的蒙板
屏幕快照 2016-08-25 上午11.09.06.png
拉伸过滤:

直接贴个原书中的解释:4.5 拉伸过滤

变换

UIView中的transform属性可以完成视图旋转、伸缩、平移等的变换。

    CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
    imgV.transform = transform; // 把imgV顺时针旋转45度

同样的,在CALayer中也有相应的属性来完成相同的效果。在图层中对应的属性是affineTransform(图层中也确实有叫transform的属性,但它的类型却是CATransform3D,它用来完成3D变换)。affineTransform类型为CGAffineTransform,属于Core Graphics框架,该框架为2D绘图API。

    CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4); // 旋转45度
//  transform = CGAffineTransformMakeScale(0.5, 0.5); // 宽高均缩小一半
//  transform = CGAffineTransformMakeTranslation(0, 100); // 向下平移100
    imgV.layer.affineTransform = transform;
屏幕快照 2016-08-25 下午3.58.25.png

上面提到了CALayer中的transform属性在类型为CATransform3D,表示3D变换。下面我们将图层绕着y轴旋转45度。

    CATransform3D transform  = CATransform3DMakeRotation(M_PI_4, 0, 1, 0); // 绕着y轴旋转45度
    imgV.layer.transform = transform;
屏幕快照 2016-08-25 下午4.16.42.png

得到的效果图似乎不像是旋转了45度,而更像是被水平压缩变瘦了。是我们3D旋转出问题了吗?其实完全没问题,在现实生活中,我们观察到某物体旋转一个角度后较远的一端会感觉较小,这是因为我们观察的视角不是在正前方,而是在一个斜向的视角。为了更接近实际生活场景,我们在创建transform对象之后要设置一个叫m34的属性,它的值在-500~-1000间。

    CATransform3D transform = CATransform3DIdentity; // 创建一个初始的transform
    transform.m34 = - 1.0 / 500.0; // 关键
    transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
    imgV.layer.transform = transform;

** 注意:**m34属性一定要在完成变换之前设置,不然无效。因此像下面这种写法运行后是无效果的,看起来仍然像被压缩过一样。

    CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
    transform.m34 = - 1.0 / 500.0; // 关键
    imgV.layer.transform = transform;

几个专用图层

CAShapeLayer

图层中并非只能表现颜色和图片,上面我们在“自定义绘制”中已经说了可以直接给图层自定义绘制内容。要么在图层的代理方法实现里,要么在图层对应的UIViewdrawRect:方法里进行绘图。而所谓CAShapeLayer就是一个封装了绘制功能的CALayer,它暴露了些有关绘制的属性,比如path,fillColor,lineCap等,只需要进行设置简单设置就可以完成绘图。我们的图形是什么样,完全取决于path,它是CGPathRef类型的,我们可通过UIBezierPath绘制出路径,然后将其转为CGPathRef

    UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(30, 30, 70, 70)]; // 绘制一个矩形
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.frame = CGRectMake(0, 0, 100, 100);
    shapeLayer.backgroundColor = [UIColor lightGrayColor].CGColor;
    shapeLayer.path = path.CGPath;
    shapeLayer.fillColor = [UIColor orangeColor].CGColor;
    [imgV.layer addSublayer:shapeLayer];
屏幕快照 2016-08-25 下午4.53.59.png

CATextLayer

同样的,当图层想表现文本内容时,除了我们可以自定义绘制文本外。系统也直接提供了便于变现文本的CATextLayer图层。

    CATextLayer *textLayer = [CATextLayer layer];
    textLayer.frame = CGRectMake(0, 0, CGRectGetWidth(imgV.frame), 40);
    textLayer.backgroundColor = [UIColor lightGrayColor].CGColor;
    textLayer.foregroundColor = [UIColor orangeColor].CGColor;
    UIFont *font = [UIFont systemFontOfSize:15];
    CFStringRef fontName = (__bridge CFStringRef)(font.fontName);
    CGFontRef fontRef = CGFontCreateWithFontName(fontName);
    textLayer.font = fontRef;
    textLayer.fontSize = font.pointSize;
    textLayer.wrapped = YES;
    textLayer.string = @"让青春吹动了你的长发,让他牵引你的梦。";
    [imgV.layer addSublayer:textLayer];
屏幕快照 2016-08-25 下午5.07.47.png

CAGradientLayer

用于生成多种颜色平滑渐变的效果。

    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = imgV.bounds;
    gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id)[UIColor orangeColor].CGColor, (__bridge id)[UIColor greenColor].CGColor];
    gradientLayer.startPoint = CGPointMake(0, 0);
    gradientLayer.endPoint = CGPointMake(1, 1);
    gradientLayer.locations = @[@0.0, @0.1, @0.7]; // 注意locations和colors的个数一定要相同
    [imgV.layer addSublayer:gradientLayer];
屏幕快照 2016-08-25 下午5.16.29.png

CAEmitterLayer

高性能的粒子引擎。可实现焰火,烟雾,下雨等动态效果。

    CAEmitterLayer *emitterLayer = [CAEmitterLayer layer];
    emitterLayer.frame = imgV.bounds;
    emitterLayer.renderMode = kCAEmitterLayerAdditive; // 渲染模式
    emitterLayer.emitterPosition = CGPointMake(CGRectGetWidth(self.view.frame)/2.f, CGRectGetHeight(self.view.frame));
    emitterLayer.emitterMode = kCAEmitterLayerPoints; // 发射模式
    emitterLayer.emitterShape = kCAEmitterLayerPoint; // 发射源的形状
    emitterLayer.emitterSize = CGSizeMake(5.f, 5.f); // 发射源尺寸大小
    emitterLayer.lifetime = 5.f; // 粒子生命周期
//  emitterLayer.scale =
    emitterLayer.spin = 3.f; // 自旋转速度
    emitterLayer.speed = 3.f; //
    emitterLayer.velocity = 3.f; // 粒子速度
    
    
    // 粒子cell
    CAEmitterCell *cell = [[CAEmitterCell alloc] init];
    cell.contents = (__bridge id)[UIImage imageNamed:@"spark"].CGImage;
//  cell.alphaRange =
    cell.birthRate = 150;
    cell.lifetime = 5.0;
    cell.color = [UIColor colorWithRed:1 green:0.5 blue:0.1 alpha:1.0].CGColor;
    cell.alphaSpeed = -0.4;
    cell.velocity = 20;
    cell.velocityRange = 50;
    cell.emissionRange = 0.3*M_PI;
    cell.emissionLongitude = 3.f;
    emitterLayer.emitterCells = @[cell];
    
    [self.view.layer addSublayer:emitterLayer];
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容