UIView动画真面目--CALayer

CALayer简介

在介绍动画操作之前我们必须先来了解一个动画中常用的对象CALayer。CALayer包含在QuartzCore框架中,这是一个跨平台的框架,既可以用在iOS中又可以用在Mac OS X中。在使用Core Animation开发动画的本质就是将CALayer中的内容转化为位图从而供硬件操作,所以要熟练掌握动画操作必须先来熟悉CALayer。
  在Core Animation中我们操作更多的则不再是UIView而是直接面对CALayer。下图描绘了CALayer和UIView的关系,在UIView中有一个layer属性作为根图层,根图层上可以放其他子图层,在UIView中所有能够看到的内容都包含在layer中:


UIView动画真面目--CALayer_第1张图片

CALayer常用属性

在iOS中CALayer的设计主要是了为了内容展示和动画操作,CALayer本身并不包含在UIKit中,它不能响应事件。由于CALayer在设计之初就考虑它的动画操作功能,CALayer很多属性在修改时都能形成动画效果,这种属性称为“隐式动画属性”。但是对于UIView的根图层而言属性的修改并不形成动画效果,因为很多情况下根图层更多的充当容器的做用,如果它的属性变动形成动画效果会直接影响子图层。另外,UIView的根图层创建工作完全由iOS负责完成,无法重新创建,但是可以往根图层中添加子图层或移除子图层。
下表列出了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 图层形变
  • 隐式属性动画的本质是这些属性的变动默认隐含了CABasicAnimation动画实现,详情大家可以参照Xcode帮助文档中“Animatable Properties”一节。
  • 在CALayer中很少使用frame属性,因为frame本身不支持动画效果,通常使用bounds和position代替。
  • CALayer中透明度使用opacity表示而不是alpha;中心点使用position表示而不是center。
  • anchorPoint属性是图层的锚点,范围在(01,01)表示在x、y轴的比例,这个点永远可以同position(中心点)重合,当图层中心点固定后,调整anchorPoint即可达到调整图层显示位置的作用(因为它永远和position重合)

为了进一步说明anchorPoint的作用,假设有一个层大小100*100,现在中心点位置(50,50),由此可以得出frame(0,0,100,100)。上面说过anchorPoint默认为(0.5,0.5),同中心点position重合,此时使用图形描述如图1;当修改anchorPoint为(0,0),此时锚点处于图层左上角,但是中心点poition并不会改变,因此图层会向右下角移动,如图2;然后修改anchorPoint为(1,1,),position还是保持位置不变,锚点处于图层右下角,此时图层如图3。


UIView动画真面目--CALayer_第2张图片

- (void)viewDidLoad {
[super viewDidLoad];
[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, WIDTH,WIDTH);
//设置圆角,当圆角半径等于矩形的一半时看起来就是一个圆形
layer.cornerRadius=WIDTH/2;
//设置阴影
layer.shadowColor=[UIColor grayColor].CGColor;
layer.shadowOffset=CGSizeMake(2, 2);
layer.shadowOpacity=.9;
//设置边框
//layer.borderColor=[UIColor whiteColor].CGColor;
//layer.borderWidth=1;

//设置锚点
//    layer.anchorPoint=CGPointZero;

[self.view.layer addSublayer:layer];

}
#pragma mark 点击放大
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch=[touches anyObject];
CALayer *layer=self.view.layer.sublayers[0];
CGFloat width=layer.bounds.size.width;
if (width==WIDTH) {
    width=WIDTH*4;
}else{
    width=WIDTH;
}
layer.bounds=CGRectMake(0, 0, width, width);
layer.position=[touch locationInView:self.view];
layer.cornerRadius=width/2;
}
@end 

运行效果:

UIView动画真面目--CALayer_第3张图片
MVVL

CALayer绘图

上一篇文章中重点讨论了使用Quartz 2D绘图,当时调用了UIView的drawRect:方法绘制图形、图像,这种方式本质还是在图层中绘制,但是这里会着重介绍一下如何直接在图层中绘图。在图层中绘图的方式跟原来基本没有区别,只是drawRect:方法是由UIKit组件进行调用,因此里面可以使用一些UIKit封装的方法进行绘图,而直接绘制到图层的方法由于并非UIKit直接调用因此只能用原生的Core Graphics方法绘制。
  图层绘图有两种方法,不管使用哪种方法绘制完必须调用图层的setNeedDisplay方法(注意是图层的方法,不是UIView的方法,前面我们介绍过UIView也有此方法)

  1. 通过图层代理drawLayer: inContext:方法绘制
  2. 通过自定义图层drawInContext:方法绘制
使用代理方法绘图

通过代理方法进行图层绘图只要指定图层的代理,然后在代理对象中重写*-(void)drawLayer:(CALayer )layer inContext:(CGContextRef)ctx方法即可。需要注意这个方法虽然是代理方法但是不用手动实现CALayerDelegate,因为CALayer定义中给NSObject做了分类扩展,所有的NSObject都包含这个方法。另外设置完代理后必须要调用图层的setNeedDisplay方法,否则绘制的内容无法显示。
  下面的代码演示了在一个自定义图层绘制一张图像并将图像设置成圆形,这种效果在很多应用中很常见,如最新版的手机QQ头像就是这种效果:

   - (void)viewDidLoad {
   [super viewDidLoad];

   //自定义图层
   CALayer *layer=[[CALayer alloc]init];
   layer.bounds=CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT);
   layer.position=CGPointMake(160, 200);
   layer.backgroundColor=[UIColor redColor].CGColor;
   layer.cornerRadius=PHOTO_HEIGHT/2;
   //注意仅仅设置圆角,对于图形而言可以正常显示,但是对于图层中绘制的图片无法正确显示
   //如果想要正确显示则必须设置masksToBounds=YES,剪切子图层
   layer.masksToBounds=YES;
   //阴影效果无法和masksToBounds同时使用,因为masksToBounds的目的就是剪切外边框,
   //而阴影效果刚好在外边框
     //    layer.shadowColor=[UIColor grayColor].CGColor;
     //    layer.shadowOffset=CGSizeMake(2, 2);
     //    layer.shadowOpacity=1;
   //设置边框
   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, -PHOTO_HEIGHT);

   UIImage *image=[UIImage imageNamed:@"photo.png"];
   //注意这个位置是相对于图层而言的不是屏幕
   CGContextDrawImage(ctx, CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT), image.CGImage);

     //    CGContextFillRect(ctx, CGRectMake(0, 0, 100, 100));
     //    CGContextDrawPath(ctx, kCGPathFillStroke);

   CGContextRestoreGState(ctx);
     }

     @end

运行效果:

UIView动画真面目--CALayer_第4张图片

  使用代理方法绘制图形、图像时在 drawLayer:inContext:方法中可以通过事件参数获得绘制的图层和图形上下文。在这个方法中绘图时所有的位置都是相对于图层而言的,图形上下文指的也是当前图层的图形上下文。
  需要注意的是上面代码中绘制图片圆形裁切效果时如果不设置masksToBounds是无法显示圆形,但是对于其他图形却没有这个限制。原因就是当绘制一张图片到图层上的时候会重新创建一个图层添加到当前图层,这样一来如果设置了圆角之后虽然底图层有圆角效果,但是子图层还是矩形,只有设置了masksToBounds为YES让子图层按底图层剪切才能显示圆角效果。同样的,有些朋友经常在网上提问说为什么使用UIImageView的layer设置圆角后图片无法显示圆角,只有设置masksToBounds才能出现效果,也是类似的问题。


扩展1--带阴影效果的圆形图片裁切

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

CGPoint position= CGPointMake(160, 200);
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.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, -PHOTO_HEIGHT);

UIImage *image=[UIImage imageNamed:@"photo.jpg"];
//注意这个位置是相对于图层而言的不是屏幕
CGContextDrawImage(ctx, CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT), image.CGImage);

//    CGContextFillRect(ctx, CGRectMake(0, 0, 100, 100));
//    CGContextDrawPath(ctx, kCGPathFillStroke);

CGContextRestoreGState(ctx);
}

运行效果:


UIView动画真面目--CALayer_第5张图片

扩展2--图层的形变

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

CGPoint position= CGPointMake(160, 200);
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.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:@"photo.jpg"];
//注意这个位置是相对于图层而言的不是屏幕
CGContextDrawImage(ctx, CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT), image.CGImage);
}

你可能感兴趣的:(UIView动画真面目--CALayer)