CoreAnimation之常用图层

CoreAnimation之CALayer基础
CoreAnimation之变换

1. CAShapeLayer

CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类,使用CAShapeLayer有以下一些优点:

  • 渲染快速——CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多

  • 高效使用内存——一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存

  • 不会被图层边界剪裁掉——一个CAShapeLayer可以在边界之外绘制。你的图层路径不会像在使用Core Graphics的普通CALayer一样被剪裁掉

  • 不会出现像素化——当你给CAShapeLayer做3D变换时,它不像一个有寄宿图的普通图层一样变得像素化

CAShapeLayer可以用来绘制所有能够通过CGPath来表示的形状。这个形状不一定要闭合,图层路径也不一定要不可破,事实上你可以在一个图层上绘制好几个不同的形状。你可以控制一些属性比如lineWith,lineCap,和lineJoin。但是在图层层面你只有一次机会设置这些属性,如果你想用不同颜色或风格来绘制多个形状,就不得不为每个形状准备一个图层了。

说了这么多,CAShapeLayer到底能用来做什么呢?

  • CAShapeLayer实现视图的部分圆角:
-(void)drawCorner{
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 100.0f, 100.0f)];
    view.center = self.view.center;
    view.backgroundColor = [UIColor blackColor];
    [self.view addSubview:view];
    
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:view.frame byRoundingCorners:UIRectCornerTopLeft | UIRectCornerBottomRight cornerRadii:CGSizeMake(30.0f, 30.0f)];
    
    CAShapeLayer *layer = [[CAShapeLayer alloc] init];
    layer.bounds = view.frame;
    layer.position = CGPointMake(50.0f, 50.0f);
    layer.path = path.CGPath;
    
    view.layer.mask = layer;
}

运行效果:


Paste_Image.png
  • CAShapeLayer实现一个呆萌的火柴人:
-(void)drawMatchman{
    CGFloat radius = 25.0f;             //半径
    
    UIBezierPath *path = [[UIBezierPath alloc] init];
    
    CGPoint point1 = CGPointMake(self.view.center.x + radius, self.view.center.y);
    [path moveToPoint:point1];          //将画笔移动到point1
    [path addArcWithCenter:self.view.center radius:radius startAngle:0.0f endAngle:2.0f*M_PI clockwise:YES];        //画一个圆代表火柴人的头
    
    CGPoint point2 = CGPointMake(point1.x - radius, point1.y + radius);
    [path moveToPoint:point2];          //将画笔移动到point2,准备画身体
    
    CGPoint point3 = CGPointMake(point2.x, point2.y+50.0f);
    [path addLineToPoint:point3];       //画一根长为50的竖线代表火柴人的身体,起点是point2,终点是point3
    
    CGPoint point4 = CGPointMake(point3.x - radius, point3.y+25.0f);
    [path addLineToPoint:point4];       //画一根长为25的左斜线代表火柴人的左脚,起点是point3,终点是point4
    
    [path moveToPoint:point3];          //将画笔移动到point3,准备画右脚
    CGPoint point5 = CGPointMake(point3.x + radius, point3.y+25.0f);
    [path addLineToPoint:point5];       //画一根长为25的右斜线代表火柴人的右脚,起点是point3,终点是point5
    
    //最后画一根横线,代表火柴人的手
    CGPoint point6 = CGPointMake(point2.x - radius, point2.y + 25.0f);
    [path moveToPoint:point6];
    [path addLineToPoint:CGPointMake(point6.x + 50.0f, point6.y)];
    
    shapeLayer = [CAShapeLayer layer];
    shapeLayer.strokeColor = [UIColor redColor].CGColor;    //画笔颜色
    shapeLayer.fillColor = [UIColor clearColor].CGColor;    //填充色
    shapeLayer.lineWidth = 6.0f;    //线条宽度
    shapeLayer.lineJoin = kCALineJoinRound; //线条连接处的样式
    shapeLayer.lineCap = kCALineCapRound;   //线条末端处的样式
    shapeLayer.path = path.CGPath;
    [self.view.layer addSublayer:shapeLayer];
    
    [self addFlagPoint:point1];
    [self addFlagPoint:point2];
    [self addFlagPoint:point3];
    [self addFlagPoint:point4];
    [self addFlagPoint:point5];
    [self addFlagPoint:point6];
}
-(void)addFlagPoint:(CGPoint)aPoint{
    UIView *flag = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 6.0f, 6.0f)];
    flag.center = aPoint;
    flag.layer.cornerRadius = 3.0f;
    flag.backgroundColor = [UIColor blackColor];
    [self.view addSubview:flag];
}

运行效果:


Paste_Image.png
  • CAShapeLayer实现一个扇形动画:
-(void)drawCircular{
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 100.0f, 100.0f)];
    view.center = self.view.center;
    UIImage *image = [UIImage imageNamed:@"3"];
    view.layer.contents = (__bridge id _Nullable)(image.CGImage);
    view.layer.contentsGravity = kCAGravityCenter;
    view.layer.contentsScale = [UIScreen mainScreen].scale;
    [self.view addSubview:view];
    
    shapeLayer = [CAShapeLayer layer];
    shapeLayer.frame = view.bounds;
    shapeLayer.strokeEnd = 0.0f;
    shapeLayer.strokeStart = 0.0f;
    UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:view.bounds];
    shapeLayer.path = path.CGPath;
    shapeLayer.fillColor = [UIColor clearColor].CGColor;
    shapeLayer.lineWidth = 100.0f;
    shapeLayer.strokeColor = [UIColor redColor].CGColor;
    view.layer.mask = shapeLayer;
    [NSTimer scheduledTimerWithTimeInterval:0.005f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}
-(void)timerAction{
    static BOOL flag = NO;
    if (shapeLayer.strokeEnd >= 1.5f) {
        flag = YES;
    }
    if (shapeLayer.strokeEnd <= -0.5f) {
        flag = NO;
    }
    if (flag) {
        shapeLayer.strokeEnd -= 0.005f;
    }else{
        shapeLayer.strokeEnd += 0.005f;
    }
}

运行效果:

running.gif

更多关于CAShapeLayer的动画:动画黄金搭档:CADisplayLink & CAShapeLayer


2. CATransformLayer

之前我们在CoreAnimation之变换中构造了一个残缺的正方体,最后在旋转正方体的时候遇到了问题,原因在于CALayer是扁平的,所以直接将superLayer绕y轴旋转的时候看不出正方体的3D效果

CoreAnimation有一个专用图层叫CATransformLayer,它是CALayer的子类,但是不同于普通的CALayer,因为它不能显示它自己的内容,只有当存在了一个能作用域子图层的变换它才真正存在,而且CATransformLayer并不平面化它的子图层,所以它能够用于构造一个层级的3D结构

总之一句话,CATransformLayer相当于一个容器,一个3D的容器

这次我们依然以构建一个正方体为例,开始写代码前,我们不妨在脑袋里先构造一下这个正方体:

  • 首先,有六块木板,都是平放在一个3D空间里

  • 第一步,构建上下两面,把上面这块木板往上移动50个点(即沿z轴移动50个点);再把下面这块木板往下移动50个点(即沿z轴移动-50个点),这样就构建出了上下两面

  • 第二步,构建左右两面,将左面这块木板往左边直立起来(即沿y轴旋转-90度),再将左面这块木板往上移动50个点(即沿z轴移动50个点);同理,右面这块木板就是先往右边直立起来再往上移动

  • 第三步,构建前后两面,将前面这块木板往前边直立起来(即沿x轴旋转-90度),再将前面这块木板往上移动50个点(即沿z轴移动50个点);同理,后面这块木板就是先往后边直立起来再往上移动

代码如下(楼主也是初学,为了逻辑清晰,就没有封装方法,直接一个面一个方法,所以代码有点多哈):

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController{
    CATransformLayer *transformLayer;
    NSTimer *timer;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    CATransform3D sublayerTransform = CATransform3DIdentity;
    sublayerTransform.m34 = -1.0f/500.0f;
    self.view.layer.sublayerTransform = sublayerTransform;
    
    transformLayer = [CATransformLayer layer];
    transformLayer.position = self.view.layer.position;
    
    CATransform3D transform = CATransform3DIdentity;
    transform = CATransform3DRotate(transform, -M_PI_4, 1.0f, 0.0f, 0.0f);
    transform = CATransform3DRotate(transform, -M_PI_4, 0.0f, 1.0f, 0.0f);
    transformLayer.transform = transform;
    
    [self.view.layer addSublayer:transformLayer];
    
    [self addFace1];
    [self addFace2];
    [self addFace3];
    [self addFace4];
    [self addFace5];
    [self addFace6];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [timer invalidate];
    timer = nil;
    timer = [NSTimer scheduledTimerWithTimeInterval:0.025f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}

-(void)timerAction{
    CATransform3D transform = transformLayer.transform;
    transform = CATransform3DRotate(transform, 1.0f/180.0f*M_PI, 1.0f, 1.0f, 0.0f);
    transformLayer.transform = transform;
}

//上面 ———— 把上面这块木板往上移动50个点(即沿z轴移动50个点)
-(void)addFace1{
    CALayer *layer = [[CALayer alloc] init];
    layer.bounds = CGRectMake(0.0f, 0.0f, 100.0f, 100.0f);
    layer.backgroundColor = [UIColor redColor].CGColor;
    CATransform3D transform = CATransform3DIdentity;
    transform = CATransform3DTranslate(transform, 0.0f, 0.0f, 50.0f);
    layer.transform = transform;
    [transformLayer addSublayer:layer];
}

//下面 ———— 把下面这块木板往下移动50个点(即沿z轴移动-50个点)
-(void)addFace2{
    CALayer *layer = [[CALayer alloc] init];
    layer.bounds = CGRectMake(0.0f, 0.0f, 100.0f, 100.0f);
    layer.backgroundColor = [UIColor orangeColor].CGColor;
    CATransform3D transform = CATransform3DIdentity;
    transform = CATransform3DTranslate(transform, 0.0f, 0.0f, -50.0f);
    layer.transform = transform;
    [transformLayer addSublayer:layer];
}

//左面
-(void)addFace3{
    CALayer *layer = [[CALayer alloc] init];
    layer.bounds = CGRectMake(0.0f, 0.0f, 100.0f, 100.0f);
    layer.backgroundColor = [UIColor yellowColor].CGColor;
    CATransform3D transform = CATransform3DIdentity;
    //将左面这块木板往左边直立起来(即沿y轴旋转-90度)
    transform = CATransform3DRotate(transform, -M_PI_2, 0.0f, 1.0f, 0.0f);
    //再将左面这块木板往上移动50个点(即沿z轴移动50个点)
    transform = CATransform3DTranslate(transform, 0.0f, 0.0f, 50.0f);
    layer.transform = transform;
    [transformLayer addSublayer:layer];
}

//右面
-(void)addFace4{
    CALayer *layer = [[CALayer alloc] init];
    layer.bounds = CGRectMake(0.0f, 0.0f, 100.0f, 100.0f);
    layer.backgroundColor = [UIColor greenColor].CGColor;
    CATransform3D transform = CATransform3DIdentity;
    //将右面这块木板往右边直立起来(即沿y轴旋转90度)
    transform = CATransform3DRotate(transform, M_PI_2, 0.0f, 1.0f, 0.0f);
    //再将右面这块木板往上移动50个点(即沿z轴移动50个点)
    transform = CATransform3DTranslate(transform, 0.0f, 0.0f, 50.0f);
    layer.transform = transform;
    [transformLayer addSublayer:layer];
}

//前面
-(void)addFace5{
    CALayer *layer = [[CALayer alloc] init];
    layer.bounds = CGRectMake(0.0f, 0.0f, 100.0f, 100.0f);
    layer.backgroundColor = [UIColor blueColor].CGColor;
    CATransform3D transform = CATransform3DIdentity;
    //将前面这块木板往前边直立起来(即沿x轴旋转-90度)
    transform = CATransform3DRotate(transform, -M_PI_2, 1.0f, 0.0f, 0.0f);
    //再将前面这块木板往上移动50个点(即沿z轴移动50个点)
    transform = CATransform3DTranslate(transform, 0.0f, 0.0f, 50.0f);
    layer.transform = transform;
    [transformLayer addSublayer:layer];
}

//后面
-(void)addFace6{
    CALayer *layer = [[CALayer alloc] init];
    layer.bounds = CGRectMake(0.0f, 0.0f, 100.0f, 100.0f);
    layer.backgroundColor = [UIColor purpleColor].CGColor;
    CATransform3D transform = CATransform3DIdentity;
    //将后面这块木板往后边直立起来(即沿x轴旋转90度)
    transform = CATransform3DRotate(transform, M_PI_2, 1.0f, 0.0f, 0.0f);
    //再将后面这块木板往上移动50个点(即沿z轴移动50个点)
    transform = CATransform3DTranslate(transform, 0.0f, 0.0f, 50.0f);
    layer.transform = transform;
    [transformLayer addSublayer:layer];
}

@end

以前在CALayer上旋转正方体的时候我们要这样写(即把所有的子图层挨个儿做一次变换):

-(void)timerAction{
    static CGFloat angle = 1.0f;
    CATransform3D transform3d = self.containerView.layer.sublayerTransform;
    transform3d = CATransform3DRotate(transform3d, angle/180.0f*M_PI, 0.0f, 1.0f, 0.0f);
    self.containerView.layer.sublayerTransform = transform3d;
}

而现在在CATransformLayer上旋转正方体,只需要将CATransformLayer绕x轴或者绕y轴旋转就行了:

-(void)timerAction{
    CATransform3D transform = transformLayer.transform;
    transform = CATransform3DRotate(transform, 1.0f/180.0f*M_PI, 1.0f, 1.0f, 0.0f);
    transformLayer.transform = transform;
}

运行效果:

running.gif

3. CAGradientLayer

CAGradientLayer可以用来实现渐变效果:

    CAGradientLayer *layer = [CAGradientLayer layer];
    layer.bounds = CGRectMake(0.0f, 0.0f, 150.0f, 150.0f);
    layer.position = self.view.layer.position;
    layer.colors = @[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor greenColor].CGColor,(__bridge id)[UIColor blueColor].CGColor];
    layer.locations = @[@.25,@0.5,@0.75];
    layer.startPoint = CGPointMake(0.0f, 0.0f);
    layer.endPoint = CGPointMake(1.0f, 0.0f);
    [self.view.layer addSublayer:layer];

运行效果:


Paste_Image.png

需要特别说明一下locations这个属性,locations数组里面装的是相对位置,这个相对位置必须是单调递增的,但是这个位置并不是代表颜色的位置,而是说从这个位置开始,将要开始渐变成下一个颜色了

拿示例代码来说,从0.25开始,将要由红变绿了,从0.5开始将要由绿变蓝了,从0.75开始又要开始下一个渐变了,但是由于没有下一个颜色了,所以后面全是蓝色,你可以在示例代码的colors里面再添加一个颜色试试


4. CAReplicatorLayer

学习CAReplicatorLayer之前,我们再来复习一下变换的顺序:

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController{
    CALayer *layer;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    layer = [CALayer layer];
    layer.backgroundColor = [UIColor cyanColor].CGColor;
    UIImage *image = [UIImage imageNamed:@"3"];
    layer.frame = CGRectMake(110.0f, 100.0f, 100.0f, 100.0f);
    layer.contents = (__bridge id)image.CGImage;
    layer.contentsGravity = kCAGravityResizeAspect;
    layer.contentsScale = [UIScreen mainScreen].scale;
    [self.view.layer addSublayer:layer];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self rotate];
}

-(void)rotate{
    __weak __typeof__(self) weakSelf = self;
    [UIView animateWithDuration:3.0f animations:^{
        CATransform3D transform = layer.transform;
        transform = CATransform3DRotate(transform, M_PI, 0.0f, 0.0f, 1.0f);
        layer.transform = transform;
    } completion:^(BOOL finished) {
        [weakSelf translate];
    }];
}

-(void)translate{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [UIView animateWithDuration:3.0f animations:^{
            CATransform3D transform = layer.transform;
            transform = CATransform3DTranslate(transform, 0.0f, 100.0f, 0.0f);
            layer.transform = transform;
        }];
    });
}

@end

运行效果


running.gif

注意这句代码:

transform = CATransform3DTranslate(transform, 0.0f, 100.0f, 0.0f);

明明是沿y轴移动200个点,为啥运行的时候会往上跑呢❓

原因在于,在移动之前,我们已经绕z轴旋转过了:

transform = CATransform3DRotate(transform, M_PI, 0.0f, 0.0f, 1.0f);

旋转之后的layer,其相对于superLayer的坐标系已经发生了改变

拿示例代码来说,绕z轴旋转180度之后,x轴和y轴的方向都已经变成与原来相反的方向了

变换的顺序,在CAReplicatorLayer中体现得尤为明显,因为CAReplicatorLayer的instance的变换是逐步增加的,每个实例都是相对于前一实例布局

CAReplicatorLayer实现一个带倒影的ImageView:

#import "ReplicatorImageView.h"

@implementation ReplicatorImageView

+ (Class)layerClass{
    return [CAReplicatorLayer class];
}

- (void)setUp{
    CAReplicatorLayer *layer = (CAReplicatorLayer *)self.layer;
    layer.instanceCount = 2;
    CATransform3D transform = CATransform3DIdentity;
    transform = CATransform3DScale(transform, 1, -1, 0);
    transform = CATransform3DTranslate(transform, 0.0f, -self.frame.size.height, 0.0f);
    layer.instanceTransform = transform;
    layer.instanceAlphaOffset = -0.6;
    
    CALayer *imageLayer = [CALayer layer];
    imageLayer.frame = layer.bounds;
    imageLayer.contents = (__bridge id _Nullable)(self.image.CGImage);
    imageLayer.contentsScale = [UIScreen mainScreen].scale;
    imageLayer.contentsGravity = kCAGravityResizeAspect;
    [layer addSublayer:imageLayer];
}

-(void)setReplicatorImage:(UIImage *)replicatorImage{
    self.image = replicatorImage;
    [self setUp];
}

@end
- (void)viewDidLoad {
    [super viewDidLoad];
    
    ReplicatorImageView *imageView = [[ReplicatorImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 150.0f, 150.0f)];
    imageView.center = self.view.center;
    [self.view addSubview:imageView];
    
    UIImage *image = [UIImage imageNamed:@"3"];
    imageView.replicatorImage = image;
}

运行效果:

Paste_Image.png

instanceCount指定了总共要复制多少个图层(包含本身)

instanceAlphaOffset = -0.6f; 即当前图层实例的alpha值 = 上一个图层实例的alpha - 0.6f,与之类似的属性还有这些:

/* The color components added to the color of instance k-1 to produce
 * the modulation color of instance k. Defaults to the clear color (no
 * change). Animatable. */

@property float instanceRedOffset;
@property float instanceGreenOffset;
@property float instanceBlueOffset;

更多CAReplicatorLayer动画:基于CAReplicatorLayer的炫酷动画

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容