概述
这里主要就iOS中如何使用图层精简非交互式绘图,如何通过核心动画创建基础动画、关键帧动画、动画组、转场动画,如何通过UIView的装饰方法对这些动画操作进行简化等做一些介绍。
CALayer
首先我们需要了解下动画中常用的对象CALayer,CALayer包含在QuartzCore框架中,这是一个跨平台的框架,既可以用在iOS中又可以用在Mac OS X中。
在使用Core Animation开发动画的本质就是将CALayer中的内容转化为位图从而供硬件操作,所以要熟练掌握动画操作必须先来熟悉CALayer。
CALayer和UIView的关系,在UIView中有一个layer属性作为根图层,根图层上可以放其他子图层,在UIView中所有能够看到的内容都包含在layer中。
1.CALayer的属性
在iOS开发中使用每一个对象之前,要了解这个对象有哪些属性可以给我们使用。如下图:
补充说明:
- 在CALayer中很少使用frame属性,因为frame本身不支持动画效果,通常使用bounds和position代替。
- CALayer中透明度使用opacity表示而不是alpha;中心点使用position表示而不是center。
- anchorPoint属性是图层的锚点,范围在(01,01)表示在x、y轴的比例,这个点永远可以同position(中心点)重合,当图层中心点固定后,调整anchorPoint即可达到调整图层显示位置的作用(因为它永远和position重合)
2.使用CALayer绘图
这里主要介绍用原生的Core Graphics方法直接绘制到图层的方法。图层绘图有两种方法,不管使用哪种方法绘制完必须调用图层的setNeedDisplay方法(注意是图层的方法,不是UIView的方法)
- 通过图层代理drawLayer: inContext:方法绘制
- 通过自定义图层drawInContext:方法绘制
使用代理方法绘图
通过代理方法进行图层绘图只要指定图层的代理,然后在代理对象中重写如下方法即可。
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
需要注意这个方法虽然是代理方法但是不用手动实现CALayerDelegate,因为CALayer定义中给NSObject做了分类扩展,所有的NSObject都包含这个方法。另外设置完代理后必须要调用图层的setNeedDisplay方法,否则绘制的内容无法显示
下面的代码演示了在一个自定义图层绘制一张图像并将图像设置成圆形,这种效果在很多应用中很常见,如最新版的手机QQ头像就是这种效果:
//
// CALayerViewController.m
// CALayer
//
// Created by fly on 2018/2/24.
// Copyright © 2018年 fly. All rights reserved.
//
#import "CALayerViewController.h"
@interface CALayerViewController ()
@end
@implementation CALayerViewController
- (void)viewDidLoad {
[super viewDidLoad];
//自定义图层
CALayer *layer=[[CALayer alloc]init];
layer.bounds=CGRectMake(0, 0, 150, 150);
layer.position=CGPointMake(160, 200);
layer.backgroundColor=[UIColor redColor].CGColor;
layer.cornerRadius=150/2;
//注意仅仅设置圆角,对于图形而言可以正常显示,但是对于图层中绘制的图片无法正确显示
//如果想要正确显示则必须设置masksToBounds=YES,剪切子图层
layer.masksToBounds=YES;
//阴影效果无法和masksToBounds同时使用,因为masksToBounds的目的就是剪切外边框,
//设置边框
layer.borderColor=[UIColor whiteColor].CGColor;
layer.borderWidth=2;
//设置图层代理
layer.delegate=self;
//添加图层到根图层
[self.view.layer addSublayer:layer];
//调用图层setNeedDisplay,否则代理方法不会被调用
[layer setNeedsDisplay];
}
#pragma mark 绘制图形、图像到图层,注意参数中的ctx是图层的图形上下文,其中绘图位置也是相对图层而言的
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
// NSLog(@"%@",layer);//这个图层正是上面定义的图层
// 开始
CGContextSaveGState(ctx);
//图形上下文形变,解决图片倒立的问题
CGContextScaleCTM(ctx, 1, -1);
CGContextTranslateCTM(ctx, 0, -150);
UIImage *image=[UIImage imageNamed:@"thumb_IMG_0705_1024.jpg"];
//注意这个位置是相对于图层而言的不是屏幕
CGContextDrawImage(ctx, CGRectMake(0, 0, 150, 150), image.CGImage);
// 结束
CGContextRestoreGState(ctx);
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
结果如图:
注意:
使用代理方法绘制图形、图像时在drawLayer:inContext:方法中可以通过事件参数获得绘制的图层和图形上下文。在这个方法中绘图时所有的位置都是相对于图层而言的,图形上下文指的也是当前图层的图形上下文。
需要注意的是上面代码中绘制图片圆形裁切效果时如果不设置masksToBounds是无法显示圆形,但是对于其他图形却没有这个限制。原因就是当绘制一张图片到图层上的时候会重新创建一个图层添加到当前图层,这样一来如果设置了圆角之后虽然底图层有圆角效果,但是子图层还是矩形,只有设置了masksToBounds为YES让子图层按底图层剪切才能显示圆角效果。同样的,有些朋友经常在网上提问说为什么使用UIImageView的layer设置圆角后图片无法显示圆角,只有设置masksToBounds才能出现效果,也是类似的问题。
扩展1--带阴影效果的圆形图片裁切
如果设置了masksToBounds=YES之后确实可以显示图片圆角效果,但是设置了这个属性之后就无法设置阴影效果。
因为masksToBounds=YES就意味着外边框不能显示,而阴影恰恰作为外边框绘制的,这样两个设置就产生了矛盾。要解决这个问题不妨换个思路:使用两个大小一样的图层,下面的图层负责绘制阴影,上面的图层用来显示图片。
//
// CALayerViewController.m
// CALayer
//
// Created by fly on 2018/2/24.
// Copyright © 2018年 fly. All rights reserved.
//
#import "CALayerViewController.h"
@interface CALayerViewController ()
@end
@implementation CALayerViewController
- (void)viewDidLoad {
[super viewDidLoad];
CGPoint position= CGPointMake(160, 200);
CGRect bounds=CGRectMake(0, 0, 150, 150);
CGFloat cornerRadius=150/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.shadowOpacity=1;
layerShadow.borderColor=[UIColor whiteColor].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;
layer.borderWidth=borderWidth;
//设置图层代理
layer.delegate=self;
//添加图层到根图层
[self.view.layer addSublayer:layer];
//调用图层setNeedDisplay,否则代理方法不会被调用
[layer setNeedsDisplay];
}
#pragma mark 绘制图形、图像到图层,注意参数中的ctx是图层的图形上下文,其中绘图位置也是相对图层而言的
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
// NSLog(@"%@",layer);//这个图层正是上面定义的图层
CGContextSaveGState(ctx);
//图形上下文形变,解决图片倒立的问题
CGContextScaleCTM(ctx, 1, -1);
CGContextTranslateCTM(ctx, 0, -150);
UIImage *image=[UIImage imageNamed:@"thumb_IMG_0705_1024.jpg"];
//注意这个位置是相对于图层而言的不是屏幕
CGContextDrawImage(ctx, CGRectMake(0, 0, 150, 150), image.CGImage);
// CGContextFillRect(ctx, CGRectMake(0, 0, 100, 100));
// CGContextDrawPath(ctx, kCGPathFillStroke);
CGContextRestoreGState(ctx);
}
@end
运行结果:
扩展2--图层的形变
从上面代码中大家不难发现使用Core Graphics绘制图片时会倒立显示,对图层的图形上下文进行了反转。
其实可以控制图层直接旋转而不用借助于图形上下文的形变操作,而且这么操作起来会更加简单和直观。对于上面的程序,只需要设置图层的transform属性即可。需要注意的是transform是CATransform3D类型,形变可以在三个维度上进行,使用方法和前面介绍的二维形变是类似的,而且都有对应的形变设置方法(如:CATransform3DMakeTranslation()、CATransform3DMakeScale()、CATransform3DMakeRotation())。下面的代码通过CATransform3DMakeRotation()方法在x轴旋转180度解决倒立问题:
- (void)viewDidLoad {
[super viewDidLoad];
CGPoint position= CGPointMake(160, 200);
CGRect bounds=CGRectMake(0, 0, 150, 150);
CGFloat cornerRadius=150/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.shadowOpacity=1;
layerShadow.borderColor=[UIColor whiteColor].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;
layer.borderWidth=borderWidth;
//利用图层形变解决图像倒立问题
layer.transform=CATransform3DMakeRotation(M_PI, 1, 0, 0);
//设置图层代理
layer.delegate=self;
//添加图层到根图层
[self.view.layer addSublayer:layer];
//调用图层setNeedDisplay,否则代理方法不会被调用
[layer setNeedsDisplay];
}
#pragma mark 绘制图形、图像到图层,注意参数中的ctx时图层的图形上下文,其中绘图位置也是相对图层而言的
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
// NSLog(@"%@",layer);//这个图层正是上面定义的图层
UIImage *image=[UIImage imageNamed:@"thumb_IMG_0705_1024.jpg"];
//注意这个位置是相对于图层而言的不是屏幕
CGContextDrawImage(ctx, CGRectMake(0, 0, 150, 150), image.CGImage);
}
事实上如果仅仅就显示一张图片在图层中当然没有必要那么麻烦,直接设置图层contents就可以了,不牵涉到绘图也就没有倒立的问题了。
- (void)viewDidLoad {
[super viewDidLoad];
CGPoint position= CGPointMake(160, 200);
CGRect bounds=CGRectMake(0, 0, 150, 150);
CGFloat cornerRadius=150/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.shadowOpacity=1;
layerShadow.borderColor=[UIColor whiteColor].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;
layer.borderWidth=borderWidth;
//设置内容(注意这里一定要转换为CGImage)
UIImage *image=[UIImage imageNamed:@"thumb_IMG_0705_1024.jpg"];
// layer.contents=(id)image.CGImage;
[layer setContents:(id)image.CGImage];
//添加图层到根图层
[self.view.layer addSublayer:layer];
}
既然如此为什么还大费周章的说形变呢,因为形变对于动画有特殊的意义。在动画开发中形变往往不是直接设置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()方法得到的上下文正是前面创建的上下文。
MMELayer.m 继承自CALayer
//
// MMELayer.m
// MMEdu
//
// Created by fly on 2018/2/26.
// Copyright © 2018年 fly. All rights reserved.
//
#import "MMELayer.h"
@implementation MMELayer
-(void)drawInContext:(CGContextRef)ctx{
NSLog(@"3-drawInContext:");
NSLog(@"CGContext:%@",ctx);
// CGContextRotateCTM(ctx, M_PI_4);
// 填充色,CGColorRef对象,图形填充色,默认为黑色
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);
// 起始点
CGContextMoveToPoint(ctx, 94.5, 33.5);
//// Star Drawing,五角星的各个坐标
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
MMEView.m 继承UIView
//
// MMEView.m
// MMEdu
//
// Created by fly on 2018/2/26.
// Copyright © 2018年 fly. All rights reserved.
//
#import "MMEView.h"
#import "MMELayer.h"
@implementation MMEView
-(instancetype)initWithFrame:(CGRect)frame{
NSLog(@"initWithFrame:");
if (self=[super initWithFrame:frame]) {
MMELayer *layer=[[MMELayer 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;
}
@end
CALayerViewController.m 视图控制器
//
// CALayerViewController.m
// CALayer
//
// Created by fly on 2018/2/24.
// Copyright © 2018年 fly. All rights reserved.
//
#import "CALayerViewController.h"
#import "MMEView.h"
@interface CALayerViewController ()
@end
@implementation CALayerViewController
- (void)viewDidLoad {
[super viewDidLoad];
MMEView *view=[[MMEView alloc]initWithFrame:[UIScreen mainScreen].bounds];
view.backgroundColor=[UIColor colorWithRed:249.0/255.0 green:249.0/255.0 blue:249.0/255.0 alpha:1];
[self.view addSubview:view];
}
@end
运行效果:
Core Animation
iOS中最基础的动画操作就是只要调用UIView的块代码即可实现一个动画效果,这在其他系统开发中基本不可能实现。下面通过一个简单的UIView进行一个图片放大动画效果演示:
//
// CALayerViewController.m
// CALayer
//
// Created by fly on 2018/2/24.
// Copyright © 2018年 fly. All rights reserved.
//
#import "CALayerViewController.h"
#import "MMEView.h"
@interface CALayerViewController ()
@end
@implementation CALayerViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIImage *image=[UIImage imageNamed:@"252F4E1F02C4506EF85F9A6E22F07B96.jpg"];
UIImageView *imageView=[[UIImageView alloc]init];
imageView.image=image;
imageView.frame=CGRectMake(120, 140, 80, 80);
[self.view addSubview:imageView];
//两秒后开始一个持续一分钟的动画
[UIView animateWithDuration:5 delay:2 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
imageView.frame=CGRectMake(80, 100, 200, 300);
} completion:nil];
}
@end
效果如图:有点失真不知道为啥
用UIView为我们封装的方法是非常方便的,但是有些问题还是无法解决,如:如怎么控制动画的暂停?
这里就需要了解iOS的核心动画Core Animation(包含在Quartz Core框架中)。在iOS中核心动画分为几类:基础动画、关键帧动画、动画组、转场动画。各个类的关系大致如下:
CAAnimation:核心动画的基础类,不能直接使用,负责动画运行时间、速度的控制,本身实现了CAMediaTiming协议。
CAPropertyAnimation:属性动画的基类(通过属性进行动画设置,注意是可动画属性),不能直接使用。
CAAnimationGroup:动画组,动画组是一种组合模式设计,可以通过动画组来进行所有动画行为的统一控制,组中所有动画效果可以并发执行。
CATransition:转场动画,主要通过滤镜进行动画效果设置。
CABasicAnimation:基础动画,通过属性修改进行动画参数控制,只有初始状态和结束状态。
CAKeyframeAnimation:关键帧动画,同样是通过属性进行动画参数控制,但是同基础动画不同的是它可以有多个状态控制。
基础动画、关键帧动画都属于属性动画,就是通过修改属性值产生动画效果,开发人员只需要设置初始值和结束值,中间的过程动画(又叫“补间动画”)由系统自动计算产生。和基础动画不同的是关键帧动画可以设置多个属性值,每两个属性中间的补间动画由系统自动完成,因此从这个角度而言基础动画又可以看成是有两个关键帧的关键帧动画。