iOS之CALayer相关使用
CALayer介绍
CALayer
在概念和UIView
类似,也是一些被层级关系树管理的矩形块,可以包含图片、文字、背景色等内容。和UIView
的最大不同是不能够处理与用户的交互。
每一个UIView
都有个CALayer
实例的图层属性,被称为backing layer(支持层),由视图负责创建并管理这个图层,以确保当子视图在层级关系中添加或被移除时,对应的关联图层也有相同的操作。
-
图层与视图的底层关系
1.UIView、UIColor、UIImage都定义于UIKit框架中;
2.CALayer定义在QuartzCore框架中的CoreAnimation中;
3.CGImageRef、CGColorRef两种数据类型是定义在CoreGraphics框架中
4.QuartzCore框架和CoreGraphics框架可以跨平台使用,在iOS和Mac OS上都能使用,但是UIKit只能在iOS中使用;为了保证可移植性,QuartzCore是不能直接使用UIImage和UIColor的,如果使用需要将其转化为CGImageRef,CGColorRef -
为什么使用图层?
在智能手机等设备上,用户希望他们的一切操作都很快。保持连贯的帧速率很关键,这样用户才会觉得「丝滑流畅」。在 iOS 中,帧速率是每秒 60 帧。为了保持系统能在这个速率下运作,一个直接运行在 GPU 上、非常基础但是功能强大的图形功能层诞生了,它就是 OpenGL。
OpenGL 提供了大部分底层的(而且是最快的)访问权限,直达 iOS 设备的图像硬件。然而你需要做出权衡:OpenGL 太靠近底层了,即便是为了完成最简单的任务,都需要大量的代码。
为了能够缓解这个问题,苹果创建了 Core Graphics,它提供了更高级一些的方法,代码量也随之更少。使用 Core Graphics 的初衷是应用到一些比较底层的功能上。为了使 Core Graphics 的使用更简单,苹果创建了 Core Animation。它提供了
CALayer
类,并且允许一些基本的底层图像操作。当苹果认为 Core Animation 中的很多高级高级功能在常规应用中并不总是需要的时候,UIKit 就诞生了,它提供了 iOS 中最顶层的图像访问权限。此设计方案的优点是,在你的应用中,你可以选择你需要的图像访问级别并且应用它,允许你挑选并精准地选择所需要的功能量级,使你不必编写无用的代码。
缺点是较高级别的图形 API 提供的功能比较少。讲这个故事是为了说明:因为CALayer
的存在,iOS 系统可以洞悉你的应用中的视图层次结构,快速生成层次结构的位图信息,然后将其传递到 Core Graphics 中去,最终到达 OpenGL,这样通过设备的 GPU 处理后图像就呈现在屏幕上了。尽管在大多数情况下都不需要直接使用CALayer
,但是较为底层的 API 为用户提供了一些更灵活的自定义方案,我们将在本文中讨论。
CALayer的使用
最简单的使用方式
CALayer *redLayer = [CALayer layer]; redLayer.backgroundColor = [UIColor redColor].CGColor; redLayer.frame = CGRectMake(100, 100, 100, 100); [self.view.layer addSublayer:redLayer];
contents属性
contents
属性被定义为id
类型,意味着它可以是任何类型的对象,实际上给contents
属性赋任何值都是可以编译通过的,但是,在实践中,如果contents
赋值不是CGImage,得到的图层是空白的。
实际上,真正要赋值的类型是CGImageRef
,他是一个指向CGImage结构的指针。UIImage有一个CGImage属性,返回一个CGImageRef
,因为他不是一个Cocoa对象,而是一个CoreFoundation类型,需要通过桥接__bridge关键字进行转换,如下
layer.contents = (__bridge id)(image.CGImage);
注意:通过此方法,可以利用CALayer在一个普通的UIView中显示一张图片了。
疑问:那为什么要把contents
属性定义成泛型呢?
因为在Mac OS系统上contents
属性对CGImage
和NSImage
都起作用,但是在iOS系统中将UIImage
类型的值赋给它,得到的依然是一个空白图层。
-
UIView方法绘制自定义寄宿图
给contents赋值CGImage的值并不是唯一设置寄宿图的方法,我们可以使用CoreGraphics直接绘制寄宿图,即通过继承UIView并实现-drawRect:的方式。
-drawRect: 方法是UIView没有默认实现的方法,因为寄宿图并不是必须的;但如果UIView检测到此方法被实现了,此方法会被自动调用,然后我们就可以在其中使用CoreGraphics绘制自己需要的内容了;下面的代码就演示了drawRect自定义绘制寄宿图的具体操作,实现一个环形的绘制:
#import "CustomView.h" @implementation CustomView - (void)drawRect:(CGRect)rect { [self testFunctionWithTwo]; } ///方式一 - (void)testFunctionWithOne { CGRect rect = self.bounds; //计算半径 CGFloat radius = MIN(rect.size.width, rect.size.height) * 0.5; //创建圆的路径 UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius]; [[UIColor redColor] set]; [bezierPath fill]; } ///方式二 - (void)testFunctionWithTwo { CGRect rect = self.bounds; //获取画布 CGContextRef context = UIGraphicsGetCurrentContext(); //画笔颜色 CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor); //画笔宽度 CGFloat lineWidth = 5; CGContextSetLineWidth(context, lineWidth); //圆点图标 CGFloat conterX = CGRectGetWidth(rect)/2.0; CGFloat conterY = CGRectGetHeight(rect)/2.0; CGFloat cusRadius = (self.frame.size.width/2.0 - lineWidth/2.0f); double PI = 3.14159265358979323846; //绘制路径:初始角度,结束角度 CGContextAddArc(context, conterX, conterY, cusRadius, 1.5*PI, 1.5*PI + 2*PI, NO); CGContextDrawPath(context, kCGPathStroke); } @end
绘制效果如下:
特别注意1:如果没有自定义绘制任务不需要寄宿图,就不要在子类中写一个空的-drawRect:方法,否则会造成CPU资源和内存的浪费;
特别注意2:如果我们将绘制过程的角度参数改为动态,并结合定时器调用-setNeedsDisplay方法,就可以实现环形动画的效果;
特别注意3:其实UIView内部负责绘制的
layer
图层的代理其实就是UIView本身,也是就是说view.layer.delegate = view
,相当于这样一个逻辑。而-drewRect:
只不过是layer
代理方法封装的产物。 -
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决定)和一个CoreGraphics的绘制上下文环境,为绘制寄宿图做准备,并将其以ctx参数传入。
现在我们以方法2为例,演示CALayer绘制自定义寄宿图的过程,具体代码如下:
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self renderViews]; }
#pragma mark - Initail Methods - (void)renderViews { CALayer *blackLayer = [CALayer new]; blackLayer.backgroundColor = [UIColor blackColor].CGColor; blackLayer.frame = CGRectMake(0.0f, 0.0f, 100.f, 200.f); blackLayer.position = self.view.center; blackLayer.contentsScale = [UIScreen mainScreen].scale; blackLayer.delegate = self; [self.view.layer addSublayer:blackLayer]; [blackLayer display]; } - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx { CGContextSetLineWidth(ctx, 10.0f); CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor); CGContextStrokeEllipseInRect(ctx, layer.bounds); } @end
效果如下:
代码分析:-
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方法;
-
contentsGravity属性
使用contentsGravity
属性来设置图层中内容在边界中如何对齐,效果等同于UIView的contentMode
。该属性的值都是字符串类型,官方解释如下:
/* A string defining how the contents of the layer is mapped into its bounds rect. Options are
center
,top
,bottom
,left
,right
,topLeft
,topRight
,bottomLeft
,bottomRight
,resize
,resizeAspect
,resizeAspectFill
. The default value isresize
. Note that "bottom" always means "Minimum Y" and "top" always means "Maximum Y". */
使用方式
layer.contentsGravity = kCAGravityCenter;
contentsScale属性
使用contentsScale
属性来定义寄宿图的像素尺寸和视图大小的比利,默认值为1.0。属于支持高分辨率(又称为Hi-DPI或Retina)屏幕机制的一部分,用来判断在绘制图层时候为寄宿图创建的空间大小,和需要显示的图片的拉伸度(假设没有设置contentsGravity属性)。设置为1.0,表示将会以每个点1个像素绘制图片,设置为2.0,将以每个点2个像素绘制图片。
使用代码处理寄宿图时,需要手动设置图层的contentsScale,
redLayer.contentsScale = [UIScreen mainScreen].scale;
maskToBounds属性
使用该属性来决定是否显示超出边界的内容,类似于UIView的clipsToBounds
属性。
contentsRect属性
使用该属性来显示寄宿图的一个子域,它使用单位坐标,是一个相对值。(点就是虚拟的像素,也成为逻辑像素。在标准设备上,一个点就是一个像素,但在Retain设备上,一个点等于 2 * 2个像素)
默认的contentsRect是{0,0,1,1},意味着整个寄宿图都是可见的。主要应用是图片拼合,因为单张大图比多张小图载入更快,可以有效提高载入性能
contentsCenter
需要注意的是contentsCenter
其实是一个CGRect,它定义了一个固定的边框和一个在图层上可拉伸的区域,默认值是{0,0,1,1}。改变contentsCenter
的值并不会影响到寄宿图的显示,除非这个图层的大小改变了,才能看到效果。
举例说明:将contentsCenter设置为{0.25,0.25,0.5,0.5},效果如下
还可以在IB中直接设置
最终实现的效果和UIImage的resizableImageWithCapInsets:
方法类似。
frame、bounds、position
1、position类似于UIView中的center,表示相对于父图层anchorPoint所在的位置
2、frame是计算属性,是根据bounds,position,transform计算而来的,通常情况下,frame
的宽高和bounds
的宽高是相同的,所以当其中任何一个发生改变时候,他都会发生改变。
如以下代码执行情况为:
UIView *redView = [[UIView alloc]init]; redView.backgroundColor = [UIColor redColor]; redView.frame = CGRectMake(100, 100, 100, 100); [self.view addSubview:redView]; redView.transform = CGAffineTransformMakeRotation(M_PI_4); NSLog(@"frame %@ bounds %@",NSStringFromCGRect(redView.frame),NSStringFromCGRect(redView.bounds));
结果为:
frame {{79.289321881345259, 79.289321881345245}, {141.42135623730951, 141.42135623730951}} bounds {{0, 0}, {100, 100}}
anchorPoint 锚点
视图的center属性和图层的position属性都指定了anchorPoint相对于父图层的位置。默认情况下,anchorPoint
位于图层的中点,所以图层会以这个点为中心放置。(注:UIView没有暴露锚点的属性,也是视图的position可以被称为center的原因)
锚点anchorPoint
是用单位坐标来描述的,而且x和y的值可以大于1或小于0,使他可以放置在图层范围之外。
在聊锚点之前,我们有必要重新认识一下center
属性和position
属性。我们知道,这两个属性都是设置当前视图(或图层)的中心点在父视图(或父图层)坐标系统中位置,从而确定当前视图(或图层)在父视图(或父图层)位置。作为视图这个点是当前视图的中心点,但是对于图层来说可能就不是中心点了。
在CALayer中有一个属性叫做anchorPoint
,也就是我们接下来要说的锚点。但是在UIView中,这个属性并没有被接口暴露出来,这也是为什么UIView的position
属性被叫做center
的原因,因为center
包含了位置
+中心
两层含义,center
其实是由position
和anchorPoint
两个属性叠加得到的,此时anchorPoint
对应的值是默认的中心点位置。
那么position
和anchorPoint
到底代表什么呢?下面我们通过一个物理模型“图钉说”来帮助大家理解一下(请牢记这个模型):
我们可以把当前图层比作一张纸,父图层比作一面墙。我们通过
position
和anchorPoint
属性将当前图层添加到父图层上,其实就相当于拿一枚图钉将这张纸固定到这面墙上。那么position
和anchorPoint
分别代表这个物理模型中的什么呢?这里图钉穿过纸张的位置就是anchorPoint
锚点的位置,图钉扎在墙上的位置就是position
的位置。也就是说anchorPoint
锚点的位置是相对于当前图层的,而position
是相对于父图层的。
【 这里要说明一点的是:anchorPoint
使用的坐标系统跟我们上面提到的contentsRect
是一样的,使用的是相对坐标系统,也就是说左上角的坐标是{0,0}右下角的坐标是{1,1}。并且我们也可以通过设置小于0或大于1的值,把锚点放置在图层范围之外。】
看完上面这个物理模型之后,我们再来思考这样一个问题:如果在position
不变的情况下改变anchorPoint
,此时会发生什么变化?继续利用上面的模型,相当于我们将图钉摘下,从纸的另一个位置穿过,然后重新扎回上次的位置。此时图钉在墙上的位置不变,但是纸张相对墙的位置却发生了变化,其实也就是frame
发生了变化。

关于anchorPoint
和position
就先聊这么多,如果想找一个应用场景通过代码来深入理解一下anchorPoint
和position
的话,可以看下面的示例,示例中用到了锚点的知识。这里还要给大家补充另外一个知识点:当一个图层通过transform
进行旋转的时候,图层所围绕的中心就是锚点,这跟我们上面提到的物理模型也是相吻合的。
举例说明,实现一个简单的钟表:
@interface ViewController () @property(nonatomic,strong) UIView *hourView; @property(nonatomic,strong) UIView *minuteView; @property(nonatomic,strong) UIView *secondView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. self.view.backgroundColor = [UIColor cyanColor]; self.hourView = [[UIView alloc]init]; self.hourView.backgroundColor = [UIColor greenColor]; self.hourView.bounds = CGRectMake(0, 0, 20, 100); self.hourView.center = self.view.center; [self.view addSubview:self.hourView]; self.hourView.layer.anchorPoint = CGPointMake(0.5, 0.9); self.minuteView = [[UIView alloc]init]; self.minuteView.backgroundColor = [UIColor lightGrayColor]; self.minuteView.bounds = CGRectMake(0, 0, 15, 100); self.minuteView.center = self.view.center; [self.view addSubview:self.minuteView]; self.minuteView.layer.anchorPoint = CGPointMake(0.5, 0.9); self.secondView = [[UIView alloc]init]; self.secondView.backgroundColor = [UIColor purpleColor]; self.secondView.bounds = CGRectMake(0, 0, 10, 100); self.secondView.center = self.view.center; [self.view addSubview:self.secondView]; self.secondView.layer.anchorPoint = CGPointMake(0.5, 0.9); NSTimer *timer = [[NSTimer alloc]initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(tick) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes]; } - (void)tick{ NSCalendar *calendar = [[NSCalendar alloc]initWithCalendarIdentifier:NSCalendarIdentifierChinese]; NSDateComponents *components = [calendar components:NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond fromDate:[NSDate date]]; NSLog(@"Tick...hour %ld minute %ld second %ld",(long)components.hour,(long)components.minute,(long)components.second); CGFloat secondAngle = (components.second / 60.0) * M_PI *2.0; CGFloat minuteAngle = (components.minute / 60.0) * M_PI * 2.0; CGFloat hourAngle = (components.hour / 12.0) * M_PI * 2.0 + (components.minute / 60.0) * M_PI / 6.0; self.hourView.transform = CGAffineTransformMakeRotation(hourAngle); self.minuteView.transform = CGAffineTransformMakeRotation(minuteAngle); self.secondView.transform = CGAffineTransformMakeRotation(secondAngle); } @end
实现效果如图:
geometryFlipped
一般来说,在iOS上,一个图层的position是相对于父图层的左上角,但在Mac上,是相对于左下角。那么可以通过将属性geometryFlipped
设置为YES来改变这种情况。如果将一个视图的geometryFlipped
属性设置为YES,那么所有添加到该图层上的图层和视图的frame都将是相对于左下角而言的,
Z坐标轴
UIView
是存在于一个二维坐标系中的,而CALayer
是存在于一个三维空间中的,描述Z轴上的位置有两个属性zPosition
和anchorPointZ
。zPosition
可以用作变换图层,还有改变图层的显示顺序。
conrnerRadius
可以利用该属性控制图层角的曲率,默认值是0(也就是直角),但这个曲率只会影响背景颜色而不会影响背景图片或者子视图,如果需要截取图层里的东西,需要将masksToBounds
设置为YES。
redLayer.cornerRadius = 30; redLayer.masksToBounds = YES;
图层边框
利用属性borderWidth
和borderColor
设置图层边框。borderWidth
是以点为单位定义边框粗细的浮点数,默认值为0;borderColor
定义边框的颜色,默认是黑色,是CGColorRef
类型
redLayer.borderWidth = 3.0; redLayer.borderColor = [UIColor yellowColor].CGColor;
阴影
设置阴影主要有四个属性来控制,shadowOpacity
、shadowColor
、shadowOffset
、shadowRadius
。
-
shadowOpacity
默认值为0,是一个取值在0.0(不可见)和1.0(完全不透明)之间的浮点数。如果将其设置为1.0,在图层的上方将会显示一个有轻微模糊的黑色阴影。
-
shadowColor
控制阴影的颜色。默认值是黑色,类型是CGColorRef
-
shadowOffset
控制阴影的方向和距离,是一个CGSize类型,宽度控制阴影横向的位移,高度控制纵向的位移。默认值是{0,-3},意思是相对Y轴有3个点的向上位移。
-
shadowRadius
控制阴影的模糊度,值越大,边界线看上去就会越来越模糊和自然(最好设置为非零值)。 -
shadowPath
我们已经知道图层阴影并不总是方的,而是从图层内容的形状继承而来。这看上去不错,但是实时计算阴影也是一个非常消耗资源的,尤其是图层有多个子图层,每个图层还有一个有透明效果的寄宿图的时候。如果你事先知道你的阴影形状会是什么样子的,你可以通过指定一个
shadowPath
来提高性能。shadowPath
是一个CGPathRef
类型(一个指向CGPath
的指针)。CGPath
是一个Core Graphics对象,用来指定任意的一个矢量图形。我们可以通过这个属性单独于图层形状之外指定阴影的形状。下图展示了同一寄宿图的不同阴影设定。如你所见,我们使用的图形很简单,但是它的阴影可以是你想要的任何形状。
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *layerView1; @property (nonatomic, weak) IBOutlet UIView *layerView2; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //enable layer shadows self.layerView1.layer.shadowOpacity = 0.5f; self.layerView2.layer.shadowOpacity = 0.5f; //create a square shadow CGMutablePathRef squarePath = CGPathCreateMutable(); CGPathAddRect(squarePath, NULL, self.layerView1.bounds); self.layerView1.layer.shadowPath = squarePath; CGPathRelease(squarePath); //create a circular shadow CGMutablePathRef circlePath = CGPathCreateMutable(); CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds); self.layerView2.layer.shadowPath = circlePath; CGPathRelease(circlePath); } @end
如果是一个矩形或者是圆,用
CGPath
会相当简单明了。但是如果是更加复杂一点的图形,UIBezierPath
类会更合适,它是一个由UIKit提供的在CGPath基础上的Objective-C包装类。大一些的阴影位移和角半径会增加图层的深度即视感
mask(遮罩)
使用mask
属性,可以利用一个32位有alpha通道的png图片创建无矩形视图。它本身是一个CALayer类型,和其他视图有一样的绘制和布局属性,类似于一个子图层,相对于父图层(即拥有该属性的图层)布局,但不同于那些绘制在父图层中的子图层,mask图层定义了父图层的部分可见区域,mask
图层的Color
属性是无关紧要的,真正重要的是图层的轮廓。mask
属性就像是一个饼干切割机,mask
图层实心的部分会被保留下来,其他的则会被抛弃。
如果mask
图层比父图层要小,只有在mask
图层里面的内容才是它关心的,除此以外的一切都会被隐藏起来。
如下图
代码示例:
///遮罩层
CALayer *mask = [CALayer layer]; mask.frame = redLayer.bounds; mask.contents = (__bridge id)[UIImage imageNamed:@"circle"].CGImage;
///父图层
redLayer.mask = mask;
CALayer蒙版图层最厉害的地方在于他不限于静态图,任何有图层构成的都可以作为mask属性,意味着蒙版可以通过代码甚至是动画实时生成。
minificationFilter & magnificationFilter(拉伸过滤)
当图片需要显示不同的大小时候,可以使用拉伸过滤算法,他可以作用于原图的像素上并根据需要生成新的像素显示在屏幕上。CALayer提供了三种拉伸过滤方法:kCAFilterLinear
、kCAFilterNearest
、kCAFilterTrilinear
。minification(缩小图片)和magnification(放大图片)默认的过滤器都是kCAFilterLinear
。
一些特定的CALayer使用方式
CAShaperLayer
CAShapLayer是一个通过矢量图形而不是bitmap来绘制的图层子类,可以使用CGPaht来定义想要的任何图形,他可以将其自动渲染出来。
举例说明:
UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(175, 100)]; [path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:(M_PI *2) clockwise:YES]; [path moveToPoint:CGPointMake(150, 125)]; [path addLineToPoint:CGPointMake(150, 175)]; [path addLineToPoint:CGPointMake(125, 225)]; [path moveToPoint:CGPointMake(150, 175)]; [path addLineToPoint:CGPointMake(175, 225)]; [path moveToPoint:CGPointMake(100, 150)]; [path addLineToPoint:CGPointMake(200, 150)]; CAShapeLayer *shapeLayer = [CAShapeLayer layer]; shapeLayer.strokeColor = [UIColor redColor].CGColor; shapeLayer.fillColor = [UIColor clearColor].CGColor; shapeLayer.lineWidth = 5; shapeLayer.lineJoin = kCALineJoinRound; shapeLayer.lineCap = kCALineCapRound; shapeLayer.path = path.CGPath; shapeLayer.shadowOpacity = 1.0; [self.view.layer addSublayer:shapeLayer];
实现效果:

还可以利用CAShapeLayer实现指定方向有圆角
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(50, 50, 100, 100) byRoundingCorners:UIRectCornerTopLeft|UIRectCornerBottomRight cornerRadii:CGSizeMake(20, 20)]; CAShapeLayer *shapeLayer = [CAShapeLayer layer]; shapeLayer.strokeColor = [UIColor redColor].CGColor; shapeLayer.fillColor = [UIColor clearColor].CGColor; shapeLayer.lineWidth = 5; shapeLayer.lineJoin = kCALineJoinRound; shapeLayer.lineCap = kCALineCapRound; shapeLayer.path = path.CGPath; shapeLayer.shadowOpacity = 1.0; [self.view.layer addSublayer:shapeLayer];
效果如下:

CATextLayer
CATextLayer
使用了CoreText,比使用UILabel渲染要快得多,而且基本包含了UILabel提供的所有绘制特性。 基本使用方式:
CATextLayer *textLayer = [CATextLayer layer]; textLayer.frame = CGRectMake(100, 100, 200, 80); [self.view.layer addSublayer:textLayer]; //设置文本内容 textLayer.string = @"子曰:君子食无求饱,居无求安,敏於事而慎於言,就有道而正焉,可谓好学也已。"; //设置背景颜色 textLayer.backgroundColor = [UIColor redColor].CGColor; //设置字体颜色 默认是白色 textLayer.foregroundColor = [UIColor blackColor].CGColor; //设置字体 需要将UIFont类型转换成为CGFontRef类型 UIFont *font = [UIFont systemFontOfSize:15]; CFStringRef fontName = (__bridge CFStringRef)font.fontName; CGFontRef fontRef = CGFontCreateWithFontName(fontName); textLayer.font = fontRef; textLayer.fontSize = font.pointSize;//字体大小需要使用fontSize来进行单独设置,因为CGFontRef类型并不像UIFont一样包含大小 CGFontRelease(fontRef); //设置字体对齐方式 textLayer.alignmentMode = kCAAlignmentJustified; textLayer.wrapped = YES; //设置文字分辨率 textLayer.contentsScale = [UIScreen mainScreen].scale;
实现效果:

富文本的使用方式:
CATextLayer *textLayer = [CATextLayer layer]; textLayer.frame = CGRectMake(100, 100, 200, 80); [self.view.layer addSublayer:textLayer]; textLayer.contentsScale = [UIScreen mainScreen].scale; textLayer.alignmentMode = kCAAlignmentJustified; textLayer.wrapped = YES; NSMutableAttributedString *string = [[NSMutableAttributedString alloc]initWithString:@"子曰:君子食无求饱,居无求安,敏於事而慎於言,就有道而正焉,可谓好学也已。"]; UIFont *font = [UIFont systemFontOfSize:15]; CFStringRef fontName = (__bridge CFStringRef)font.fontName; CGFloat fontSize = font.pointSize; CTFontRef fontRef = CTFontCreateWithName(fontName, fontSize, NULL); NSDictionary *attribs = @{ (__bridge id)kCTForegroundColorAttributeName : (__bridge id)[UIColor blackColor].CGColor, (__bridge id)kCTFontAttributeName : (__bridge id)fontRef, }; [string setAttributes:attribs range:NSMakeRange(0, string.length)]; NSDictionary *underlineAttri = @{ (__bridge id)kCTForegroundColorAttributeName : (__bridge id)[UIColor redColor].CGColor, (__bridge id)kCTFontAttributeName : (__bridge id)fontRef, (__bridge id)kCTUnderlineStyleAttributeName : @(kCTUnderlineStyleDouble), }; CFRelease(fontRef); [string setAttributes:underlineAttri range:NSMakeRange(0, 2)]; textLayer.string = string;
实现效果:
CAGradientLayer
使用CAGradientLayer
可以生成两种或多种颜色平滑渐变,它使用硬件加速。
简单的两种颜色的渐变
CAGradientLayer *layer = [CAGradientLayer layer]; layer.frame = CGRectMake(100, 100, 100, 100); [self.view.layer addSublayer:layer]; layer.colors = @[ (__bridge id)[UIColor redColor].CGColor, (__bridge id)[UIColor blueColor].CGColor, ]; layer.startPoint = CGPointMake(0.5, 0.5);//以单位坐标系进行定义 layer.endPoint = CGPointMake(1.0, 1.0);
实现效果:

多重渐变
colors
属性可以包含很多颜色,默认情况下,这些颜色是均匀的被渲染,但也可以使用locations
属性来调整空间。locations
是一个浮点数值的数组,使用NSNumber
进行包装。
注:locations和colors两个数组的大小一定要一致,不然将会得到一个空白的渐变
CAGradientLayer *layer = [CAGradientLayer layer]; layer.frame = CGRectMake(100, 100, 100, 100); [self.view.layer addSublayer:layer]; layer.colors = @[ (__bridge id)[UIColor redColor].CGColor, (__bridge id)[UIColor blueColor].CGColor, (__bridge id)[UIColor greenColor].CGColor, ]; layer.locations = @[ @(0.0), @(0.25), @(0.5), ]; layer.startPoint = CGPointMake(0, 0);//以单位坐标系进行定义 layer.endPoint = CGPointMake(1.0, 1.0);
均匀分布效果图:

按照locations分布效果图:
CAReplicatorLayer
利用CAReplicatorLayer
可以高效生成许多类似的图层,他可以绘制一个或多个图层的子图层,并且在每个复制体上应用不同的变换。
重复图层
使用方式:
CAReplicatorLayer *replicatorLayer = [CAReplicatorLayer layer]; replicatorLayer.frame = CGRectMake(0, 0, 100, 100); replicatorLayer.position = self.view.center; [self.view.layer addSublayer:replicatorLayer]; replicatorLayer.backgroundColor = [UIColor redColor].CGColor; replicatorLayer.instanceCount = 2; replicatorLayer.instanceTransform = CATransform3DMakeTranslation(10, 10, 0); replicatorLayer.instanceRedOffset = -0.1; replicatorLayer.instanceBlueOffset = -0.1; CALayer *layer = [CALayer layer]; layer.frame = CGRectMake(100, 100, 100, 100); layer.backgroundColor = [UIColor whiteColor].CGColor; [replicatorLayer addSublayer:layer];
显示效果:

实现反射效果
可以使用CAReplicatorLayer
并应用一个负比例变换于一个复制图层,可以创建指定视图内容的镜像图片,就实现了反射效果。
封装一个View来实现:
#import "IVReflectionView.h" @implementation IVReflectionView + (Class)layerClass{ return [CAReplicatorLayer class]; } - (instancetype)initWithFrame:(CGRect)frame{ self = [super initWithFrame:frame]; if (self) { [self setup]; } return self; } - (void)awakeFromNib{ [super awakeFromNib]; //this is called when view is created from a nib [self setup]; } - (void)setup{ CAReplicatorLayer *layer = (CAReplicatorLayer *)self.layer; layer.instanceCount = 2; CATransform3D transform = CATransform3DIdentity; CGFloat verticalOffset = self.bounds.size.height + 2; transform = CATransform3DTranslate(transform, 0, verticalOffset, 0); transform = CATransform3DScale(transform, 1, -1, 0); layer.instanceTransform = transform; layer.instanceAlphaOffset = -0.6; } @end
使用方法:
IVReflectionView *reflectionView = [[IVReflectionView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; reflectionView.backgroundColor = [UIColor redColor]; UIImageView *imageView = [[UIImageView alloc]initWithFrame:reflectionView.bounds]; imageView.image = [UIImage imageNamed:@"media"]; [reflectionView addSubview:imageView]; [self.view addSubview:reflectionView];
显示效果:

CAScrollLayer
CAScrollLayer
有一个scrollToPoint:
方法,他能自动适应bounds
的原点以便图层内容出现在滑动的地方,但并不负责将触摸事件转换为滑动事件,既不渲染滚动条,也不实现iOS指定行为例如滑动反弹。
举例说明:将CAScrollLayer
作为视图的宿主图层,创建一个自定义的UIView,然后使用UIPanGestureRecognizer
实现触摸事件响应,运行效果:显示一个大于frame的UIImageView
#import "IVScrollView.h" @implementation IVScrollView + (Class)layerClass{ return [CAScrollLayer class]; } - (instancetype)initWithFrame:(CGRect)frame{ self = [super initWithFrame:frame]; if (self) { [self setup]; } return self; } - (void)awakeFromNib{ [super awakeFromNib]; [self setup]; } - (void)setup{ UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(pan:)]; [self addGestureRecognizer:pan]; } - (void)pan:(UIPanGestureRecognizer *)recognizer{ CGPoint moveBy = [recognizer translationInView:self]; CGPoint origin = self.bounds.origin; CGFloat offsetX = origin.x - moveBy.x; CGFloat offsetY = origin.y - moveBy.y; [(CAScrollLayer *)self.layer scrollPoint:CGPointMake(offsetX, offsetY)]; [recognizer setTranslation:CGPointZero inView:self]; } /* // Only override drawRect: if you perform custom drawing. // An empty implementation adversely affects performance during animation. - (void)drawRect:(CGRect)rect { // Drawing code } */ @end
使用方式:
IVScrollView *scrollView = [[IVScrollView alloc]initWithFrame:CGRectMake(0, 100, 300, 300)]; scrollView.backgroundColor = [UIColor redColor]; [self.view addSubview:scrollView]; UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 500, 500)]; imageView.image = [UIImage imageNamed:@"Accounts"]; [scrollView addSubview:imageView];
显示效果:

其他的一些方法和属性
- (void)scrollRectToVisible:(CGRect)r
滑动是的指定区域可见
@property(readonly) CGRect visibleRect;
获取当前的可视区域
CAEmitterLayer
CAEmitterLayer
是一个高性能的粒子引擎,被用来创建实时粒子动画,如:烟雾、火、雨雪等。
使用实例:
CAEmitterLayer *emitter = [CAEmitterLayer layer]; emitter.frame = CGRectMake(100, 100, 200, 200); [self.view.layer addSublayer:emitter]; emitter.renderMode = kCAEmitterLayerAdditive; emitter.emitterPosition = CGPointMake(emitter.frame.size.width * 0.5f, emitter.frame.size.height * 0.5f); CAEmitterCell *cell = [[CAEmitterCell alloc]init]; cell.contents = (__bridge id)[UIImage imageNamed:@"score"].CGImage; 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 = 50; cell.velocityRange = 50; cell.emissionRange = M_PI * 2.0; emitter.emitterCells = @[cell];
AVPlayerLayer
需要注意的是,AVPlayerLayer
与其上的Layer不同,并不是CoreAnimation框架的一部分,而是属于AVFoundation框架。可以用来播放视频,是MPMoivePlayer
的底层实现,提供了显示视频的底层控制。
使用方法:
- 直接使用
playerLayerWithPlayer:
方法创建一个已经绑定了视频播放器的图层 - 可以先创建一个图层,然后用
player
属性绑定一个AVPlayer
实例。
举例说明:
因为AVPlayerLayer
是CALayer
的子类,它继承了父类的所有的特性,可以添加边框、蒙版等特性。
NSURL *url = [[NSBundle mainBundle]URLForResource:@"life" withExtension:@"mp4"]; //NSString *path = [[NSBundle mainBundle]pathForResource:@"life" ofType:@"mp4"]; //NSURL *url = [NSURL URLWithString:path]; //这种方式获取的URL无法播放 AVPlayer *player = [AVPlayer playerWithURL:url]; AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player]; playerLayer.frame = CGRectMake(100, 100, 200, 200); [self.view.layer addSublayer:playerLayer]; playerLayer.backgroundColor = [UIColor redColor].CGColor; CATransform3D transform = CATransform3DIdentity; transform.m34 = -1.0/500.0; transform = CATransform3DRotate(transform, M_PI_4, 1, 1, 0); playerLayer.transform = transform; playerLayer.masksToBounds = YES; playerLayer.cornerRadius = 20.0; playerLayer.borderColor = [UIColor redColor].CGColor; playerLayer.borderWidth = 5.0f; [player play];
扩展:
导入资源如果无法使用,查看一下设置中的Build Phases > Copy Bundle Resources
是否已经将资源文件添加进去了,如果里面没有,那就需要在这里手动添加了。
CATiledLayer
可以使用CATiledLayer
来绘制一个很大的图片,因为由于内存的限制,将整个图片读取到内存中是很不明智的。
- (void)tiledLayer{ UIScrollView *scrollView = [[UIScrollView alloc]initWithFrame:CGRectMake(100, 100, 200, 200)]; scrollView.backgroundColor = [UIColor whiteColor]; [self.view addSubview:scrollView]; CATiledLayer *tileLayer = [CATiledLayer layer]; tileLayer.frame = CGRectMake(0, 0, 2048, 2048); tileLayer.delegate = self; tileLayer.contentsScale = [UIScreen mainScreen].scale;//以屏幕的原生分辨率来渲染 [scrollView.layer addSublayer:tileLayer]; scrollView.contentSize = tileLayer.frame.size; [tileLayer setNeedsDisplay]; } - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{ CGRect bounds = CGContextGetClipBoundingBox(ctx); UIImage *image = [UIImage imageNamed:@"media"]; UIGraphicsPushContext(ctx); [image drawInRect:bounds]; UIGraphicsPopContext(); }
CATransaction实现动画
先举例说明,改变一个CALayer的backgroundColor,发现并没有立即在屏幕体现出来,而是有一个平滑的过程。
代码:
- (void)changeColor{ self.redLayer = [CALayer layer]; self.redLayer.frame = CGRectMake(100, 100, 100, 100); [self.view.layer addSublayer:self.redLayer]; self.redLayer.backgroundColor = [UIColor redColor].CGColor; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ self.redLayer.backgroundColor = [UIColor greenColor].CGColor; // self.view.layer.backgroundColor = [UIColor greenColor].CGColor; }
效果:(效果展示不太明显,录屏问题)
这就是所谓的隐式动画,由CoreAnimation来决定如何并且何时去做动画。实际上动画的类型由图层行为决定,持续时间是由当前事务来设置的。而CATransaction
就是用来管理事务的类。
CATransaction
没有+alloc
和init
方法,使用+begin
和+commit
进行入栈和出栈管理。
示例,修改动画执行的时间
[CATransaction begin]; [CATransaction setAnimationDuration:2.0];//设置事务的动画时间,默认时间是0.25s [CATransaction setCompletionBlock:^{ NSLog(@"The Animation finish"); }];//设置动画结束之后的动作 self.redLayer.backgroundColor = [UIColor greenColor].CGColor; [CATransaction commit];
展示效果:
在UIView中有类似方法,
[UIView beginAnimations:@"" context:nil]; [UIView commitAnimations];
还有:
[UIView animateWithDuration:1.0 animations:^{ //do something here }];
他们本质上都是由CATransaction
来实现的。
可会发现一个问题,当我们对UIView关联的图层做同样的动画时,会瞬间切换到新值,而不会出现之前的过渡效果,UIView关联图层的动画被禁用了 (注:completionBlock还是会执行的)。
分析原因之前先了解一下隐式动画是如何实现的:
将改变属性时CALayer
自动应用的动画成为行为,当CALayer
的属性被修改时,他会调用-actionForKey:
方法,传递属性的名称,剩下的是下面的几步:
- 图层首先检测他是否有委托,并且是否是实现
CALayerDelegate
协议指定的-actionForLayer:forKey
方法,如果有,则直接调用并返回结果。 - 如果没有委托,或没有实现
-actionForLayer:forKey
方法,图层接着检查包含属性名称对应行为映射的actions
字典。 - 如果
actions
字典没有对应的属性,那么图层接着会在他的style
字典接着搜索属性名 - 最后,如果在
style
里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的defaultActionForKey:
方法。
所以,完成这一轮搜索之后,actionForKey:
要么返回空(这种情况下将不会有动画发生),要么是返回CAAction
协议定义的对象,最后CALayer
那这个结果对先前和当前的值做动画。
这也解释了UIKit是如何禁用隐式动画的:每个UIView对他所关联的图层都扮演一个委托,并且提供了actionForLayer:forKey
的实现方法,当不再一个动画块里实现的时候,UIView对所有的图层行为都返回nil
,但在动画block范围之内时候,他就返回一个非空值。
测试UIView的actionForLayer:forKey:
的实现
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. self.view.backgroundColor = [UIColor cyanColor]; self.redView = [[UIView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; self.redView.backgroundColor = [UIColor redColor]; [self.view addSubview:self.redView]; NSLog(@"outside: %@",[self.redView actionForLayer:self.redView.layer forKey:@"backgroundColor"]); [UIView animateWithDuration:1.0 animations:^{ NSLog(@"inside: %@",[self.redView actionForLayer:self.redView.layer forKey:@"backgroundColor"]); }]; //另一种实现方式 [UIView beginAnimations:nil context:nil]; NSLog(@"second inside: %@",[self.redView actionForLayer:self.redView.layer forKey:@"backgroundColor"]); [UIView commitAnimations]; }
打印结果为:
iOSFang[921:34878] outside: <null> iOSFang[921:34878] inside: <CABasicAnimation: 0x600000413d40> iOSFang[921:34878] second inside: <CABasicAnimation: 0x600000413f80>
当然,返回nil
并不是禁用隐式动画的唯一办法,CATransaction
有个方法叫+setDisableActions:
,可以用来对所有属性打开或者关闭隐式动画。
UIView使用动画的方式
- 使用UIView的动画函数
- 继承UIView,覆盖
-actionForLayer:forKey:
方法,或者直接创建一个显示动画
自定义动画行为
- (void)customLayerAnimation{ self.redLayer = [CALayer layer]; self.redLayer.frame = CGRectMake(100, 100, 100, 100); self.redLayer.backgroundColor = [UIColor redColor].CGColor; [self.view.layer addSublayer:self.redLayer]; CATransition *transition = [CATransition animation]; transition.type = kCATransitionPush; transition.subtype = kCATransitionFromLeft; self.redLayer.actions = @{ @"backgroundColor":transition, }; }
执行效果为:
presentationLayer图层
当改变一个图层的属性时,属性值是立刻更新的,但屏幕上并没有马上发生改变,只是定义了图层动画结束之后的值。
当前显示在屏幕上的属性值记录在一个叫做呈现图层的独立图层中,可以通过presentationLayer
来进行访问。这个呈现图层实际上是模型图层的复制,他的属性值代表了任何指定时刻当前外观效果,可以通过它来获取当前屏幕上真正显示出来的值。在呈现图层上调用modelLayer
获取它所依赖的CALayer(通常返回的是self)。