IOS动画(1)CALayer

参考博客:

http://geeklu.com/2012/09/animation-in-ios/

http://www.cnblogs.com/kenshincui/p/3972100.html#calayer

CALayer是CoreAnimation部分的内容,CALayer的概念类似于photoshop中层的概念,每个UIView都有一个根CALayer,每个CALayer又可以添加子CALayer,从结构上来看CALayer是一种树形结构,UIView的绘制工作都交由CALayer完成。

我们可以从以下几点掌握CALayer的用法

  1. CALayer常用属性
  2. CALayer图层绘制
  3. CALayer模型树,呈现树以及渲染树
  4. CALayer tranform变化
  5. CALayer事务
  6. CALayer时间系统

CALayer常用属性

我们可以进入CALayer.h中查看CALayer支持的属性,其中注解中标注Animation的属性表示支持隐式动画,当这些属性的值改变时系统自带了平滑过渡的动画效果(非根Layer支持)

下表列出了CALayer常用的属性:

属性说明是否支持隐式动画
anchorPoint 和中心点position重合的一个点,称为“锚点”,锚点的描述是相对于x、y位置比例而言的默认在图像中心点(0.5,0.5)的位置
backgroundColor 图层背景颜色
borderColor 边框颜色
borderWidth 边框宽度
bounds 图层大小
contents 图层显示内容,例如可以将图片作为图层内容显示
contentsRect 图层显示内容的大小和位置
cornerRadius 圆角半径
doubleSided 图层背面是否显示,默认为YES
frame 图层大小和位置,不支持隐式动画,所以CALayer中很少使用frame,通常使用bounds和position代替
hidden 是否隐藏
mask 图层蒙版
maskToBounds 子图层是否剪切图层边界,默认为NO
opacity 透明度 ,类似于UIView的alpha
position 图层中心点位置,类似于UIView的center
shadowColor 阴影颜色
shadowOffset 阴影偏移量
shadowOpacity 阴影透明度,注意默认为0,如果设置阴影必须设置此属性
shadowPath 阴影的形状
shadowRadius 阴影模糊半径
sublayers 子图层
sublayerTransform 子图层形变
transform 图层形变

其中anchorPoint设置CALayer的锚点,CGPoint类型,取值范围为0~1,当设置了layer的transform属性进行形变时,所有的翻转,缩放效果都是以锚点为中心的,锚点默认为(0.5,0.5),表示在layer的中心,与中心点position重合

当修改锚点值时,layer会移动使锚点与中心点重合,比如layer的初始frame为(0,0,20,20),此时中心点为(10,10),锚点默认(0.5,0.5)处于(10,10)的位置,当将锚点设为(0,0)后,layer会向右下方移动,移动后frame的值为(10,10,20,20),此时锚点仍然处在(10,10)的位置。

以下代码在视图控制器根视图的根layer中加入了新的layer,并且设置了这个新的layer的颜色,大小,位置,圆角,阴影等一系列属性,当有触摸事件产生后,我们修改了这个layer的位置以及大小。

- (void)viewDidLoad
{
    [super viewDidLoad];

    CALayer *layer = [CALayer layer];
    //设置层的大小
    layer.bounds = CGRectMake(0, 0, 80, 80);
    //设置层初始位置
    layer.position = CGPointMake(100, 100);
    //设置层的背景色,CoreAnimation跨平台,需要使用CGColor
    layer.backgroundColor = [UIColor magentaColor].CGColor;
    //设置层圆角,弧度为40
    layer.cornerRadius = 40;
    //设置阴影颜色,偏移量以及颜色
    layer.shadowColor = [UIColor blackColor].CGColor;
    layer.shadowOffset = CGSizeMake(3,3);
    layer.shadowOpacity = 0.7;
    
    
    [self.view.layer addSublayer:layer];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint point = [touch locationInView:self.view];
    
    //获取之前添加的layer
    CALayer *layer = [self.view.layer.sublayers lastObject];
    //设置layer的新位置为手指点击的位置
    layer.position = point;
    float width = layer.bounds.size.width;
    //设置layer的大小
    if (width == 80) {
        layer.bounds = CGRectMake(0,0,40,40);
        layer.cornerRadius = 20;
    } else {
        layer.bounds = CGRectMake(0,0,80,80);
        layer.cornerRadius = 40;
    }
}

 

执行以上代码,当layer位置和大小改变时会有一种平滑过渡的动画效果,这是因为layer的position以及bound都支持隐式动画,查看上面的属性表,frame属性并不支持映射动画,因此如果我们直接修改layer的frame属性,并不会出现很"顺滑"的过渡效果。

CALayer图层绘制

1. 如果刚好有现成的CGImage对象,那么可以通过直接设置CALayer的contents属性进行图层绘制

- (void)viewDidLoad
{
    [super viewDidLoad];

    CALayer *layer = [CALayer layer];
    //设置层的大小
    layer.bounds = CGRectMake(0, 0, 120, 120);
    //设置层初始位置
    layer.position = CGPointMake(100, 100);
    //设置层的背景色,CoreAnimation跨平台,需要使用CGColor
    layer.backgroundColor = [UIColor magentaColor].CGColor;
    //设置层圆角,弧度为40
    layer.cornerRadius = 60;
    //设置阴影颜色,偏移量以及颜色
    layer.shadowColor = [UIColor blackColor].CGColor;
    layer.shadowOffset = CGSizeMake(3,3);
    layer.shadowOpacity = 0.7;
    //设置contents属性
    layer.contents = (id)[UIImage imageNamed:@"5"].CGImage;
    layer.masksToBounds = YES;

    [self.view.layer addSublayer:layer];
}

 

上面示例中设置了layer的一个属性masksToBounds,这个属性控制是否裁剪子层,当绘制一张图片到图层上的时候会重新创建一个图层添加到当前图层,这样一来如果设置了圆角之后虽然底图层有圆角效果,但是子图层还是矩形,只有设置了masksToBounds为YES让子图层按底图层剪切才能显示圆角效果。同时这个属性设置为YES后,layer的阴影将失效,如果仍旧希望显示阴影,那就需要再添加一层作为单独的阴影层放置在显示图片的layer下面。

2.设置layer的代理,然后在代理类中重写- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx即可,当调用layer的setNeedsDisplay方法后会执行该代理方法,设置layer代理不需要实现CALayerDelegate,因为CALayer定义中给NSObject做了分类扩展,所有的NSObject都包含这个方法。

- (void)viewDidLoad
{
    [super viewDidLoad];

    CALayer *layer = [CALayer layer];
    //设置层的大小
    layer.bounds = CGRectMake(0, 0, 120, 120);
    //设置层初始位置
    layer.position = CGPointMake(100, 100);
    //设置层的背景色,CoreAnimation跨平台,需要使用CGColor
    layer.backgroundColor = [UIColor magentaColor].CGColor;
    //设置层圆角,弧度为40
    layer.cornerRadius = 60;
    layer.masksToBounds = YES;
    layer.delegate = self;
    [layer setNeedsDisplay];
    [self.view.layer addSublayer:layer];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    
    CGContextScaleCTM(ctx, 1, -1);
    CGContextTranslateCTM(ctx, 0, -layer.bounds.size.height);
    CGContextDrawImage(ctx, layer.bounds, [UIImage imageNamed:@"5"].CGImage);
    
}

 

drawLayer:inContext:中使用了CGContextDrawImage函数,因为Quartz2D坐标与UIKit坐标y轴的差异性,我们需要先进行坐标转换工作。

此外drawLayer:inContext:这个函数很容易让我们想起UIView中的drawRect函数,同样用来绘图,同样需要通过调用setNeedDisplay来执行,只不过前者由CALayer对象调用触发,后者由UIView对象调用触发。

前面提过每个UIView都有一个根CALayer,而且UIView的绘制工作都交由CALayer完成,所以drawRect函数实际是在UIView的根layer上进行绘制的,drawRect中通过UIGraphicsGetCurrentContext()获得的CGContextRef与drawLayer:inContext:函数中的第二个参数是一个对象。

为了验证以上说法,我们自定义一个类继承UIVIew,重写drawRect和drawLayer:inContext:方法

- (void)drawRect:(CGRect)rect
{
    // Drawing code
    CGContextRef context = UIGraphicsGetCurrentContext();
    NSLog(@"drawRect:%@,%@",context,self.layer);
    
    CGContextSetFillColorWithColor(context, [UIColor magentaColor].CGColor);
    CGContextFillRect(context, rect);
}

-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
    NSLog(@"drawLayerinContext:%@,%@",ctx,layer);
    [super drawLayer:layer inContext:ctx];
}

以上代码输出

drawLayerinContext:<CGContext 0x8b6b650><CALayer: 0x8b65ac0>

drawRect:<CGContext 0x8b6b650><CALayer: 0x8b65ac0>

可以发现代码先执行drawLayer:inContext: 然后才执行drawRect,而且两个函数中的绘图上下文是同一个对象,而触发drawLayer:inContext:代理方法的正是UIView的根layer。

如果我们将上面代码中drawLayer:inContext:方法最后一行[super drawLayer:layer inContext:ctx]注释掉,会发现界面上不再显示drawRect中绘制的洋红色矩形背景,同时控制台仅输出一句

drawLayerinContext:<CGContext 0x8b6b650><CALayer: 0x8b65ac0>

由此可以得出结论drawRect确实是由 drawLayer:inContext:调用的。

3.对于自定义的CALayer对象,我们可以通过在对象的-(void)drawInContext:(CGContextRef)ctx方法中对层进行绘制,此方法也需要CALayer对象调用setNeedsDisplay才会触发。

@implementation ZLTLayer

-(void)drawInContext:(CGContextRef)ctx {
    CGContextScaleCTM(ctx, 1, -1);
    CGContextTranslateCTM(ctx, 0, -self.bounds.size.height);
    CGContextDrawImage(ctx, self.bounds, [UIImage imageNamed:@"2"].CGImage);
}

@end


//视图控制器
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    ZLTLayer *zltLayer = [ZLTLayer layer];
    zltLayer.bounds = CGRectMake(0, 0, 200, 200);
    zltLayer.position = CGPointMake(200, 200);
    zltLayer.borderWidth = 3;
    zltLayer.borderColor = [UIColor yellowColor].CGColor;
    [self.view.layer addSublayer:zltLayer];
    [zltLayer setNeedsDisplay];

}

 

此时产生一个疑问,如果我们修改视图控制器的代码,设置zltLayer的delegate为当前是视图控制器,那么当执行layer的setNeedsDisplay方法时,会否会执行之前所说的drawLayer:inContext:方法呢

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    ZLTLayer *zltLayer = [ZLTLayer layer];
    zltLayer.bounds = CGRectMake(0, 0, 200, 200);
    zltLayer.position = CGPointMake(200, 200);
    zltLayer.borderWidth = 3;
    zltLayer.borderColor = [UIColor yellowColor].CGColor;
    zltLayer.delegate = self;
    [self.view.layer addSublayer:zltLayer];
    [zltLayer setNeedsDisplay];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    NSLog(@"%s",__func__);
    CGContextDrawImage(ctx, layer.bounds, [UIImage imageNamed:@"5"].CGImage);
}

结果显示drawLayer:inContext:并未执行

而当把ZLTLayer中的-(void)drawInContext:(CGContextRef)ctx注释掉,不再重写此函数时 drawLayer:inContext:却得到了执行了,说明CALayer中drawInContext方法调用了代理对象的drawLayer:inContext:方法。

继续修改之前的代码

@implementation ZLTLayer

-(void)drawInContext:(CGContextRef)ctx {
    CGContextScaleCTM(ctx, 1, -1);
    CGContextTranslateCTM(ctx, 0, -self.bounds.size.height);
    CGContextDrawImage(ctx, self.bounds, [UIImage imageNamed:@"2"].CGImage);
     
    //调用父类实现
    [super drawInContext:ctx];
}

@end

//视图控制器
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    ZLTLayer *zltLayer = [ZLTLayer layer];
    zltLayer.bounds = CGRectMake(0, 0, 200, 200);
    zltLayer.position = CGPointMake(200, 200);
    zltLayer.borderWidth = 3;
    zltLayer.borderColor = [UIColor yellowColor].CGColor;
    zltLayer.delegate = self;
    [self.view.layer addSublayer:zltLayer];
    [zltLayer setNeedsDisplay];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    NSLog(@"%s",__func__);
    CGContextDrawImage(ctx, layer.bounds, [UIImage imageNamed:@"5"].CGImage);
}

至此我们可以得出结论,当调用layer的setNeedsDisplay函数后,会执行layer的drawInContext方法,在drawInContext方法中又会调用代理对象的drawLayer:inContext:方法

CALayer模型树,呈现树以及渲染树

Layer也和View一样存在着一个层级树状结构,称之为图层树(Layer Tree),有以下三种:

模型树:直接创建的或者通过UIView获得的(view.layer)用于显示的图层树称之为模型树(Model Tree),模型树的属性在其被修改的时候就变成了新的值,这个是可以用代码直接操控的部分,模型树的背后还存在两份图层树的拷贝,一个是呈现树(Presentation Tree),一个是渲染树(Render Tree)。

呈现树:可以通过普通layer(其实就是模型树)的layer.presentationLayer获得,而模型树则可以通过modelLayer属性获得,呈现树的属性值和动画运行过程中界面上看到的是一致的。

渲染树:渲染树是私有的,你无法访问到,渲染树是对呈现树的数据进行渲染,为了不阻塞主线程,渲染的过程是在单独的进程或线程中进行的,所以你会发现Animation的动画并不会阻塞主线程。

渲染树是CoreAnimation内部的功能,我们基本不会遇到它,而模型树和呈现树却经常会碰到,具体的处理方式会在之后的属性动画中具体提到。

CALayer tranform变化

transform是CALayer的一个属性,通过设置它可以控制layer的旋转,位移以及缩放

之前调用CGContextDrawImage对层绘制时需要转换坐标,也可以设置transform,使其绕x轴旋转180度

- (void)viewDidLoad
{
    [super viewDidLoad];

    CALayer *layer = [CALayer layer];
    //设置层的大小
    layer.bounds = CGRectMake(0, 0, 120, 120);
    layer.position = CGPointMake(100, 100);
    layer.cornerRadius = 60;
    layer.masksToBounds = YES;
    layer.delegate = self;
    
    //第一个参数表示旋转角度,后面三个参数控制是否绕x轴,y轴,z轴旋转
    layer.transform = CATransform3DMakeRotation(M_PI, 1, 0, 0);
    [layer setNeedsDisplay];
    [self.view.layer addSublayer:layer];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    CGContextDrawImage(ctx, layer.bounds, [UIImage imageNamed:@"5"].CGImage);
}

 

CATransform3D数据结构定义了一个三维变换(4x4 CGFloat值的矩阵),用于图层的旋转,缩放,偏移,歪斜和应用的透视,常用函数有下面几种:

1:CATransform3DMakeRotation(CGFloat angle, <#CGFloat x#>, <#CGFloat y#>, <#CGFloat z#>)  

如果x=1,y=0,z=0则绕x轴旋转angle角度

如果x=0,y=1,z=0则绕y轴旋转angle角度

如果x=0,y=0,z=1则绕z轴旋转angle角度

如果x=1,y=1,z=0则绕x轴和y轴夹角旋转angle角度

如果x=1,y=1,z=1则绕3轴夹角旋转angle角度

上诉的旋转中心都是layer的锚点(anchorPoint)

2:CATransform3DRotate(<#CATransform3D t#>, <#CGFloat angle#>, <#CGFloat x#>, <#CGFloat y#>, <#CGFloat z#>)  功能与上一函数类似,但可以叠加一个CATransform3D效果

3:CATransform3DMakeScale(<#CGFloat sx#>, <#CGFloat sy#>, <#CGFloat sz#>)  用于缩放,三个参数是x轴,Y轴,z轴上的缩放程度,缩放中心是layer的锚点

4:CATransform3DScale(<#CATransform3D t#>, <#CGFloat sx#>, <#CGFloat sy#>, <#CGFloat sz#>)  功能与上一函数类似,但可以叠加一个CATransform3D效果

5:CATransform3DMakeTranslation(CGFloat tx, <#CGFloat ty#>, <#CGFloat tz#>)  用于平移

6:CATransform3DTranslate(<#CATransform3D t#>, <#CGFloat tx#>, <#CGFloat ty#>, <#CGFloat tz#>)

7:CATransform3DConcat(CATransform3D a, <#CATransform3D b#>)  将两个CATransform3D效果叠加起来

我们也可以通过KVC更方便的设置transform的属性

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

可以通过KVC设置以下与transform相关的属性

transform.rotation.x

transform.rotation.y

transform.rotation.z

transform.scale.x

transform.scale.y

transform.scale.z

transform.translation.x

transform.translation.y

transform.translation.z

更多CATransform3D原理性介绍内容可以自行网上搜索

 

CALayer事务

CALayer中"Animatable"属性变化都在CATrasaction的管理内,之前提到的属性支持隐式动画是指在某次Runroop中修改"Animatable"时,如果没有设置事务,则会自动创建一个CATransaction,并在当前线程的下一个RunLoop中commit这个CATransaction。

事务可以嵌套,当事务嵌套时,只有最外层的事务commit了之后整个动画才开始。

事务开启:[CATransaction begin]

事务提交:[CATransaction commit]

我们可以通过事务控制动画的时长,甚至禁止动画效果,还可以设置completionBlock,当当前CATransaction的所有动画执行结束后,,completionBlock会被调用

修改第一个例子

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    CGPoint point = [touch locationInView:self.view];
    
    //开启事务
    [CATransaction begin];
    
    //禁止动画效果
    //[CATransaction setDisableActions:YES];
    
    //设置动画时长3秒,默认0.25
    [CATransaction setValue:@3 forKey:kCATransactionAnimationDuration];
    
    //获取之前添加的layer
    CALayer *layer = [self.view.layer.sublayers lastObject];
    //设置layer的新位置为手指点击的位置
    layer.position = point;
    float width = layer.bounds.size.width;
    //设置layer的大小
    if (width == 120) {
        layer.bounds = CGRectMake(0,0,60,60);
        layer.cornerRadius = 30;
    } else {
        layer.bounds = CGRectMake(0,0,120,120);
        layer.cornerRadius = 60;
    }
    
    //提交事务
    [CATransaction commit];
}

 

CALayer时间系统

CALayer实现了CAMediaTiming协议,同时CAAnimation也实现了CAMediaTiming协议,这个协议提供了动画的持续时间,速度,和重复计数等属性,关于时间系统这一部分的具体操作可以放到动画中具体演示

原文地址:https://www.cnblogs.com/zanglitao/p/4043459.html