【iOS】——浅析CALayer

文章目录


一、CALayer介绍

在官方文档中CALayer是管理基于图像的内容并允许您对该内容执行动画的对象。通俗来说就是在iOS中我们能看到的所有的UIView对象例如文本框、按钮、输入框等等之所以能够显示到屏幕上就是因为该UIView对象内部有一个图层专门用来显示,也就是CALayer对象,通过访问layer属性便可以访问它的图层。

objectivec 复制代码
@property(nonatomic,readonly,strong)CALayer  *layer;    

每当我们创建一个UIView对象时并将其添加到视图层级后,UIKit 会为其自动创建并关联一个 CALayer(RootLayer)。当视图需要显示时,系统会触发视图的 layoutSubviews 方法(如有必要)进行布局调整,然后调用drawRect:方法进行绘图。绘制完成后,CALayer 的内容被更新,并通过渲染管线最终显示到屏幕上。也就是说UIView本身不具备显示功能,而是它内部的图层有显示功能。

二、UIview与CALayer

1.区别

  • UIView:继承自 UIResponder, 主要负责事件响应,属于基于 UIKit 框架
  • CALayer:继承自 NSObject, 负责图像渲染,属于 QuartzCore 框架

将图像渲染和事件响应这两个功能分别去实现而不让 UIView 具有直接具有图像渲染是因为

1.CALayer 所属的 QuartzCore 框架是可以跨平台使用的,在 iOS和MacOS 中都可以使用,但是UIView只能在iOS中使用,在MacOS中使用Application Kit,在这两个系统里,页面绘图框架是可以公用的,但是两个系统的交互方式却不相同,一个是通过触摸事件,另一个是通过鼠标和键盘。

2.UIView 的主要职责是负责接收并响应事件,而 CALayer 的主要职责是负责显示 UI。这里就遵循了软件工程中的"单一职责原则",使得每个组件专注于自己的核心任务,提高了代码的可读性、可维护性和可扩展性。

由于CALayer只涉及控件在屏幕上的显示而没有事件响应,因此当只需要显示控件而不需要响应事件的时候可以优先选择CALayer,避免不必要的开销。

UIView 中有两个属性与图层相关分别是layer属性和layerClass属性:

  • layer 属性返回的是 UIView 所持有的主 Layer(RootLayer) 实例,我们可以通过其来设置 UIView中 layer 的一些属性例如阴影、圆角、边框、背景颜色等;
  • layerClass属性 则返回 RootLayer 所使用的类,我们可以通过重写该属性,来让 UIView 使用不同的 CALayer例如CAShapeLayer、CATextLayer 等等。

重写 layerClass 属性通常在 UIView 的子类中进行,如下所示:

objectivec 复制代码
@interface MyCustomView : UIView

@end

@implementation MyCustomView

+ (Class)layerClass {
    return [MyCustomCALayerSubclass class];
}
@end

2.联系

UIView和CALayer是相互依赖的关系。UIView依赖于CALayer提供的内容,CALayer依赖UIView提供的容器来显示绘制的内容(对应的是backing store, 实际上一个bitmap类型的位图)

CALayer 本身构成了一个层次化的树形结构,这一结构与 UIView 的视图层级密切相关,它们分别可以有自己的SubLayer和SubView,并且可以向它的 RootLayer 上添加子 layer。

Layer 内部有三份layer tree,分别是:

  • layer tree(model tree):一般我们称模型树, 也就是各个树的节点的 model 信息, 比如常见的 frame,affineTransform, backgroundColor 等等, 这些 model 数据都是在开发中可以设置的, 我们任何对于view/layer 的修改都能反应在 model tree 中;
  • presentation tree:这是一个中间层,我们 APP 无法主动操作, 这个层内容是 iOS 系统在 Render Server中生成的;
  • render tree:这是直接对应于提交到 render server 上进行显示的树。

Model Tree代表CALayer的真实属性,Presentation Tree对应动画过程中的属性。无论动画进行中还是已经结束,Model Tree都不会发生变化,变化的是Presentation Tree。而动画结束后,Presentation Tree就被重置回到了初始状态。为了让其保持旋转状态,需要在加两句代码:

objectivec 复制代码
layer.fillMode=kCAFillModeForwards;
layer.removedOnCompletion=NO;


CALayer 是所有 layer 的基类,其派生类会有一些特定的功能,比如绘制文本的 CATextLayer、渐变效果的 CAGradientLayer 等等。种类如下图所示

我们通常见到的 layer 都是依附于一个 UIView,但是也有一些单独的 layer 不需要附加到 UIView 上,就可以直接在屏幕上显示内容,如 AVCaptureVideoPreviewLayer、CAShapeLayer 等。

三、CALayer的使用

前面提到CALayer用来进行内容的绘制和渲染,下面介绍下CALayer应该如何使用

1.初始化方法

objectivec 复制代码
//默认初始化方法
- (instancetype)init;
//类方法
+ (instancetype)layer;
//基于 coder 的初始化方法,从 storyboard、xib 文件或归档数据中解码恢复时,系统会使用此方法初始化 CALayer
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder;

2.常用属性

objectivec 复制代码
//宽度和高度
@property CGRect bounds;

//位置(默认指中点距离父图层原点的位置,具体由anchorPoint决定)
@property CGPoint position;

//锚点(x,y的范围都是0~1),用于定义图层在自身坐标系统中的旋转、缩放和倾斜等变换操作的参照点,默认值为 (0.5, 0.5),即图层的中心点。
@property CGPoint anchorPoint;

//背景颜色(CGColorRef类型)
@property CGColorRef backgroundColor;

//形变属性
@property CATransform3D transform;

//边框颜色(CGColorRef类型)
@property  CGColorRef  borderColor;

//边框宽度
@property CGFloat borderWidth;

//圆角半径
@property CGFloat cornerRadius;

//内容(比如设置为图片CGImageRef)
@property(retain) id contents;

//不透明度
@property float opacity;

//是否裁剪,如果为YES将会剪掉超出layer边框的部分(包括阴影)
@property BOOL masksToBounds;

如果要将图片作为layer的contents属性代码格式如下:

objectivec 复制代码
self.view.layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"123"].CGImage); // 跨框架赋值需要进行桥接

这里需要用到桥接,因为imageNamed:方法返回的是OC对象,遵循ARC规则
,而layer.contents接收的是Core Foundation对象,遵循MRC规则,这里用到__bridge 关键字就是告诉编译器进行"无内存管理语义"的类型转换。

注意:

contents 属性的类型为 id。在这种情况下,可以给 contents 属性赋予任何值,项目仍可以编译通过。但是在实践中,如果 content 的值不是 CGImage ,得到的图层将是空白的。

既然如此,为什么要将 contents 的属性类型定义为 id 而非 CGImage。这是因为在 Mac OS 系统中,该属性对 CGImage 和 NSImage 类型的值都起作用,而在 iOS 系统中,该属性只对 CGImage 起作用。

下面是一个关于layer一些常用属性的展示代码:

objectivec 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //创建CALayer对象
    self.myLayer = [[CALayer alloc] init];
    //设置CALayer对象的位置和大小
    self.myLayer.frame = CGRectMake(100, 300, 100, 100);
    //self.myLayer.position = CGPointMake(100, 300);
    //self.myLayer.bounds = CGRectMake(100, 100, 100, 100);
    //self.myLayer.anchorPoint = CGPointMake(0.5, 0.5);

    //设置背景颜色
    self.myLayer.backgroundColor = [UIColor whiteColor].CGColor;
    //设置阴影颜色
    self.myLayer.shadowColor = [UIColor grayColor].CGColor;
    //设置阴影偏移量
    self.myLayer.shadowOffset = CGSizeMake(10, 10);
    //设置阴影不透明度
    self.myLayer.shadowOpacity = 0.6;
    //设置边框宽度
    self.myLayer.borderWidth = 3.0;
    //设置边框颜色
    self.myLayer.borderColor = [UIColor blackColor].CGColor;
    //设置圆角半径
    self.myLayer.cornerRadius = 12;
    //设置是否裁剪
    self.myLayer.masksToBounds = NO;
    //设置内容
    self.myLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"Apple.jpg.png"].CGImage); // 跨框架赋值需要进行桥接

    //将对象添加到控制器的Layer
    [self.view.layer addSublayer: self.myLayer];
}

运行结果如下:

需要注意的是如果将 self.myLayer.masksToBounds设置为YES,那么就会将超过layer边框的部分裁剪掉,也就是说阴影部分就会消失。

如果将self.myLayer.frame = CGRectMake(100, 300, 100, 100);替换成 self.myLayer.position = CGPointMake(100, 300); self.myLayer.bounds = CGRectMake(100, 100, 100, 100);那么测试结果会和前面一样吗

可以看到layer的位置发生了改变,这是因为frame属性改变的是layer在父视图中的位置和大小,frame 表示图层左上角位于父图层原点 (通常是左上角)水平方向 100 点、垂直方向 300 点的位置,而bounds属性 只改变layer的大小不改变其位置,position属性 改变的是layer中心点在父视图中的位置,一个是layer左上角作为参考点,一个是layer中心点作为参考点因此位置不一样。

四.CALayer坐标系

1.position属性和anchorPoint属性

  • position属性 表示了layer的anchorPoint属性设置的锚点距离父图层左上角(0,0)的位置。其计算公式如下:

position.x = frame.origin.x + anchorPoint.x * bounds.size.width ;

position.y = frame.origin.y + anchorPoint.y * bounds.size.height 。

  • anchorPoint属性 设置的锚点是用于定义图层在自身坐标系统中的旋转、缩放和倾斜等变换操作的参照点,它是一个 CGPoint类型的值,其坐标范围是 [0, 1],表示图层内部坐标系统的相对位置。默认值为 (0.5, 0.5),即图层的中心点。它是相对于图层自身的坐标系统而言的,而非其父图层。这意味着 anchorPoint 的位置是基于图层的 bounds(即图层内部内容的大小)来确定的。

2.position和anchorPoint的关系

  1. 前面提到position表示了layer的anchorPoint属性设置的锚点距离父图层左上角(0,0)的位置,如果修改了anchorPoint属性的话,position是否会发生改变呢,答案是不会。
objectivec 复制代码
//设置CALayer对象的位置
    self.myLayer.frame = CGRectMake(100, 300, 100, 100);
    self.myLayer.anchorPoint = CGPointMake(0, 0);
    NSLog(@"x:%f y:%f",self.myLayer.position.x, self.myLayer.position.y);
objectivec 复制代码
 //设置CALayer对象的位置
    self.myLayer.frame = CGRectMake(100, 300, 100, 100);
    self.myLayer.anchorPoint = CGPointMake(1, 1);
    NSLog(@"self.myLayer.position.x:%f \n self.myLayer.positiony:%f",self.myLayer.position.x, self.myLayer.position.y);


可以看到虽然改变了anchorPoint但是position没有发生变化。

  1. 如果改变position的话,会导致 anchorPoint 的变化吗?答案也是不会。
objectivec 复制代码
self.myLayer.frame = CGRectMake(100, 300, 100, 100);
    self.myLayer.position = CGPointMake(100, 100);
    NSLog(@"self.myLayer.anchorPoint.x:%f \n self.myLayer.anchorPoint.y:%f",self.myLayer.anchorPoint.x, self.myLayer.anchorPoint.y);
objectivec 复制代码
self.myLayer.frame = CGRectMake(100, 300, 100, 100);
    self.myLayer.position = CGPointMake(100, 100);
    NSLog(@"self.myLayer.anchorPoint.x:%f \n self.myLayer.anchorPoint.y:%f",self.myLayer.anchorPoint.x, self.myLayer.anchorPoint.y);


可以看到修改了position但是anchorPoint没有改变,这是因为它是相对于图层自身的坐标系统而言的,取决于自身图层的大小。

3.position、anchorPoint和frame的关系

CALayerframe 在文档中被描述为是一个计算型属性,它是从 boundsanchorPointposition 的值中派生出来的。为此属性指定新值时,图层会更改其 position 和 bounds 属性以匹配您指定的矩形

那它们是如何决定 frame 的?根据图片可以套用如下公式:

frame.x = position.x - anchorPoint.x * bounds.size.width ;

frame.y = position.y - anchorPoint.y * bounds.size.height 。

这就解释了为什么修改 position 和 anchorPoint 会导致 frame 发生变化

因此,更改 anchorPoint 会直接影响图层在屏幕上显示的位置,除非同时调整 position 以保持视觉位置不变。假设有一个图层,其 position 为 (100, 100),anchorPoint 为默认的 (0.5, 0.5),且 bounds 为 (100, 100)。此时,图层的中心点位于父图层坐标系中的 (100, 100)。如果将 anchorPoint 更改为 (0, 0)(左上角),为了保持图层在屏幕上的视觉位置不变,需要将 position 调整为 (50, 50),这样图层的左上角仍保持在 (100, 100) 处。

注意:

如果修改了 frame 的值是会导致 position 发生变化的,因为 position 是基于父图层定义的;frame 的改变意味着它自身的位置在父图层中有所改变,position 也会因此改变。

但是修改了 frame 并不会导致 anchorPoint 发生变化,因为 anchorPoint 是基于自身图层定义的,无论外部怎么变,anchorPoint 都不会跟着变化。

五、CALayerDelegate

可以使用 delegate (CALayerDelegate) 对象来提供图层的内容,处理任何子图层的布局,并提供自定义操作以响应与图层相关的更改。如果图层是由 UIView 创建的,则该 UIView 对象通常会自动指定为图层的委托。跳转到CALayerDelegate定义发现其代理方法有以下这五个方法,且都是可选方法而非必选。所以delegate 只是另一种为图层提供处理内容的方式,并不是唯一的。UIView 的显示跟它图层委托没有太大关系。

  • - (void)displayLayer:(CALayer *)layer;
    当图层标记其内容为需要更新 (调用 setNeedsDisplay() 方法) 时,调用此方法。如果定义了此方法,应当在这里实现整个显示过程,通常通过设置contents属性来完成。这允许自定义图层内容的渲染逻辑。

下面是一段示例代码:

objectivec 复制代码
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //创建CALayer对象
    self.myLayer = [[CALayer alloc] init];
    //设置CALayer对象的位置
    self.myLayer.position = CGPointMake(200, 200);
    self.myLayer.bounds = CGRectMake(100, 100, 100, 100);
    //设置背景颜色
    self.myLayer.backgroundColor = [UIColor whiteColor].CGColor;
    //设置阴影颜色
    self.myLayer.shadowColor = [UIColor grayColor].CGColor;
    //设置阴影偏移量
    self.myLayer.shadowOffset = CGSizeMake(10, 10);
    //设置阴影不透明度
    self.myLayer.shadowOpacity = 0.6;
    //设置边框宽度
    self.myLayer.borderWidth = 3.0;
    //设置边框颜色
    self.myLayer.borderColor = [UIColor blackColor].CGColor;
    //设置圆角半径
    self.myLayer.cornerRadius = 12;
    //设置是否裁剪
    self.myLayer.masksToBounds = NO;
    //设置内容
    self.myLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"Apple.jpg.png"].CGImage); // 跨框架赋值需要进行桥接
    //设置代理
    self.myLayer.delegate = self;
    //将对象添加到控制器的Layer
    [self.view.layer addSublayer: self.myLayer];
    
   
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.myLayer.anchorPoint = CGPointMake(0.5, 0.5);
    self.myLayer.bounds = CGRectMake(100, 100, 200, 200);
    //更新图层
    [self.myLayer setNeedsDisplay];
    
}

- (void)displayLayer:(CALayer *)layer {
    self.myLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"AppleLogo.png"].CGImage);
}
@end

运行结果如下:

点击后更新图层

  • - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
    - (void)displayLayer:(CALayer *)layer 一样,但是可以使用图层的 CGContext也就是上下文 来实现显示的过程
  • - (void)layerWillDraw:(CALayer *)layer;

在默认的 -display 方法执行前调用,允许代理在调用 -drawLayer:inContext: 之前配置任何影响图层内容的图层状态,比如contentsFormat(内容格式)和opaque(是否不透明)。如果代理实现了 -displayLayer: 方法,则此方法不会被调用。

  • - (void)layoutSublayersOfLayer:(CALayer *)layer;
    在默认的 -layoutSublayers 实现中调用,且在系统布局之前。当发现边界发生变化并且其 sublayers 可能需要重新排列时(例如通过 frame 改变大小),将调用此方法。注意,如果代理方法被调用,系统布局将被忽略,从而允许代理完全控制子图层的布局过程。

1

  • - (nullable id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event;

当图层需要对特定事件(如动画)作出响应时,由默认的 -actionForKey: 方法调用。应当返回一个遵循CAAction协议的对象来定义该事件的行为。可以返回nil表示代理没有为此事件指定行为,或者返回[NSNull null]明确表示不进行进一步的搜索,即不调用+defaultActionForKey:方法。

六、CALayer绘图机制

1.绘图流程

下图是 CALayer 在渲染之前的流程:

  1. 首先调用[UIView setNeedsDisplay][view.layer setNeedsDisplay]:来给一个视图或者其对应的图层打上脏标记表示需要重新绘制,但此时它还显示原来的内容,等到下一轮 RunLoop 修改才会生效。
  2. 接着当图层需要重新绘制时,会调用display方法,这个方法负责更新图层的内容,然后检查图层的代理(delegate)是否响应displayLayer:方法:
  3. 如果图层有一个代理,并且代理实现了displayLayer:方法 (YES),那么系统会调用这个方法来异步绘制图层的内容,这意味着可以通过实现displayLayer:方法来自定义图层的绘制过程,这个过程是异步进行的,不会阻塞主线程。
  4. 如果代理不响应displayLayer:方法(NO),则进入"系统绘制流程"。在这种情况下,系统会按照默认的方式绘制图层的内容。


下面是系统绘制的流程:

  1. 系统绘制时, 会先创建 用于存储像素数据的缓存区域backing storage(CGContextRef),我们可以理解为 CGContextRef 上下文;这个上下文是绘制的基础,所有图形、颜色等绘制指令都会在这个上下文中执行。
  2. 判断 layer 是否有 delegate,然后进入到不同的渲染分支中去,但是最后无论哪两个分支, 都有 CAlayer 上传 backing store。
  3. 如果有 delegate,则会执行 [layer.delegate drawLayer:inContext],然后在这个方法中会调用 view 的 drawRect: 方法,也就是我们重写 view 的 drawRect: 方法才会被调用到;
  4. 如果没有 delegate,会调用 layer 的 drawInContext 方法,也就是我们可以重写的 layer 的该方法,此刻会被调用到;

drawRect: 方法是在 CPU 执行的, 在它执行完之后, 通过 context 将数据 (通常情况下这里的最终结果会是一个 bitmap, 类型是 CGImageRef) 写入 backing store, 通过 rendserver 交给 GPU 去渲染,将 backing store 中的 bitmap 数据显示在屏幕上。

下面是异步绘制的流程

  1. 把UIView 显示的内容(包括 UILabel 的文字,UIImageView 的图片等)绘制生成的位图(bitmap)在子线程完成。
  2. 然后在回到主线程把bitmap赋值给view.layer.content属性。
  1. 首先在主线程中调用[AsyncDrawingView setNeedsDisplay]来标记视图需要重新绘制。
  2. 接着当视图需要重绘时,会调用[CALayer display]方法。
  3. display方法会调用[AsyncDrawingView displayLayer:]方法,这个方法会在全局队列中异步执行,也就是进行异步绘制工作。
  4. 在异步绘制工作中,使用CGContextCreate()创建一个位图上下文,并使用Core Graphic API进行绘制操作。
  5. 完成绘制后,通过CGBitmapContextCreateImage()将位图上下文转换为图片。
  6. 最后切换回主线程,调用[CALayer setContents:]方法,将绘制好的图像设置为layer的contents。

2.绘图方法

CAlayer图层绘图有两种方法,不管使用哪种方法绘制完必须调用图层的setNeedDisplay方法,例如[self.view.layer setNeedDisplay];,下面是图层绘制的两种方法:

  • 通过图层代理drawLayer:inContext:方法绘制
  • 通过自定义图层drawInContext:方法

1.图层代理绘制

通过代理方法进行图层绘图只要指定图层的代理,然后在代理对象中重写-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx 方法即可。需要注意这个方法虽然是代理方法但是不用手动实现CALayerDelegate,因为CALayer定义中给NSObject做了分类扩展,所有的NSObject都包含这个方法。另外设置完代理后必须要调用图层的setNeedDisplay方法,否则绘制的内容无法显示。

下面是示例代码:

objectivec 复制代码
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //创建CALayer对象
    self.myLayer = [[CALayer alloc] init];
    //设置CALayer对象的位置
    self.myLayer.position = CGPointMake(200, 200);
    self.myLayer.bounds = CGRectMake(100, 100, 100, 100);
    //设置背景颜色
    self.myLayer.backgroundColor = [UIColor whiteColor].CGColor;
    //设置阴影颜色
    self.myLayer.shadowColor = [UIColor grayColor].CGColor;
    //设置阴影偏移量
    self.myLayer.shadowOffset = CGSizeMake(10, 10);
    //设置阴影不透明度
    self.myLayer.shadowOpacity = 0.6;
    //设置边框宽度
    self.myLayer.borderWidth = 3.0;
    //设置边框颜色
    self.myLayer.borderColor = [UIColor blackColor].CGColor;
    //设置圆角半径
    self.myLayer.cornerRadius = 12;
    //设置是否裁剪
    self.myLayer.masksToBounds = NO;
    //设置代理
    self.myLayer.delegate = self;
    //将对象添加到控制器的Layer
    [self.view.layer addSublayer: self.myLayer];
    [self.myLayer setNeedsDisplay];
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    CGContextSaveGState(ctx);
    //解决图形上文形变,图片倒立的问题
    
    CGContextScaleCTM(ctx, 1, -1);
    CGContextTranslateCTM(ctx, 0, -100);
    UIImage* image = [UIImage imageNamed:@"Apple.jpg.png"];
    CGContextDrawImage(ctx, CGRectMake(0, 0, 100, 100), image.CGImage);
    CGContextRestoreGState(ctx);
}

实现带阴影效果的圆形图片裁剪

我们知道当maskToBounds设置为YES时将会裁剪掉超过图层边框范围的部分,而阴影正是属于那部分的内容,如果要实现带阴影效果的圆形图片裁剪可以通过使用两个大小一样的图层,下面的图层负责绘制阴影,上面的图层用来显示图片

示例代码如下:

objectivec 复制代码
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //创建CALayer对象
    [self shadowLayerCreate];
    [self myLayerCreate];
}
- (void)shadowLayerCreate {
    CALayer* layerShadow = [[CALayer alloc]init];
        layerShadow.bounds = CGRectMake(100, 100, 100, 100);
        layerShadow.position = CGPointMake(200, 200);
        layerShadow.cornerRadius = layerShadow.bounds.size.width / 2;
        layerShadow.shadowColor = [UIColor grayColor].CGColor;
        layerShadow.shadowOffset = CGSizeMake(2, 1);
        layerShadow.borderColor = [UIColor grayColor].CGColor;
        layerShadow.shadowOpacity = 1;
        layerShadow.backgroundColor = [UIColor blackColor].CGColor;
        layerShadow.borderWidth = 3.0;
        [self.view.layer addSublayer:layerShadow];
}
- (void)myLayerCreate {
    self.myLayer = [[CALayer alloc] init];
    //设置CALayer对象的位置
    self.myLayer.position = CGPointMake(200, 200);
    self.myLayer.bounds = CGRectMake(100, 100, 100, 100);
    //设置背景颜色
    self.myLayer.backgroundColor = [UIColor whiteColor].CGColor;
    //设置边框宽度
    self.myLayer.borderWidth = 3.0;
    //设置边框颜色
    self.myLayer.borderColor = [UIColor blackColor].CGColor;
    //设置圆角半径
    self.myLayer.cornerRadius = self.myLayer.bounds.size.width / 2;
    //设置是否裁剪
    self.myLayer.masksToBounds = YES;
    //设置内容
    self.myLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"Apple.jpg.png"].CGImage); // 跨框架赋值需要进行桥接
    //设置代理
    self.myLayer.delegate = self;
    //将对象添加到控制器的Layer
    [self.view.layer addSublayer: self.myLayer];
}

需要注意的是先添加阴影layer再添加自己的layer负责阴影layer会覆盖在自己的layer之上。

2.自定义图层drawInContext:方法

在自定义图层中绘图时只要编写一个继承于CALayer的类然后在drawInContext:中绘图即可。要显示图层中绘制的内容也要调用图层的setNeedDisplay方法,否则drawInContext方法将不会调用。

示例代码如下:

objectivec 复制代码
#import <QuartzCore/QuartzCore.h>

NS_ASSUME_NONNULL_BEGIN

@interface CustomLayer : CALayer

@end

NS_ASSUME_NONNULL_END

#import "CustomLayer.h"

@implementation CustomLayer

// 初始化方法,如果有自定义属性,需要在这里处理

- (instancetype)init {
    self = [super init];
    if (self) {
        // 初始化自定义设置,如需要
    }
   return self;
}

- (void)drawInContext:(CGContextRef)ctx {
    // 设置绘图颜色、线宽等属性
    CGContextSetRGBFillColor(ctx, 1.0, 0.0, 0.0, 1.0); // 设置填充颜色为红色
    CGContextSetRGBStrokeColor(ctx, 0.0, 0.0, 1.0, 1.0); // 设置描边颜色为蓝色
    CGContextSetLineWidth(ctx, 2.0); // 设置线宽为2.0

    // 自定义绘图命令
    CGContextAddEllipseInRect(ctx, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height)); // 绘制一个椭圆,充满整个图层边界
    CGContextDrawPath(ctx, kCGPathFillStroke); // 填充并描边路径
}

@end

#import <UIKit/UIKit.h>
#import "CustomLayer.h"
@interface ViewController : UIViewController<CALayerDelegate>

@property (strong, nonatomic) CustomLayer *customLayer;

@end
#import "ViewController.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    // 创建自定义图层实例
    self.customLayer = [[CustomLayer alloc] init];
   // 设置图层的frame
   self.customLayer.frame = CGRectMake(50, 50, 200, 200);

   // 添加自定义图层到视图的layer上
   [self.view.layer addSublayer:self.customLayer];
    
}

七、CALayer处理点击事件

前面提到CAlayer继承于继承自 NSObject, 而不是UIResponder类,因此本身不具备响应事件,但是依然有两种方法可以帮助我们实现捕捉并且处理CALayer的点击事件。

1.方法一:convertPoint:

objectivec 复制代码
@interface ViewController : UIViewController
@property (nonatomic, strong) CALayer *whiteAppleLayer;
@property (nonatomic, strong) CALayer *blackAppleLayer;

@end

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    self.whiteAppleLayer = [CALayer layer];
    self.whiteAppleLayer.frame = CGRectMake(100, 100, 200, 200);
    self.whiteAppleLayer.backgroundColor = [UIColor whiteColor].CGColor;
    self.whiteAppleLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"AppleLogo.png"].CGImage);
    [self.view.layer addSublayer:self.whiteAppleLayer];
    
    self.blackAppleLayer = [CALayer layer];
    self.blackAppleLayer.frame = CGRectMake(100, 300, 200, 200);
    self.blackAppleLayer.backgroundColor = [UIColor blackColor].CGColor;
    self.blackAppleLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"Apple.jpg.png"].CGImage);
    [self.view.layer addSublayer:self.blackAppleLayer];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    CGPoint point = [[touches anyObject] locationInView:self.view];
    CGPoint whitePoint = [self.whiteAppleLayer convertPoint:point fromLayer:self.view.layer];
    CGPoint blackPoint = [self.blackAppleLayer convertPoint:point fromLayer:self.view.layer];
    if ([self.whiteAppleLayer containsPoint:whitePoint]) {
        NSLog(@"whiteApple");
    }
    if ([self.blackAppleLayer containsPoint:blackPoint]) {
        NSLog(@"blackApple");
    } 
}
@end

点击白色苹果layer时会打印whiteApple

点击黑色苹果layer后会打印blackApple

首先使用locationInView方法获取到点击的点在view(默认为根视图)上的坐标。接着通过convertPoint:fromLayer :方法传入一个CGPoint来转换坐标系,将在其父图层上的坐标转换为相对于图层自身的坐标,这样转换坐标系的方法还有以下几个:

objectivec 复制代码
//此方法用于将一个点的坐标从指定的layer坐标系转换到当前调用该方法的图层的坐标系中。
- (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer; 
 //与前一个方法相反,此方法将一个点的坐标从当前调用该方法的图层的坐标系转换到指定的layer坐标系中。
- (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer *)layer; 
//转换一个矩形区域的坐标从指定的layer图层坐标系到当前调用该方法的图层坐标系中。
- (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer *)layer;
//将一个矩形区域的坐标从当前调用该方法的图层坐标系转换到指定的layer图层坐标系中。
- (CGRect)convertRect:(CGRect)rect toLayer:(CALayer *)layer;

得到触摸点相对于图层自身的坐标之后,调用containsPoint:方法。containsPoint:方法传入一个CGPoint类型参数,如果这个点在图层的frame内,则返回YES,否则返回NO。这样,就实现了对CALayer点击事件的处理。

为什么确定触摸点是否落在某个子图层上,就需要转换坐标系呢

因为子图层可能有自己独立的位置、缩放或旋转变换。换句话说,子图层的坐标原点与根视图或父视图的坐标原点可能不一致。为了准确判断触摸点是否在某个子图层内部,就需要将触摸点的坐标从全局坐标系(通常是窗口或根视图的坐标系)转换到该子图层的本地坐标系。这个转换过程考虑了子图层的所有平移、旋转和缩放变换。

2.方法二:hitTest:

objectivec 复制代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    CGPoint point = [[touches anyObject] locationInView:self.view];
    CALayer *layer = [self.view.layer hitTest:point];
    if (layer == self.whiteAppleLayer) {
        NSLog(@"whiteApple");
    }else if (layer == self.blackAppleLayer){
        NSLog(@"blackApple");
    }
}

hitTest:同样传入一个CGPoint类型参数,但它的返回值不是BOOL类型,而是图层本身。如果点击的位置在最外层图层之外,则返回nil。

八、隐式动画

每一个UIView内部都默认关联着一个CALayer,我们可称这个Layer为RootLayer(根层),所有的非RootLayer,也就是手动创建的CALayer对象,都存在着隐式动画。

当对非RootLayer的部分属性进行修改时,默认会自动产生一些动画效果而这些属性称为AnimatableProperties(可动画属性)。这个就是隐式动画

下面是几个常见的AnimatableProperties:

        bounds:用于设置CALayer的宽度和高度。修改这个属性会产生缩放动画。
        backgroundColor:用于设置CALayer的背景色。修改这个属性会产生背景色的渐变动画。
        position:用于设置CALayer的位置。修改这个属性会产生平移动画。

如果想关闭隐式动画,可以通过动画事务(CATransaction)关闭。代码如下:

objectivec 复制代码
// 开始一个动画事务。
[CATransactionbegin];
//关闭隐式动画
[CATransactionsetDisableActions:YES];
//修改可动画属性
self.myview.layer.position= CGPointMake(10, 10);
//执行动画事务
[CATransactioncommit];
相关推荐
zmd-zk1 小时前
flink学习(2)——wordcount案例
大数据·开发语言·学习·flink
Chef_Chen1 小时前
从0开始学习机器学习--Day33--机器学习阶段总结
人工智能·学习·机器学习
hopetomorrow2 小时前
学习路之压力测试--jmeter安装教程
学习·jmeter·压力测试
hopetomorrow2 小时前
学习路之PHP--使用GROUP BY 发生错误 SELECT list is not in GROUP BY clause .......... 解决
开发语言·学习·php
/**书香门第*/2 小时前
Cocos creator 3.8 支持的动画 7
学习·游戏·游戏引擎·游戏程序·cocos2d
程序猿看视界2 小时前
如何在 UniApp 中实现 iOS 版本更新检测
ios·uniapp·版本更新
美式小田3 小时前
单片机学习笔记 9. 8×8LED点阵屏
笔记·单片机·嵌入式硬件·学习
猫爪笔记3 小时前
前端:HTML (学习笔记)【2】
前端·笔记·学习·html
-一杯为品-4 小时前
【51单片机】程序实验5&6.独立按键-矩阵按键
c语言·笔记·学习·51单片机·硬件工程
风尚云网4 小时前
风尚云网前端学习:一个简易前端新手友好的HTML5页面布局与样式设计
前端·css·学习·html·html5·风尚云网