【iOS】——浅析CALayer

文章目录

  • 一、CALayer介绍
  • 二、UIview与CALayer
    • 1.区别
    • 2.联系
  • 三、CALayer的使用
    • 1.初始化方法
    • 2.常用属性
  • 四.CALayer坐标系
    • 1.position属性和anchorPoint属性
    • 2.position和anchorPoint的关系
    • 3.position、anchorPoint和frame的关系
  • 五、CALayerDelegate
  • 六、CALayer绘图机制
    • 1.绘图流程
    • 2.绘图方法
      • 1.图层代理绘制
      • 2.自定义图层drawInContext:方法
  • 七、CALayer处理点击事件
    • 1.方法一:convertPoint:
    • 2.方法二:hitTest:
  • 八、隐式动画


一、CALayer介绍

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

@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 的子类中进行,如下所示:

@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就被重置回到了初始状态。为了让其保持旋转状态,需要在加两句代码:

layer.fillMode=kCAFillModeForwards;
layer.removedOnCompletion=NO;

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

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

三、CALayer的使用

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

1.初始化方法

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

2.常用属性


//宽度和高度
@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属性代码格式如下:

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一些常用属性的展示代码:

- (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是否会发生改变呢,答案是不会。
//设置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);

请添加图片描述

 //设置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 的变化吗?答案也是不会。
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);

请添加图片描述

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属性来完成。这允许自定义图层内容的渲染逻辑。

下面是一段示例代码:

@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方法,否则绘制的内容无法显示。

下面是示例代码:

@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时将会裁剪掉超过图层边框范围的部分,而阴影正是属于那部分的内容,如果要实现带阴影效果的圆形图片裁剪可以通过使用两个大小一样的图层,下面的图层负责绘制阴影,上面的图层用来显示图片

示例代码如下:

@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方法将不会调用。

示例代码如下:

#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:

@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来转换坐标系,将在其父图层上的坐标转换为相对于图层自身的坐标,这样转换坐标系的方法还有以下几个:

//此方法用于将一个点的坐标从指定的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:

- (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)关闭。代码如下:

// 开始一个动画事务。
[CATransactionbegin];
//关闭隐式动画
[CATransactionsetDisableActions:YES];
//修改可动画属性
self.myview.layer.position= CGPointMake(10, 10);
//执行动画事务
[CATransactioncommit];

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/598890.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AGI|基于LangChain实现的三种高级RAG检索方法

一、前言 RAG(Retrieval-Augmented Generation)检索增强生成&#xff0c;是现如今基于企业私域知识的问答应用所使用的主流技术之一。相较于重新训练基于私域知识的大模型来说&#xff0c;RAG没有额外的预训练成本&#xff0c;且回答效果与之相当。 但在实际应用场景中&#xf…

容器Docker:轻量级虚拟化技术解析

引言 随着云计算和虚拟化技术的飞速发展&#xff0c;容器技术以其轻量级、高效、可移植的特性&#xff0c;逐渐成为了软件开发和部署的新宠。在众多容器技术中&#xff0c;Docker以其简单易用、功能强大的特点&#xff0c;赢得了广泛的关注和应用。本文将全面介绍Docker的基本概…

数据挖掘算法原理与实践:决策树

第2关&#xff1a;决策树算法原理 任务描述 本关任务&#xff1a;根据本关所学知识&#xff0c;完成 calcInfoGain 函数。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a; 信息熵&#xff1b;条件熵&#xff1b;信息增益。 信息熵 信息是个很抽象的概念。…

04-xss获取cookie实验

二、开发XSS服务器端 1、确认实验环境 攻击者服务器&#xff1a;192.168.74.134&#xff0c;将获取到cookie数据保存到该服务器的数据库中&#xff0c;运行PHP代码暴露一个接收Cookie的URL地址。 正常Web服务器&#xff1a;192.168.74.133&#xff0c;用于正常的用户访问的目…

Linux磁盘IO、网络IO、零拷贝详解

一、什么是I/O&#xff1f; 在计算机操作系统中&#xff0c;所谓的I/O就是输入&#xff08;input&#xff09;和输出&#xff08;output&#xff09;,也可以理解为读&#xff08;read&#xff09;和写&#xff08;write&#xff09;,针对不同的对象&#xff0c;I/O模式可以划分…

46. UE5 RPG 实现角色死亡效果

在上一篇文章中&#xff0c;我们实现了敌人受到攻击后会播放受击动画&#xff0c;并且还给角色设置了受击标签。并在角色受击时&#xff0c;在角色身上挂上受击标签&#xff0c;在c里&#xff0c;如果挂载了此标签&#xff0c;速度将降为0 。 受击有了&#xff0c;接下来我们将…

Compose 状态管理

文章目录 Compose 状态管理概述使用MutableStaterememberStatelessComposable & StatefulComposable状态提升rememberSaveable支持parceable不支持parceable 使用ViewModelViewModelProvider.Factory 使用Flow Compose 状态管理 概述 当应用程序的状态发生变化时&#xf…

如何与精益生产咨询公司合作,确保项目的成功?

随着竞争的白热化&#xff0c;企业为了提升生产效率和降低成本&#xff0c;纷纷寻求精益生产咨询公司的帮助。然而&#xff0c;与咨询公司合作并不是一蹴而就的事情&#xff0c;需要双方共同努力&#xff0c;才能确保项目的成功。那么&#xff0c;如何与精益生产咨询公司合作&a…

Unity射击游戏开发教程:(10)创建主界面

主界面开发 玩游戏时,主菜单是事后才想到要做的。实际上几乎每个游戏都有一个主界面。如果你点击打开游戏并立即开始游戏,你会感到非常惊讶。本文将讨论如何创建带有启动新游戏的交互式按钮的主界面/主菜单。 主菜单将是一个全新的场景。我们将添加一个 UI 图像元素,并在图像…

cookie,session,token

目的&#xff1a;解决用户登录状态 从一个简单的登录开始说起&#xff0c; 在我们访问bilibili的时候&#xff0c;第一次需要登录&#xff0c;但后续就不需要登录了&#xff0c;可以直接访问bilibili。 而且每次在页面请求服务器的资源都需要维持登录状态&#xff0c;如果没…

运维实施工程师之Linux服务器全套教程

一、Linux目录结构 1.1 基本介绍 Linux 的文件系统是采用级层式的树状目录结构&#xff0c;在此结构中的最上层是根目录“/”&#xff0c;然后在此目录下再创建其他的目录。 在 Linux 世界里&#xff0c;一切皆文件&#xff08;即使是一个硬件设备&#xff0c;也是使用文本来标…

暗区突围进不去/游戏无法启动/掉帧卡顿/报错的解决方法

暗区突围是一款高拟真硬核射击手游&#xff0c;打造了全新的沉浸式暗区战局体验&#xff0c;发行商是腾讯公司。这个游戏名词虽然看起来有些陌生&#xff0c;但其本身的玩法内核毫无疑问的是&#xff0c;这款游戏在画面质量和枪械操作方面&#xff0c;都是手游市场上同类游戏中…

文字转语音粤语怎么转换?6个软件教你快速进行文字转换语音

文字转语音粤语怎么转换&#xff1f;6个软件教你快速进行文字转换语音 当需要将文字转换为粤语语音时&#xff0c;可以使用多种工具和服务&#xff0c;这些工具可以帮助您快速而准确地实现这一目标。以下是六个非国内的语音转换软件&#xff0c;它们可以帮助您将文字转换为粤语…

【微磁学】对于现阶段微磁学仿真发展的思考1-理论篇

系列文章目录 对于现阶段微磁学仿真发展的思考1-理论篇 对于现阶段微磁学仿真发展的思考2-工具篇 文章目录 系列文章目录前言一、微磁学的数学区二、微磁学的物理区三、微磁学仿真现存的一些问题四、微磁学代码区&#xff1a;上手操作&#xff0c;理解更深入栗子1: 能量最小化…

人脸美妆SDK解决方案,自研人脸美妆方案

美妆已经成为视频内容中不可或缺的一部分。从拍摄到编辑&#xff0c;再到直播&#xff0c;美妆效果都能为视频内容增添魅力&#xff0c;吸引更多观众的眼球。为了满足企业对于高质量美妆效果的需求&#xff0c;美摄科技凭借多年的技术积累和创新精神&#xff0c;推出了全新的人…

Jmeter 中 CSV 如何参数化测试数据并实现自动断言

当我们使用Jmeter工具进行接口测试&#xff0c;可利用CSV Data Set Config配置元件&#xff0c;对测试数据进行参数化&#xff0c;循环读取csv文档中每一行测试用例数据&#xff0c;来实现接口自动化。此种情况下&#xff0c;很多测试工程师只会人工地查看响应结果来判断用例是…

局域网监控软件能干什么|有哪些好用的局域网监控软件

企业局域网已成为日常工作中不可或缺的一部分。 然而&#xff0c;网络环境的复杂性和员工上网行为的多样性&#xff0c;使得企业面临着诸多安全风险和管理挑战。 因此&#xff0c;高效局域网监控上网记录监测成为了企业保障信息安全和提升工作效率的重要手段。 高效局域网监控…

linux - 主次设备号自动申请

alloc_chrdev_region 原型如下&#xff0c;该函数向内核申请一个空闲的主设备号。 alloc_chrdev_region(&g_aputriger_dev, 0, APUTRIGER_MAX_NUM, "aputriger0"); 第四个参数是我们使用cat /proc/devices 看到的名称 /*** alloc_chrdev_region() - register a…

智慧交通系统:未来出行,从这里开始

随着城市化进程的加快&#xff0c;交通拥堵、事故频发、停车难等问题日益凸显&#xff0c;传统交通管理模式已难以满足现代社会的需求。智慧交通系统作为解决这些问题的关键&#xff0c;通过集成创新技术&#xff0c;实现交通管理的智能化、信息化&#xff0c;提高交通系统的运…

TC6291C 是一款电流模式升压型DC-DC转换器芯片

一般概述 TC6291C是一款电流模式升压型DC-DC转换器。其脉宽调制电路&#xff0c;内置0.2Q功率场效应管使这个调节器具有高功率效率。内部补偿网络也减少了多达6个的外部元件。误差信号放大器的同相输入端连接到0.6V精密基准电压&#xff0c;内部软启动功能可以减小瞬间突…