前言
通常我们一提到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上对CGImage
和UIImage
均起作用。
myLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"ym01.jpg"].CGImage);
contentsGravity属性:
contentsGravity和UIView
中的contentMode
比较类似,都是表示内容的位置或者对齐方式。与contentMode
类型为枚举不同的是该属性值为NSString
类型的,可选常量值有多种。
这篇博文对此属性不同常量值的含义解释的比较清楚:CoreAnimation编程指南(五)图层内容
为了弄清楚几个属性值的具体含义,我自己也在代码里试了试,结果如下图:
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);
contentsRect属性:
contentsRect属性意为“全面拉伸”,指在寄宿图中圈定出一块区域,图片拉伸时这块区域会纵向横向全面的拉伸,而外围边框处的只做单向的拉伸或不拉伸。
关于该属性,这篇文章解释得很清楚:关于CALayer的contentsCenter属性
** 自定义绘制:** 给图层的
contents
赋CGImage
的值不是唯一的设置寄宿图的方法,我们还可以直接用Core Graphics
直接绘制寄宿图。
CALayer
可实现CALayerDelegate
协议,将其代理设为图层所属的视图view,并且实现代理方法displayLayer:
或者drawLayer:inContext:
,在代理方法里自己通过Core Graphics
绘制。需要注意的是,CALayer
不会自动重绘其内容,需要显示地调用display
方法。也因此,更常见的做法是我们可以重写UIView
的drawRect:
方法,在该方法内进行绘制,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)。
zPosition属性:
zPosition
和anchorPointZ
属性都是表示三维空间布局的属性。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的自动布局:
UIView
有UIViewAutoresizingMask
和NSLayoutConstraint
来实现自动布局。但是CALayer
并不支持自动布局。若遇到需要调整子图层重新布局问题,则需要手动在CALayer
的代理方法里手动调整其布局。该代理方法被触发的时机是layer
的bounds
改变,或者调用了setNeedsLayout
方法。
- (void)layoutSublayersOfLayer:(CALayer *)layer;
因为CALayer
对自动布局不友好,且无法处理触摸事件,所以能用UIView
处理的需求,还是用UIView
处理比较好。
圆角:
myLayer.cornerRadius = 20.f;
myLayer.masksToBounds = YES;
边框:
myLayer.borderColor = [UIColor lightGrayColor].CGColor;
myLayer.borderWidth = 5.f;
需要注意的是图层的边框是依附在图层的外围的,而不是图层内容的外围。如下这种情况,图层的内容超出了图层界限,但是边框还是显示在图层的边界上。
myLayer.borderColor = [UIColor lightGrayColor].CGColor;
myLayer.borderWidth = 5.f;
myLayer.contentsGravity = kCAGravityCenter;
阴影:
有关阴影可以设置的属性有:
shadowOpacity
、shadowColor
、shadowOffset
、shadowRadius(模糊程度)
、shadowPath(阴影路径)
。shadowOffset(偏移量)
myLayer.shadowOpacity = 0.7;
myLayer.shadowColor = [UIColor blackColor].CGColor;
myLayer.shadowOffset = CGSizeMake(5.f, -5.f);
myLayer.shadowRadius = 15.f;
需要注意的是在一个图层上裁剪圆角和添加阴影不能共同实现。因为阴影就是图层之外的东西,会被裁剪掉。那我们既要圆角又要阴影时怎么办呢?解决办法就是创建两个图层重叠在一起,其中一个实现圆角裁剪,一个实现添加阴影。
另外,和边框只会添加在图层的边界不同,阴影却是添加在图层内容的外围的。也就说虽然图层是个矩形,但若图层的内容为一个多边形或奇奇怪怪的多边图形,阴影是显示在多边形的边上的,即图层的内容。
也就是说图层在添加阴影时需要自己先计算出内容边界的模样,以待稍后绘制阴影,如此是比较耗费资源的。其实我们可以主动的告知阴影的路径,减少其资源消耗。这便是shadowPath
的作用。
mask(图层蒙板):
图层是个矩形,内容显示出来一般都在一个矩形内。我们可以通过设置圆角,将矩形变为圆角矩形甚至圆形。但有时候我们希望我们要展现的内容是在一个不规则范围内时,我们可以使用“图层蒙板”来实现。下面的代码表示我们给UIImageView
对应的图层设置一个不规则形状的蒙板mask
,mask
本身就是个图层,如此依赖,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对应图层的蒙板
拉伸过滤:
直接贴个原书中的解释: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;
上面提到了CALayer
中的transform
属性在类型为CATransform3D
,表示3D变换。下面我们将图层绕着y轴旋转45度。
CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0); // 绕着y轴旋转45度
imgV.layer.transform = transform;
得到的效果图似乎不像是旋转了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
图层中并非只能表现颜色和图片,上面我们在“自定义绘制”中已经说了可以直接给图层自定义绘制内容。要么在图层的代理方法实现里,要么在图层对应的UIView
的drawRect:
方法里进行绘图。而所谓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];
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];
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];
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];