iOS开发之核心动画(一):了解核心动画及CALayer常用方法介绍

飒飒东风细雨来,芙蓉塘外有轻雷。
金蟾啮锁烧香入,玉虎牵丝汲井回。
贾氏窥帘韩掾少,宓妃留枕魏王才。
春心莫共花争发,一寸相思一寸灰!

李商隐 《无题·飒飒东风细雨来》

核心动画介绍

本质

  • 核心动画都是假象,并不会改变layer的属性值
  • 而UIView只有属性改变才会有动画
  • 使用场景:
    • 如果在执行过程中不需要用户交互,就可以用核心动画
    • 在转场动画中,核心动画用得比较多

CAAnimation的继承结构:

  • 要想执行动画,就必须初始化一个CAAnimation对象
  • 其实,一般情况下,我们使用的比较多的是CAAnimation的子类,因此,先大致看看CAAnimation的继承结构:
hack_appList_result_failed

CAAnimation

  • 所有动画对象的父类,负责控制动画的持续时间和速度,是个抽象类,不能直接使用,应该使用它具体的子类。
  • 属性解析:

    • duration:动画的持续时间
    • repeatCount:动画的重复次数
    • repeatDuration:动画的重复时间
    • removedOnCompletion:默认为YES,代表动画执行完毕后就从图层上移除,图形会恢复到动画执行前的状态。如果想让图层保持显示动画执行后的状态,那就设置为NO,不过还要设置fillMode为kCAFillModeForwards
    • fillMode:决定当前对象在非active时间段的行为.比如动画开始之前,动画结束之后
    • beginTime:可以用来设置动画延迟执行时间,若想延迟2s,就设置为CACurrentMediaTime()+2,CACurrentMediaTime()为图层的当前时间
    • timingFunction:速度控制函数,控制动画运行的节奏
    • delegate:动画代理
  • timingFunction可选的值有:

1
2
3
4
5
6
7
8
// (线性):匀速,给你一个相对静态的感觉
kCAMediaTimingFunctionLinear
// (渐进):动画缓慢进入,然后加速离开
kCAMediaTimingFunctionEaseIn
// (渐出):动画全速进入,然后减速的到达目的地
kCAMediaTimingFunctionEaseOut
// (渐进渐出):动画缓慢的进入,中间加速,然后减速的到达目的地。这个是默认的动画行为。
kCAMediaTimingFunctionEaseInEaseOut

CAAnimation——动画代理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// CAAnimation在分类中定义了代理方法
@interface NSObject (CAAnimationDelegate)

/* Called when the animation begins its active duration. */

- (void)animationDidStart:(CAAnimation *)anim;

/* Called when the animation either completes its active duration or
* is removed from the object it is attached to (i.e. the layer). 'flag'
* is true if the animation reached the end of its active duration
* without being removed. */
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag;

@end

CAPropertyAnimation

  • 是CAAnimation的子类,也是个抽象类,要想创建动画对象,应该使用它的两个子类:CABasicAnimationCAKeyframeAnimation
  • 属性说明:
    • keyPath:通过指定CALayer的一个属性名称为keyPath(NSString类型),并且对CALayer的这个属性的值进行修改,达到相应的动画效果。
    • 比如,指定@”position”为keyPath,就修改CALayer的position属性的值,以达到平移的动画效果。

CALayer

  • 在iOS中,你能看得见摸得着的东西基本上都是UIView,比如一个按钮、一个文本标签、一个文本输入框、一个图标等等,这些都是UIView。
  • 其实UIView之所以能显示在屏幕上,完全是因为它内部的一个图层。
  • 在创建UIView对象时,UIView内部会自动创建一个图层(即CALayer对象),通过UIView的layer属性可以访问这个层。
1
@property(nonatomicreadonlyretain) CALayer *layer;
  • 当UIView需要显示到屏幕上时,会调用drawRect:方法进行绘图,并且会将所有内容绘制在自己的图层上,绘图完毕后,系统会将图层拷贝到屏幕上,于是就完成了UIView的显示。
  • 换句话说,UIView本身不具备显示的功能,是它内部的层才有显示功能。

图层的常见属性应用

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
28
29
30
31
32
33
34
35
/** 图像图层的属性
*/
- (void)demoImageView
{
// 设置头像的边框和颜色
self.iconVIew.layer.borderWidth = 5;
self.iconVIew.layer.borderColor = [UIColor blackColor].CGColor;

// 设置圆角半径
self.iconVIew.layer.cornerRadius = 10;
#warning 只有设置这个图片的圆角才会出来 (因为图片不是加在主层上的 是在子层上面)
// self.iconVIew.clipsToBounds = YES;

// 设置阴影和颜色
#warning 裁剪的话就不会有阴影效果 这两个属性不能共存
self.iconVIew.layer.shadowOffset = CGSizeMake(2020);
self.iconVIew.layer.shadowColor = [UIColor blackColor].CGColor;
self.iconVIew.layer.shadowOpacity = 0.5; // 不透明度 默认阴影是透明的
}

/** view的图层的设置
*/
- (void)demoView
{
// 设置边框和颜色
self.cyanView.layer.borderWidth = 10;
self.cyanView.layer.borderColor = [UIColor redColor].CGColor;
// 设置圆角半径
self.cyanView.layer.cornerRadius = 10;

// 设置阴影和颜色
self.cyanView.layer.shadowOffset = CGSizeMake(2020);
self.cyanView.layer.shadowColor = [UIColor blackColor].CGColor;
self.cyanView.layer.shadowOpacity = 0.5; // 不透明度 默认阴影是透明的
}

图层的形变属性

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
/** transfor属性
*/
- (IBAction)btnClick
{
// 3D属性 多了个Y轴 (作用于图层)
self.iconVIew.layer.transform = CATransform3DMakeTranslation(50500);
self.iconVIew.transform = CGAffineTransformMakeTranslation(5050);

// X Y Z 三条轴 形成一个旋转轴 (左上角(0,0) 右下角(1,1)
self.iconVIew.layer.transform = CATransform3DMakeRotation(M_2_PI, 110);
// 只能平面旋转
self.iconVIew.transform = CGAffineTransformMakeRotation(M_2_PI);

// 可以用KVC得方式赋值 (可以传递哪些key path, 在官方文档搜索 "CATransform3D key paths")
[self.iconVIew.layer setValue:@(M_2_PI) forKeyPath:@"transform.rotation"];

[self.iconVIew.layer setValue:@(100) forKeyPath:@"transform.translation.x"];

NSValue *value = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_2_PI, 110)];
[self.iconVIew.layer setValue:value forKeyPath:@"transform"];

NSValue *value1 = [NSValue valueWithCATransform3D:CATransform3DMakeScale(120)];
[self.iconVIew.layer setValue:value1 forKeyPath:@"transform"];

};

自定义图层

  • UIView *view;
  • view.layer.delegate == view;
  • view的完整显示过程

    • view.layer会准备一个Layer Graphics Contex(图层类型的上下文)
    • 调用view.layer.delegate(view)的drawLayer:inContext:,并传入刚才准备好的上下文
    • view的drawLayer:inContext:方法内部又会调用view的drawRect:方法
    • view就可以在drawRect:方法中实现绘图代码, 所有东西最终都绘制到view.layer上面
    • 系统再将view.layer的内容拷贝到屏幕,于是完成了view的显示
  • 自定义层,其实就是在层上绘图,一共有2种方法,下面详细介绍一下。

自定义图层方法1

  • 方法描述:创建一个CALayer的子类,然后覆盖drawInContext:方法,使用Quartz2D API进行绘图
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
28
29
30
31
32
33
34
35
36
- (void)diyLayer
{
XSLayer *layer = [XSLayer layer];
layer.bounds = CGRectMake(00100100);
layer.backgroundColor = [UIColor redColor].CGColor;
layer.anchorPoint = CGPointZero;
layer.position = CGPointMake(100100);
layer.delegate = self;
// 只有明显地调用setNeedsDisplay方法,才会调用drawInContext:方法进行绘制
[layer setNeedsDisplay];
[self.view.layer addSublayer:layer];
}

- (void)demoLayer
{
CALayer *layer = [CALayer layer];
layer.bounds = CGRectMake(00100100);
layer.backgroundColor = [UIColor redColor].CGColor;
layer.anchorPoint = CGPointZero; // 决定了position的位置
layer.position = CGPointMake(100100);
layer.delegate = self;
// 只有明显地调用setNeedsDisplay方法,才会调用drawInContext:方法进行绘制
[layer setNeedsDisplay];
[self.view.layer addSublayer:layer];
}

#pragma mark - 代理方法
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
// 设置颜色
CGContextSetRGBFillColor(ctx, 0101);
// 画圆
CGContextAddEllipseInRect(ctx, CGRectMake(50505050));
// 渲染
CGContextFillPath(ctx);
}

自定义的图层方法2

  • 方法描述:创建一个CALayer的子类,然后覆盖drawInContext:方法,使用uartz2D API进进行绘图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#import "XSLayer.h"

@implementation XSLayer
/** 只有明显地调用setNeedsDisplay方法,才会调用drawInContext:方法进行绘制
*/
- (void)drawInContext:(CGContextRef)ctx
{
// 设置颜色
CGContextSetRGBFillColor(ctx, 0011);
// 画圆
CGContextAddRect(ctx, CGRectMake(005050));
// 渲染
CGContextFillPath(ctx);
}
@end

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
25
26
#pragma mark 暂停CALayer的动画
-(void)pauseLayer:(CALayer*)layer
{
CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];

// 让CALayer的时间停止走动
layer.speed = 0.0;
// 让CALayer的时间停留在pausedTime这个时刻
layer.timeOffset = pausedTime;
}

#pragma mark 恢复CALayer的动画
-(void)resumeLayer:(CALayer*)layer
{
CFTimeInterval pausedTime = layer.timeOffset;
// 1. 让CALayer的时间继续行走
layer.speed = 1.0;
// 2. 取消上次记录的停留时刻
layer.timeOffset = 0.0;
// 3. 取消上次设置的时间
layer.beginTime = 0.0;
// 4. 计算暂停的时间(这里也可以用CACurrentMediaTime()-pausedTime)
CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
// 5. 设置相对于父坐标系的开始时间(往后退timeSincePause)
layer.beginTime = timeSincePause;
}

隐式动画

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

    • 当对非RootLayer的部分属性进行修改时,默认会自动产生一些动画效果,而这些属性称为AnimatableProperties(可动画属性)
  • 列举几个常见的Animatable Properties:

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

事务

  • 可以通过动画事务(CATransaction)关闭默认的隐式动画效果
1
2
3
4
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.myview.layer.position = CGPointMake(10, 10);
[CATransaction commit];

CALayer注意点

关于CALayer的疑惑

  • 为什么CALayer中使用CGColorRefCGImageRef这2种数据类型,而不用UIColorUIImage
  • 首先
    • CALayer是定义在QuartzCore框架中的,CGImageRefCGColorRef两种数据类型是定义在CoreGraphics框架中的。
    • UIColor、UIImage是定义在UIKit框架中的。
  • 其次
    • QuartzCore框架和CoreGraphics框架是可以跨平台使用的,在iOS和Mac OS X上都能使用。
    • 但是UIKit只能在iOS中使用。
  • 为了保证可移植性,QuartzCore不能使用UIImage、UIColor,只能使用CGImageRef、CGColorRef。

UIView和CALayer的选择

  • 通过CALayer,就能做出跟UIImageView一样的界面效果,既然CALayer和UIView都能实现相同的显示效果,那究竟该选择谁好呢?
    • 其实,对比CALayer,UIView多了一个事件处理的功能。也就是说,CALayer不能处理用户的触摸事件,而UIView可以。
    • 所以,如果显示出来的东西需要跟用户进行交互的话,用UIView;如果不需要跟用户进行交互,用UIView或者CALayer都可以。
    • 当然,CALayer的性能会高一些,因为它少了事件处理的功能,更加轻量级。

position和anchorPoint

  • CALayer有2个非常重要的属性:positionanchorPoint
1
2
3
4
5
6
7
8
9
// 用来设置CALayer在父层中的位置
// 以父层的左上角为原点(0, 0)
@property CGPoint position;

// 称为“定位点”、“锚点”
// 决定着CALayer身上的哪个点会在position属性所指的位置
// 以自己的左上角为原点(0, 0)
// 它的x、y取值范围都是0~1,默认值为(0.5, 0.5)(即中心点)
@property CGPoint anchorPoint;

UIView和CALayer的其他关系

  • UIView可以通过subviews属性访问所有的子视图,类似地,CALayer也可以通过sublayers属性访问所有的子层
  • UIView可以通过superview属性访问父视图,类似地,CALayer也可以通过superlayer属性访问父层
  • 下面再看一张UIView和CALayer的关系图:
    hack_appList_result_failed
  • 如果两个UIView是父子关系,那么它们内部的CALayer也是父子关系。

常用的CALayer子类

CAReplicatorLayer(复制层)

  • 结合音量振动条的简单使用
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
- (void)viewDidLoad 
{
[super viewDidLoad];

// 创建复制层
CAReplicatorLayer *repLayer = [CAReplicatorLayer layer];

repLayer.frame = self.musicView.bounds;

// 设置复制份数(表示的是复制层内所有的空间为一份 复制的总的份数)
repLayer.instanceCount = 5;

// 设置复制层的形变(相对于上一个)
repLayer.instanceTransform = CATransform3DMakeTranslation(40, 0, 0);

// 复制层(每一份)的动画间隔时间 (相对于上一ge)
repLayer.instanceDelay = 0.3;

// 添加复制层
[self.musicView.layer addSublayer:repLayer];

// 创建单个音乐震动条
CALayer *redLayer = [CALayer layer];
redLayer.backgroundColor = [UIColor redColor].CGColor;
// 设置锚点
redLayer.anchorPoint = CGPointMake(0, 1);
// 设置position
redLayer.position = CGPointMake(0, 200);
CGFloat redLayerX = 0;
CGFloat redLayerY = 0;
CGFloat redLayerW = 30;
CGFloat redLayerH = 100;
redLayer.bounds = CGRectMake(redLayerX, redLayerY, redLayerW, redLayerH);
[repLayer addSublayer:redLayer];

// 添加动画
CABasicAnimation *anim = [CABasicAnimation animation];
anim.keyPath = @"transform.scale.y";
anim.toValue = @0;
anim.repeatCount = MAXFLOAT;
anim.duration = .5;
// 设置翻转动画(即是恢复时候的动画)
repLayer.autoreverses = YES;

// 添加动画
[redLayer addAnimation:anim forKey:nil];
}

CAGradientLayer(渐变层)

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
28
29
30
31
32
33
34
35
36
#pragma mark - 用渐变层给下半部分添加阴影

CAGradientLayer *gradientLayer = [CAGradientLayer layer];

// 渐变颜色 (从一个颜色到另一个颜色过渡)
gradientLayer.colors = @[(id)[UIColor clearColor].CGColor,(id)[UIColor blackColor].CGColor];

// 设置渐变层的frame
gradientLayer.frame = self.bottomImageView.bounds;

// 设置不透明度 (一开始是0 不显示)
gradientLayer.opacity = 0;

// // 设置渐变的位置
// gradientLayer.locations = @[@2,@5];

// 设置渐变的方向
// gradientLayer.startPoint
// gradientLayer.endPoint

[self.bottomImageView.layer addSublayer:gradientLayer];

self.gradientLayer = gradientLayer;

if (sender.state == UIGestureRecognizerStateEnded) {

// 阴影设置为0
self.gradientLayer.opacity = 0;

// 设置松手回弹的时候的弹簧效果
[UIView animateWithDuration:1 delay:0 usingSpringWithDamping:.2 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{

// 还原形变
self.self.topImageView.layer.transform = CATransform3DIdentity;
} completion:^(BOOL finished) {
}];

CAShapeLayer(形状图层)

  • 结合QQ bage消息的粘性计算实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (CAShapeLayer *)shapeLayer
{
if (!_shapeLayer) {
// 可以根据一个路径生成一个图层
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.fillColor = [UIColor redColor].CGColor;
[self.superview.layer insertSublayer:shapeLayer atIndex:0];
self.shapeLayer = shapeLayer;
}
return _shapeLayer;
}

// 设置形状图层的路径
self.shapeLayer.path = path.CGPath;
  • QQ bage消息的粘性计算图

    hack_appList_result_failed
  • 计算方法

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 计算不规则的矩形路径
- (UIBezierPath *)pathWithSmallView:(UIView *)smallView bigView:(UIView *)bigView
{
// 计算圆心
CGFloat d = [self distanceBetweenBigCirle:bigView andSmallCircle:smallView];

CGFloat y1 = smallView.center.y;
CGFloat x1 = smallView.center.x;
CGFloat r1 = smallView.layer.cornerRadius;

CGFloat y2 = bigView.center.y;
CGFloat x2 = bigView.center.x;
CGFloat r2 = bigView.layer.cornerRadius;

if (d == 0) return nil;
// cosθ
CGFloat cosθ = (y2 - y1) / d;
// sinθ
CGFloat sinθ = (x2 - x1) / d;

// A:
CGPoint pointA = CGPointMake(x1 - r1 * cosθ, y1 + r1 * sinθ);
// B:
CGPoint pointB = CGPointMake(x1 + r1 * cosθ, y1 - r1 * sinθ);

// C:
CGPoint pointC = CGPointMake(x2 + r2 * cosθ, y2 - r2 * sinθ);

// D:
CGPoint pointD = CGPointMake(x2 - r2 * cosθ, y2 + r2 * sinθ);

// O:
CGPoint pointO = CGPointMake(pointA.x + d * 0.5 * sinθ, pointA.y + d * 0.5 * cosθ);

// P:
CGPoint pointP = CGPointMake(pointB.x + d * 0.5 * sinθ, pointB.y + d * 0.5 * cosθ);

// 描述路径
UIBezierPath *path = [UIBezierPath bezierPath];

[path moveToPoint:pointA];

// AB
[path addLineToPoint:pointB];

// BC
// 绘制曲线
[path addQuadCurveToPoint:pointC controlPoint:pointP];

// CD
[path addLineToPoint:pointD];

// DA
[path addQuadCurveToPoint:pointA controlPoint:pointO];

return path;
}
要不要鼓励一下😘