iOS-CALayer详解及应用

概览

在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看到iOS中如何使用图层精简非交互式绘图,如何通过核心动画创建基础动画、关键帧动画、动画组、转场动画,如何通过UIView的装饰方法对这些动画操作进行简化等。在今天的文章里您可以看到动画操作在iOS中是如何简单和高效,很多原来想做但是苦于没有思路的动画在iOS中将变得越发简单

一 CALayer
1.1 CALayer 简介

在介绍动画操作之前我们必须先来了解一个动画中常用的对象CALayer。CALayer包含在QuartzCore框架中,这是一个跨平台的框架,既可以用在iOS中又可以用在Mac OS X中。在使用Core Animation开发动画的本质就是将CALayer中的内容转化为位图从而供硬件操作,所以要熟练掌握动画操作必须先来熟悉CALayer。

当利用drawRect:方法绘图的本质就是绘制到了UIView的layer(属性)中。但是在Core Animation中我们操作更多的则不再是UIView而是直接面对CALayer。在UIView中有一个layer属性作为根图层,根图层上可以放其他子图层,在UIView中所有能够看到的内容都包含在layer中。

注意
  • 隐式属性动画的本质是这些属性的变动默认隐含了CABasicAnimation动画实现
  • 在CALayer中很少使用frame属性,因为frame本身不支持动画效果,通常使用boundsposition代替。
  • CALayer中透明度使用opacity表示而不是alpha;中心点使用position表示而不是center。
  • anchorPoint属性是图层的锚点,范围在(01,01)表示在x、y轴的比例,这个点永远可以同position(中心点)重合,当图层中心点固定后,调整anchorPoint即可达到调整图层显示位置的作用(因为它永远和position重合

下面通过一个简单的例子演示一下上面几个属性,程序初始化阶段我们定义一个正方形,但是圆角路径调整为正方形边长的一般,使其看起来是一个圆形,在点击屏幕的时候修改图层的属性形成动画效果(注意在程序中没有直接修改UIView的layer属性,因为根图层无法形成动画效果)

#import "CALayerDefaultAnimationVC.h"

#define kWidth 50.0

@interface CALayerDefaultAnimationVC ()
@property (nonatomic, strong) CALayer *thisLayer;
@end

@implementation CALayerDefaultAnimationVC

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    self.title = @"CALayer隐式动画";
    [self drawMyLayer];
}

#pragma mark - 绘制图层

- (void)drawMyLayer {
    CGSize size = [UIScreen mainScreen].bounds.size;
    //获取根图层
    CALayer* layer = [[CALayer alloc]init];
    //设置背景颜色,由于QuartzCore是跨平台框架,无法直接使用UIColor
    layer.backgroundColor = [UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0].CGColor;
    //设置中心点
    layer.position = CGPointMake(size.width/2, size.height/2 );
    //设置大小
    layer.bounds = CGRectMake(0, 0, kWidth, kWidth);
    //设置圆角,当圆角半径等于矩形的一半时看起来就是一个圆形
    layer.cornerRadius = kWidth/2;
    //设置阴影
    layer.shadowColor = [UIColor grayColor].CGColor;
    layer.shadowOffset = CGSizeMake(2, 2);
    layer.shadowOpacity = 0.9f;
    //设置锚点
    //layer.anchorPoint =  CGPointMake(0, 0);
    //layer.anchorPoint = CGPointMake(1, 1);
    [self.view.layer addSublayer:layer];
    self.thisLayer = layer;
}

#pragma mark - 点击放大

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint position = [touch locationInView:self.view];
    CALayer* layer = [self.view.layer.sublayers lastObject];
    CGFloat width = layer.bounds.size.width;
    
    if (width == kWidth) {
        width = 4 * kWidth;
        layer.backgroundColor = [UIColor colorWithRed:1. green:0.1 blue:0.7 alpha:1.0].CGColor;
    } else {
        width = kWidth;
        layer.backgroundColor = [UIColor colorWithRed:0 green:146/255. blue:1.0 alpha:1.0].CGColor;
    }
    
    layer.position = position;
    layer.bounds = CGRectMake(0, 0, width, width);
    NSLog(@"point:%@",NSStringFromCGPoint(position));
}

@end

运行结果如下

iOS-CALayer详解及应用_第1张图片
CoreAnimation.gif
CALayer绘图

当调用UIView的drawRect:方法绘制图形、图像,这种方式本质还是在图层中绘制,但是这里会着重介绍一下如何直接在图层中绘图。在图层中绘图的方式跟原来基本没有区别,只是drawRect:方法是由UIKit组件进行调用,因此里面可以使用一些UIKit封装的方法进行绘图,而直接绘制到图层的方法由于并非UIKit直接调用因此只能用原生的Core Graphics方法绘制。

图层绘图有两种方法,不管使用哪种方法绘制完必须调用图层的setNeedDisplay方法(注意是图层的方法,不是UIView的方法,前面我们介绍过UIView也有此方法)

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

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

#import "CALayerDrawInRectViewController.h"
#import 

#define PHOTO_HEIGHT 150

@interface CALayerDrawInRectViewController () 
@property (nonatomic, strong) CALayer *myLayer;
@end

@implementation CALayerDrawInRectViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.title = @"CALayer绘图";
    self.view.backgroundColor = [UIColor whiteColor];
    
    [self base];
}

- (void)base {
    //自定义图层
    CALayer *layer = [[CALayer alloc]init];
    layer.bounds = CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT);
    layer.position = CGPointMake(160, 200);
    layer.cornerRadius = PHOTO_HEIGHT/2.0;
    //注意仅仅设置圆角,对于图形而言可以正常显示,但是对于图层中绘制的图片无法正确显示
    //如果想要正确显示则必须设置masksToBounds=YES,剪切子图层
    layer.masksToBounds = YES;
    //阴影效果无法和masksToBounds同时使用,因为masksToBounds的目的就是剪切外边框,
    //而阴影效果刚好在外边框
    
    //layer.shadowColor=[UIColor grayColor].CGColor;
    //layer.shadowOffset=CGSizeMake(2, 2);
    //layer.shadowOpacity=1;
    
    //设置边框
    layer.borderColor = [UIColor cyanColor].CGColor;
    layer.borderWidth = 2;
    //设置图层代理
    layer.delegate = self;
    //设置图层到根图层
    [self.view.layer addSublayer:layer];
    //调用图层setNeedDisplay,否则代理方法不会被调用
    [layer setNeedsDisplay];
}

- (void)dealloc {
    self.myLayer.delegate = nil;
    NSLog(@"I'm Clearing");
}

#pragma mark - 绘制图形、图像到图层,注意参数中的ctx是图层的图形上下文,其中绘图位置也是相对图层而言的

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    CGContextSaveGState(ctx);
    //解决图形上文形变,图片倒立的问题
    
    CGContextScaleCTM(ctx, 1, -1);
    CGContextTranslateCTM(ctx, 0, -PHOTO_HEIGHT);
    UIImage* image = [UIImage imageNamed:@"girl"];
    CGContextDrawImage(ctx, CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT), image.CGImage);
    CGContextRestoreGState(ctx);
}

@end

运行结果如下

iOS-CALayer详解及应用_第2张图片
image.png

使用代理方法绘制图形、图像时在drawLayer:inContext:方法中可以通过事件参数获得绘制的图层和图形上下文。在这个方法中绘图时所有的位置都是相对于图层而言的,图形上下文指的也是当前图层的图形上下文。

需要注意的是上面代码中绘制图片圆形裁切效果时如果不设置masksToBounds是无法显示圆形,但是对于其他图形却没有这个限制。原因就是当绘制一张图片到图层上的时候会重新创建一个图层添加到当前图层,这样一来如果设置了圆角之后虽然底图层有圆角效果,但是子图层还是矩形,只有设置了masksToBounds为YES让子图层按底图层剪切才能显示圆角效果。同样的,有些朋友经常在网上提问说为什么使用UIImageView的layer设置圆角后图片无法显示圆角,只有设置masksToBounds才能出现效果,也是类似的问题。

扩展1–带阴影效果的圆形图片裁剪

如果设置了masksToBounds=YES之后确实可以显示图片圆角效果,但遗憾的是设置了这个属性之后就无法设置阴影效果。因为masksToBounds=YES就意味着外边框不能显示,而阴影恰恰作为外边框绘制的,这样两个设置就产生了矛盾。要解决这个问题不妨换个思路:使用两个大小一样的图层,下面的图层负责绘制阴影,上面的图层用来显示图片。

#import "CALayerDrawInRectViewController.h"
#import 

#define PHOTO_HEIGHT 150

@interface CALayerDrawInRectViewController () 
@property (nonatomic, strong) CALayer *myLayer;
@end

@implementation CALayerDrawInRectViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.title = @"CALayer绘图";
    
    [self extension];
}

- (void)extension {
    CGPoint position = CGPointMake(self.view.width * 0.5, self.view.height * 0.5);
    CGRect bounds = CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT);
    CGFloat cornerRadius = PHOTO_HEIGHT/2;
    CGFloat borderWidth = 2;
    
    //阴影图层
    CALayer* layerShadow = [[CALayer alloc]init];
    layerShadow.bounds = bounds;
    layerShadow.position = position;
    layerShadow.cornerRadius = cornerRadius;
    layerShadow.shadowColor = [UIColor grayColor].CGColor;
    layerShadow.shadowOffset = CGSizeMake(2, 1);
    layerShadow.borderColor = [UIColor grayColor].CGColor;
    layerShadow.shadowOpacity = 1;
    layerShadow.backgroundColor = [UIColor cyanColor].CGColor;
    layerShadow.borderWidth = borderWidth;
    [self.view.layer addSublayer:layerShadow];
    
    //容器图层
    CALayer* layer = [[CALayer alloc]init];
    layer.bounds = bounds;
    layer.position = position;
    layer.backgroundColor = [UIColor redColor].CGColor;
    layer.cornerRadius = cornerRadius;
    layer.masksToBounds = YES;
    layer.borderWidth = borderWidth;
    layer.borderColor = [UIColor whiteColor].CGColor;
    
    //设置图层代理
    layer.delegate = self;
    [self.view.layer addSublayer:layer];
    self.myLayer = layer;

    //调用图层setNeedDisplay,否则代理不会被调用
    [layer setNeedsDisplay];
}

- (void)dealloc {
    self.myLayer.delegate = nil;
    NSLog(@"I'm Clearing");
}

#pragma mark - 绘制图形、图像到图层,注意参数中的ctx是图层的图形上下文,其中绘图位置也是相对图层而言的

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    CGContextSaveGState(ctx);
    //解决图形上文形变,图片倒立的问题
    
    CGContextScaleCTM(ctx, 1, -1);
    CGContextTranslateCTM(ctx, 0, -PHOTO_HEIGHT);
    UIImage* image = [UIImage imageNamed:@"girl"];
    CGContextDrawImage(ctx, CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT), image.CGImage);
    CGContextRestoreGState(ctx);
}

@end
iOS-CALayer详解及应用_第3张图片
image.png
扩展2–图层的形变

从上面代码中大家不难发现使用Core Graphics绘制图片时会倒立显示,对图层的图形上下文进行了反转。在前一篇文章中也采用了类似的方法去解决这个问题,但是在那篇文章中也提到过如果直接让图像沿着x轴旋转180度同样可以达到正确显示的目的,只是当时的旋转靠图形上下文还无法绕x轴旋转。今天学习了图层之后,其实可以控制图层直接旋转而不用借助于图形上下文的形变操作,而且这么操作起来会更加简单和直观。对于上面的程序,只需要设置图层的transform属性即可。需要注意的是transform是CATransform3D类型,形变可以在三个维度上进行,使用方法和前面介绍的二维形变是类似的,而且都有对应的形变设置方法(如:CATransform3DMakeTranslation()、CATransform3DMakeScale()、CATransform3DMakeRotation())。下面的代码通过CATransform3DMakeRotation()方法在x轴旋转180度解决倒立问题:

- (void)extension2 {
    CGPoint position = CGPointMake(self.view.width * 0.5, self.view.height * 0.5);
    CGRect bounds = CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT);
    CGFloat cornerRadius = PHOTO_HEIGHT/2;
    CGFloat borderWidth = 2;
    
    //阴影图层
    CALayer* layerShadow = [[CALayer alloc]init];
    layerShadow.bounds = bounds;
    layerShadow.position = position;
    layerShadow.cornerRadius = cornerRadius;
    layerShadow.shadowColor = [UIColor grayColor].CGColor;
    layerShadow.shadowOffset = CGSizeMake(2, 1);
    layerShadow.borderColor = [UIColor grayColor].CGColor;
    layerShadow.shadowOpacity = 1;
    layerShadow.backgroundColor = [UIColor cyanColor].CGColor;
    layerShadow.borderWidth = borderWidth;
    [self.view.layer addSublayer:layerShadow];
    
    //容器图层
    CALayer *layer = [[CALayer alloc]    init];
    layer.bounds = bounds;
    layer.position = position;
    layer.backgroundColor = [UIColor redColor].CGColor;
    layer.cornerRadius = cornerRadius;
    layer.masksToBounds = YES;
    layer.borderColor = [UIColor whiteColor].CGColor;
    
    //设置内容(注意转换成CGImage)
    UIImage* iamge = [UIImage imageNamed:@"girl"];
    [layer setContents:(id)iamge.CGImage];
    
    //使用变换CATransform3D
    [layer setValue:@(M_PI) forKey:@"transform.rotation.x"];
    [self.view.layer addSublayer:layer];
}
iOS-CALayer详解及应用_第4张图片
image.png

事实上如果仅仅就显示一张图片在图层中当然没有必要那么麻烦,直接设置图层contents就可以了,不牵涉到绘图也就没有倒立的问题了。

既然如此为什么还大费周章的说形变呢,因为形变对于动画有特殊的意义。在动画开发中形变往往不是直接设置transform,而是通过keyPath进行设置。这种方法设置形变的本质和前面没有区别,只是利用了KVC可以动态修改其属性值而已,但是这种方式在动画中确实很常用的,因为它可以很方便的将几种形变组合到一起使用。同样是解决动画旋转问题,只要将前面的旋转代码改为下面的代码即可:

[layer setValue:@M_PI forKeyPath:@"transform.rotation.x"];

使用自定义图层绘图

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

前面的文章中曾经说过,在使用Quartz 2D在UIView中绘制图形的本质也是绘制到图层中,为了说明这个问题下面演示自定义图层绘图时没有直接在视图控制器中调用自定义图层,而是在一个UIView将自定义图层添加到UIView的根图层中(例子中的UIView跟自定义图层绘图没有直接关系)。从下面的代码中可以看到:UIView在显示时其根图层会自动创建一个CGContextRef(CALayer本质使用的是位图上下文),同时调用图层代理(UIView创建图层会自动设置图层代理为其自身)的draw: inContext:方法并将图形上下文作为参数传递给这个方法。而在UIView的draw:inContext:方法中会调用其drawRect:方法,在drawRect:方法中使用UIGraphicsGetCurrentContext()方法得到的上下文正是前面创建的上下文。

// CustomLayer.m
@implementation CustomLayer
- (void)drawInContext:(CGContextRef)ctx {
    CGContextSetRGBFillColor(ctx, 135.0/255.0, 232.0/255.0, 84.0/255.0, 1);
    CGContextSetRGBStrokeColor(ctx, 135.0/255.0, 232.0/255.0, 84.0/255.0, 1);
    
    //star drawing
    CGContextMoveToPoint(ctx, 94.5, 33.5);
    
    CGContextAddLineToPoint(ctx,104.02, 47.39);
    CGContextAddLineToPoint(ctx,120.18, 52.16);
    CGContextAddLineToPoint(ctx,109.91, 65.51);
    CGContextAddLineToPoint(ctx,110.37, 82.34);
    CGContextAddLineToPoint(ctx,94.5, 76.7);
    CGContextAddLineToPoint(ctx,78.63, 82.34);
    CGContextAddLineToPoint(ctx,79.09, 65.51);
    CGContextAddLineToPoint(ctx,68.82, 52.16);
    CGContextAddLineToPoint(ctx,84.98, 47.39);
    CGContextClosePath(ctx);
    
    CGContextDrawPath(ctx, kCGPathFillStroke);
}
@end

// CustomView.m
@implementation CustomView
- (instancetype)initWithFrame:(CGRect)frame {
    if (self=[super initWithFrame:frame]) {
        CustomLayer* layer = [[CustomLayer alloc]init];
        layer.bounds = CGRectMake(0, 0, 185, 185);
        layer.position = CGPointMake(160, 284);
        layer.backgroundColor = [UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0].CGColor;
        
        //显示图层
        [layer setNeedsDisplay];
        [self.layer addSublayer:layer];
    }
    return self;
}

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    [super drawLayer:layer inContext:ctx];
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    CustomView* view = [[CustomView alloc]initWithFrame:[UIScreen mainScreen].bounds];
    view.backgroundColor = [UIColor colorWithRed:249.0/255.0 green:249.0/255.0 blue:249/255.0 alpha:1];
    [self.view addSubview:view];
}
iOS-CALayer详解及应用_第5张图片
image.png

本文参考zmmzxxx的CALayer与iOS动画 讲解及使用,非常感谢。


  • 如有错误,欢迎指正,多多点赞,打赏更佳,您的支持是我写作的动力。

项目连接地址 - AnimationDemo

你可能感兴趣的:(iOS-CALayer详解及应用)