CALayer 绘图层


CALayer 绘图层 在 iOS 系统中,你能看得见摸得着的东西基本上都是 UIView,比如一个按钮、一个文本标签、一个文本输入框、一个图标等等,这些都是 UIView。其实 UIView 之所以能显示在屏幕上,完全是因为它内部的一个层。

1、CALayer 绘图层

  • 在 iOS 系统中,你能看得见摸得着的东西基本上都是 UIView,比如一个按钮、一个文本标签、一个文本输入框、一个图标等等,这些都是 UIView。其实 UIView 之所以能显示在屏幕上,完全是因为它内部的一个层。在创建 UIView 对象时,UIView 内部会自动创建一个层(即 CALayer 对象),通过 UIView 的 layer 属性可以访问这个层。当 UIView 需要显示到屏幕上时,会调用 drawRect: 方法进行绘图,并且会将所有内容绘制在自己的层上,绘图完毕后,系统会将层拷贝到屏幕上,于是就完成了 UIView 的显示。换句话说,UIView 本身不具备显示的功能,是它内部的层才有显示功能。
  • CALayer 的简单使用
    • CALayer 是被定义在 QuartzCore 框架中的,通过操作 CALayer 对象,可以很方便地调整 UIView 的一些界面属性,比如:阴影、圆角大小、边框宽度和颜色等。

2、基本绘图层属性设置

  • 1、设置阴影

    1
    2
    3
    4
    self.redView.layer.shadowOpacity = 1;                               // 阴影不透明度
    self.redView.layer.shadowOffset = CGSizeMake(10, 10); // 阴影偏移量
    self.redView.layer.shadowColor = [UIColor yellowColor].CGColor; // 阴影颜色
    self.redView.layer.shadowRadius = 10; // 阴影圆角半径
  • 2、圆角半径

    1
    self.redView.layer.cornerRadius = 75;                               // 主层半径
  • 3、边框

    1
    2
    self.redView.layer.borderWidth = 3;                                 // 边框宽度
    self.redView.layer.borderColor = [UIColor blueColor].CGColor; // 边框颜色
  • 4、imageView 圆角半径设置

    1
    2
    3
    4
    self.imageView.layer.cornerRadius = 75;                             // 主层半径

    // 超出主层边框的内容全部裁剪掉,image 在视图层上
    self.imageView.layer.masksToBounds = YES; // 是否对非主层裁剪

3、形变属性设置

3.1 视图形变

  • 1、单一形变

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 旋转
    /*
    (CGFloat angle) 旋转 45 度,需要输入的参数为弧度,45/180 * M_PI,1 度 = PI/180 弧度
    */
    [UIView animateWithDuration:1 animations:^{
    self.redView.transform = CGAffineTransformMakeRotation(0.25 * M_PI);
    }];

    // 缩放
    /*
    (CGFloat sx, CGFloat sy) (1, 2) 宽度和高度的放大倍数
    */
    [UIView animateWithDuration:1 animations:^{
    self.redView.transform = CGAffineTransformMakeScale(1, 2);
    }];

    // 平移
    /*
    (CGFloat tx, CGFloat ty) (100, 100) 水平和垂直方向的移动距离
    */
    [UIView animateWithDuration:1 animations:^{
    self.redView.transform = CGAffineTransformMakeTranslation(100, 100);
    }];
    • 效果

  • 2、 叠加形变

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 旋转 + 缩放
    [UIView animateWithDuration:1 animations:^{
    CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(0.25 * M_PI);
    self.redView.transform = CGAffineTransformScale(rotationTransform, 1.5, 1.5);
    }];

    // 旋转 + 平移
    [UIView animateWithDuration:1 animations:^{
    CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(0.25 * M_PI);
    self.redView.transform = CGAffineTransformTranslate(rotationTransform, 100, 0);
    }];

    // 缩放 + 平移
    [UIView animateWithDuration:1 animations:^{
    CGAffineTransform scaleTransform = CGAffineTransformMakeScale(1, 1.5);
    self.redView.transform = CGAffineTransformTranslate(scaleTransform, 100, 0);
    }];

    // 旋转 + 缩放 + 平移
    [UIView animateWithDuration:1 animations:^{
    CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(0.25 * M_PI);
    CGAffineTransform rotationScaleTransform = CGAffineTransformScale(rotationTransform, 1.5, 1.5);
    self.redView.transform = CGAffineTransformTranslate(rotationScaleTransform, 100, 0);
    }];
    • 效果

  • 3、累加形变

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 连续旋转
    [UIView animateWithDuration:1 animations:^{
    self.redView.transform = CGAffineTransformRotate(self.redView.transform, 0.25 * M_PI);
    }];

    // 连续缩放
    [UIView animateWithDuration:1 animations:^{
    self.redView.transform = CGAffineTransformScale(self.redView.transform, 1.5, 1.5);
    }];

    // 连续平移
    [UIView animateWithDuration:1 animations:^{
    self.redView.transform = CGAffineTransformTranslate(self.redView.transform, 50, 50);
    }];
    • 效果

  • 4、还原形变

    1
    2
    // 还原所有形变
    self.redView.transform = CGAffineTransformIdentity;

3.2 绘图层形变

  • 1、单一形变

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 旋转
    /*
    (CGFloat angle) 旋转 45 度,需要输入的参数为弧度,45/180 * M_PI,1 度 = PI/180 弧度
    */
    [UIView animateWithDuration:1 animations:^{
    self.redView.layer.affineTransform = CGAffineTransformMakeRotation(0.25 * M_PI);
    }];

    // 缩放
    /*
    (CGFloat sx, CGFloat sy) (1, 2) 宽度和高度的放大倍数
    */
    [UIView animateWithDuration:1 animations:^{
    self.redView.layer.affineTransform = CGAffineTransformMakeScale(1, 2);
    }];

    // 平移
    /*
    (CGFloat tx, CGFloat ty) (100, 100) 水平和垂直方向的移动距离
    */
    [UIView animateWithDuration:1 animations:^{
    self.redView.layer.affineTransform = CGAffineTransformMakeTranslation(100, 100);
    }];

    // 还原所有形变
    self.redView.layer.affineTransform = CGAffineTransformIdentity;
    • 效果

  • 2、快速进行绘图层形变,KVC

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 旋转
    [UIView animateWithDuration:1 animations:^{
    [self.redView.layer setValue:@(0.25 * M_PI) forKeyPath:@"transform.rotation"];
    }];

    // 缩放
    [UIView animateWithDuration:1 animations:^{
    [self.redView.layer setValue:@1.5 forKeyPath:@"transform.scale"];
    }];
    • 效果

  • 3、绘图层 3D 形变

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 旋转
    /*
    (CGFloat angle, CGFloat x, CGFloat y, CGFloat z) 旋转角度,x y z 轴的坐标,为 0 时在此轴上不旋转
    */
    [UIView animateWithDuration:1 animations:^{
    self.imageView.layer.transform = CATransform3DMakeRotation(M_PI, 0, 1, 0);
    }];

    // 缩放
    /*
    (CGFloat sx, CGFloat sy, CGFloat sz),x y z 轴的缩放倍数
    */
    [UIView animateWithDuration:1 animations:^{
    self.imageView.layer.transform = CATransform3DMakeScale(0.5, 0.5, 1);
    }];

    // 平移
    /*
    (CGFloat tx, CGFloat ty, CGFloat tz),x y z 轴的平移量
    */
    [UIView animateWithDuration:1 animations:^{
    self.imageView.layer.transform = CATransform3DMakeTranslation(100, 100, 0);
    }];
    • 效果

3.3 获取形变值

  • 获取旋转的角度

    1
    2
    // 根据 transform 获取旋转角度
    CGFloat angle = atan2(self.redView.transform.b, self.redView.transform.a);

4、创建新的绘图层

  • UIView 内部默认有个 CALayer 对象(层),通过 layer 属性可以访问这个层。要注意的是,这个默认的层不允许重新创建,但可以往层里面添加子层。UIView 可以通过 addSubview: 方法添加子视图,类似地,CALayer 可以通过 addSublayer: 方法添加子层。

  • 1、添加一个简单的图层

    1
    2
    3
    4
    5
    6
    7
    // 创建图层
    CALayer *myLayer = [CALayer layer];

    myLayer.frame = CGRectMake(100, 100, 200, 200);
    myLayer.backgroundColor = [UIColor redColor].CGColor;

    [self.view.layer addSublayer:myLayer];
    • 效果

  • 2、添加一个显示图片的图层

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 创建图层
    CALayer *myLayer = [CALayer layer];

    myLayer.frame = CGRectMake(100, 100, 200, 200);
    myLayer.backgroundColor = [UIColor redColor].CGColor;

    // 设置图层内容
    myLayer.contents = (id)[UIImage imageNamed:@"demo2.jpg"].CGImage;

    [self.view.layer addSublayer:myLayer];
    • 效果

  • 3、为什么 CALayer 中使用 CGColorRef 和 CGImageRef 这 2 种数据类型,而不用 UIColor 和 UIImage ?

    • 首先要知道:CALayer 是定义在 QuartzCore 框架中的;CGImageRef、CGColorRef 两种数据类型是定义在 CoreGraphics 框架中的;UIColor、UIImage 是定义在 UIKit 框架中的。
    • 其次,QuartzCore 框架和 CoreGraphics 框架是可以跨平台使用的,在 iOS 和 Mac OS X 上都能使用,但是 UIKit 只能在 iOS 中使用。
    • 因此,为了保证可移植性,QuartzCore 不能使用 UIImage、UIColor,只能使用 CGImageRef、CGColorRef。
    • 不过很多情况下,可以通过 UIKit 对象的特定方法,得到 CoreGraphics 对象,比如 UIImage 的 CGImage 方法可以返回一个 CGImageRef。
  • 4、UIView 和 CALayer 的选择

    • 细心的朋友不难发现,其实前面的 2 个效果不仅可以通过添加层来实现,还可以通过添加 UIView 来实现。比如,第 1 个红色的层可以用一个 UIView 来实现,第 2 个显示图片的层可以用一个 UIImageView 来实现。既然 CALayer 和 UIView 都能实现相同的显示效果,那究竟该选择谁好呢?
    • 其实,对比 CALayer,UIView 多了一个事件处理的功能。也就是说,CALayer 不能处理用户的触摸事件,而 UIView 可以。
    • 所以,如果显示出来的东西需要跟用户进行交互的话,用 UIView;如果不需要跟用户进行交互,用 UIView 或者 CALayer 都可以。
    • 当然,CALayer 的性能会高一些,因为它少了事件处理的功能,更加轻量级。
  • 5、UIView 和 CALayer 的关系

    • UIView 可以通过 subviews 属性访问所有的子视图,类似地,CALayer 也可以通过 sublayers 属性访问所有的子层。

    • UIView 可以通过 superview 属性访问父视图,类似地,CALayer 也可以通过 superlayer 属性访问父层。

    • 下面再看一张 UIView 和 CALayer 的关系图,如果两个 UIView 是父子关系,那么它们内部的 CALayer 也是父子关系。

5、绘图层隐式动画属性

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

  • 当对非 Root Layer 的部分属性进行相应的修改时,默认会自动产生一些动画效果,这些属性称为 Animatable Properties 可动画属性。

    animation39

  • 列举几个常见的 Animatable Properties:

    1
    2
    3
    4
    5
    6
    bounds          :用于设置 CALayer 的宽度和高度。修改这个属性会产生缩放动画。
    backgroundColor :用于设置 CALayer 的背景色。修改这个属性会产生背景色的渐变动画。
    position :用于设置 CALayer 的位置。修改这个属性会产生平移动画。比如:假设
    一开始 CALayer 的 position 为(100, 100),然后在某个时刻修改为
    (200, 200),那么整个 CALayer 就会在短时间内从 (100, 100) 这个
    位置平移到 (200, 200)
  • 1、隐式动画属性设置

    1
    2
    3
    4
    5
    6
    7
    self.myLayer.bounds = CGRectMake(0, 0, 100, 100);

    self.myLayer.backgroundColor = [UIColor greenColor].CGColor;

    self.myLayer.position = CGPointMake(arc4random_uniform(200) + 20, arc4random_uniform(400) + 50);

    self.myLayer.transform = CATransform3DMakeRotation(arc4random_uniform(360), 0, 0, 1);
    • 效果

  • 2、可以通过动画事务(CATransaction)关闭默认的隐式动画效果。

    1
    2
    3
    4
    5
    6
    [CATransaction begin];
    [CATransaction setDisableActions:YES];

    self.myLayer.position = CGPointMake(10, 10);

    [CATransaction commit];

6、绘图层 position 和 anchorPoint 属性

  • position 和 anchorPoint 属性都是 CGPoint 类型的。

    1
    2
    3
    position    :位置,可以用来设置 CALayer 在父层中的位置,它是以父层的左上角为坐标原点(0, 0)。
    anchorPoint :锚点,称为 "定位点",它决定着 CALayer 身上的哪个点会在 position 属性所指的位置。
    它的 x、y 取值范围都是 0~1,默认值为 (0.5, 0.5)。
  • 1、anchorPoint 为默认值(0.5, 0.5)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    CALayer *myLayer = [CALayer layer];

    myLayer.backgroundColor = [UIColor redColor].CGColor;
    myLayer.bounds = CGRectMake(0, 0, 100, 100);

    // 设置层的位置
    myLayer.position = CGPointMake(100, 100);

    [self.view.layer addSublayer:myLayer];
    • 设置了 myLayer 的 position 为(100, 100),又因为 anchorPoint 默认是(0.5, 0.5),所以最后的效果是 myLayer 的中点会在父层的(100, 100)位置。

  • 2、anchorPoint 为(0, 0)

    • 若将 anchorPoint 改为(0, 0),myLayer 的左上角会在(100, 100)位置。

      1
      myLayer.anchorPoint = CGPointMake(0, 0);

  • 3、anchorPoint 为(1, 1)

    • 若将 anchorPoint 改为(1, 1),myLayer 的右下角会在(100, 100)位置。

      1
      myLayer.anchorPoint = CGPointMake(1, 1);

  • 4、anchorPoint 为(0, 1)

    • 将 anchorPoint 改为(0, 1),myLayer 的左下角会在(100, 100)位置。

      1
      myLayer.anchorPoint = CGPointMake(0, 1);

7、自定义绘图层

7.1 自定义绘图层方法 1

  • 创建一个 CALayer 的子类,然后覆盖 drawInContext: 方法,使用 Quartz2D API 进行绘图。

  • QCLayer.h

    1
    2
    3
    @interface QCLayer : CALayer

    @end
  • QCLayer.m

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    @implementation QCLayer

    #pragma mark 绘制一个实心三角形

    - (void)drawInContext:(CGContextRef)ctx {

    // 设置为蓝色
    CGContextSetRGBFillColor(ctx, 0, 0, 1, 1);

    // 设置起点
    CGContextMoveToPoint(ctx, 50, 0);

    // 从 (50, 0) 连线到 (0, 100)
    CGContextAddLineToPoint(ctx, 0, 100);

    // 从 (0, 100) 连线到 (100, 100)
    CGContextAddLineToPoint(ctx, 100, 100);

    // 合并路径,连接起点和终点
    CGContextClosePath(ctx);

    // 绘制路径
    CGContextFillPath(ctx);
    }

    @end
  • ViewController.m

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    QCLayer *layer = [QCLayer layer];

    // 设置层的宽高
    layer.bounds = CGRectMake(0, 0, 100, 100);

    // 设置层的位置
    layer.position = CGPointMake(100, 100);

    // 开始绘制图层,需要调用这个方法,才会触发 drawInContext: 方法的调用,然后进行绘图
    [layer setNeedsDisplay];

    [self.view.layer addSublayer:layer];
  • 效果

    animation27

7.2 自定义绘图层方法 2

  • 设置 CALayer 的 delegate,然后让 delegate 实现 drawLayer:inContext: 方法,当 CALayer 需要绘图时,会调用 delegate 的 drawLayer:inContext: 方法进行绘图。

  • 这里要注意的是:不能再将某个 UIView 设置为 CALayer 的 delegate,因为 UIView 对象已经是它内部根层的 delegate,再次设置为其他层的 delegate 就会出问题。UIView 和它内部 CALayer 的默认关系图:

  • 创建新的层,设置 delegate,然后添加到控制器的 view 的 layer 中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    CALayer *layer = [CALayer layer];

    // 设置 delegate,这里的 self 是指控制器
    layer.delegate = self;

    // 设置层的宽高
    layer.bounds = CGRectMake(0, 0, 100, 100);

    // 设置层的位置
    layer.position = CGPointMake(100, 100);

    // 开始绘制图层,需要调用这个方法,才会通知 delegate 进行绘图
    [layer setNeedsDisplay];

    [self.view.layer addSublayer:layer];
  • 让 CALayer 的 delegate(前面设置的是控制器)实现 drawLayer:inContext: 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #pragma mark 画一个矩形框

    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {

    // 设置蓝色
    CGContextSetRGBStrokeColor(ctx, 0, 0, 1, 1);

    // 设置边框宽度
    CGContextSetLineWidth(ctx, 10);

    // 添加一个跟层一样大的矩形到路径中
    CGContextAddRect(ctx, layer.bounds);

    // 绘制路径
    CGContextStrokePath(ctx);
    }
  • 效果

7.3 UIView 的详细显示过程

  • 当 UIView 需要显示时,它内部的层会准备好一个 CGContextRef(图形上下文),然后调用 delegate(这里就是 UIView)的 drawLayer:inContext: 方法,并且传入已经准备好的 CGContextRef 对象。而 UIView 在 drawLayer:inContext: 方法中又会调用自己的 drawRect: 方法
  • 平时在 drawRect: 中通过 UIGraphicsGetCurrentContext() 获取的就是由层传入的 CGContextRef 对象,在 drawRect: 中完成的所有绘图都会填入层的 CGContextRef 中,然后被拷贝至屏幕。

8、渐变图层

  • 渐变图层 CAGradientLayer : CALayer

  • 添加渐变图层

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = self.imageView.bounds;

    // 设置透明度
    gradientLayer.opacity = 0.5;

    // 设置渐变颜色
    gradientLayer.colors = @[(id)[UIColor clearColor].CGColor, (id)[UIColor blackColor].CGColor];

    [self.imageView.layer addSublayer:gradientLayer];
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = self.imageView.bounds;

    // 设置透明度
    gradientLayer.opacity = 0.5;

    // 设置渐变颜色
    gradientLayer.colors = @[(id)[UIColor redColor].CGColor,
    (id)[UIColor greenColor].CGColor,
    (id)[UIColor yellowColor].CGColor];

    // 设置渐变定位点
    gradientLayer.locations = @[@0.1, @0.4, @0.8];

    // 设置渐变开始点,取值 0~1
    gradientLayer.startPoint = CGPointMake(0, 1);

    [self.imageView.layer addSublayer:gradientLayer];
    • 效果

9、复制图层

  • 复制图层 CAReplicatorLayer : CALayer,可以把图层里面所有子层复制

  • 添加复制图层

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    CAReplicatorLayer *repLayer = [CAReplicatorLayer layer];
    repLayer.frame = self.view.bounds;
    [self.view.layer addSublayer:repLayer];

    // 添加子层
    [repLayer addSublayer:self.imageView.layer];

    // 设置有多少个子层,包括原始层
    repLayer.instanceCount = 4;

    // 设置子层偏移量,不包括原始层,相对于原始层 x 偏移
    repLayer.instanceTransform = CATransform3DMakeTranslation(70, 0, 0);

    // 设置子层背景色
    repLayer.instanceColor = [UIColor greenColor].CGColor;

    // 设置子层阴影
    repLayer.instanceAlphaOffset = -0.1;
    repLayer.instanceRedOffset = -0.1;
    repLayer.instanceGreenOffset = -0.1;
    repLayer.instanceBlueOffset = -0.1;

    // 设置子层动画延迟时间,子层有动画时有效
    repLayer.instanceDelay = 0;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    CAReplicatorLayer *repLayer = [CAReplicatorLayer layer];
    repLayer.frame = self.view.bounds;
    [self.view.layer addSublayer:repLayer];

    // 添加子层
    CALayer *layer = [CALayer layer];
    layer.anchorPoint = CGPointMake(0.5, 1);
    layer.position = CGPointMake(100, 300);
    layer.bounds = CGRectMake(0, 0, 30, 150);
    layer.backgroundColor = [UIColor whiteColor].CGColor;
    [repLayer addSublayer:layer];

    // 添加子层动画
    CABasicAnimation *anim = [CABasicAnimation animation];
    anim.keyPath = @"transform.scale.y";
    anim.toValue = @0.1;
    anim.duration = 0.5;
    anim.repeatCount = MAXFLOAT;
    anim.autoreverses = YES;
    [layer addAnimation:anim forKey:nil];

    // 设置子层
    repLayer.instanceCount = 4;
    repLayer.instanceTransform = CATransform3DMakeTranslation(45, 0, 0);
    repLayer.instanceDelay = 0.1;
    repLayer.instanceColor = [UIColor greenColor].CGColor;
    repLayer.instanceGreenOffset = -0.3;
    • 效果