老司机带你走进Core Animation 之图层的透视、渐变及复制

老司机带你走进Core Animation 之图层的透视、渐变及复制

系列文章:


这回呢,当然还是顺着头文件里面的几个类,老司机一个一个捋吧。

老司机的想法就是要把CoreAnimation头文件中的类大概都说一遍,毕竟一开始把系列名定成了《老司机带你走进CoreAnimation》(深切的觉得自己给自己坑了。。。)。


我给自己挖的坑

所以呢,在今天的博客里你将会看到以下截个内容

  • CATransform3D
  • CATransformLayer
  • CAGradientLayer
  • CAReplicatorLayer
  • DWMirrorView

废话不多说,直接进入主题。


CATransform3D

先介绍一下CATransform3D吧。

CATransform3D

正如上图所示,我们可以清晰的看到,CATransform3D是一个结构体。而且苹果很友好的调整了一下书写格式,正如你看到的,它像是一个4 X 4的矩阵。

事实上他的原理就是一个4 X 4矩阵

其实他还有一个弟弟,CGAffineTransform。这是一个3 X 3的矩阵。
他们的作用都一样,进行坐标变换。
不同点在于,CATransform3D作用与3维坐标系的坐标变换,CGAffineTransform作用于2维坐标系的坐标变换。

所以CGAffineTransform用于对UIView进行变换,而CATransform3D用于对CALayer进行变换。

虽然老司机从小到大都是数学课代表,不过我要很郑重的告诉你,数学是一门靠悟性的学问,不是我讲给你听,你就能消化的,所以关于矩阵计算什么的,请各位同学自己消化理解(咳咳,我会告诉你我高数、线代、概率没有一科过70的么=。=)


一脸无辜

所以呢,老司机直接来介绍CATransform3D的相关api吧。(CGAffineTransform的api与CATransform3D相似,可类比使用)。

  • CATransform3DIdentity

生成一个无任何变换的默认矩阵,可用于使变换后的Layer恢复初始状态


  • CATransform3DMakeTranslation
  • CATransform3DMakeScale
  • CATransform3DMakeRotation

分别是平移、缩放、旋转,这三个api最大的相同点就在于函数名中都有Make。意思就是生成指定变换的矩阵。与之相对的就是下面的api👇

  • CATransform3DTranslate
  • CATransform3DScale
  • CATransform3DRotate

与之前三个api的不同点在于,这三个api都多了一个参数,同样是一个CATransform3D结构体。我想你一定猜到了,就是对给定的矩阵在其现有基础上进行指定的变换。

值得注意的是,以上两个旋转api中x/y/z三个参数均为指定旋转轴,可选值0和1,0代表此轴不做旋转1代表作旋转。例如想对x、y轴做45度旋转,则angle = M____PI____4,x = 1,y = 1,z = 0。另外,此处旋转角度为弧度制哦,不是角度制。


  • CATransform3DConcat

返回两个矩阵的乘积。


  • CATransform3DInvert

反转矩阵


  • CATransform3DMakeAffineTransform
  • CATransform3DGetAffineTransform

CGAffineTransform与CATransform3D相互转化的api


  • CATransform3DIsIdentity
  • CATransform3DEqualToTransform
  • CATransform3DIsAffine

这三个api见名知意了,不多说。

哦,重要的一点你一定要知道,所有的矩阵变换都是相对于图层的锚点进行的。还记得锚点的概念么?不记得可以去这个系列的第一篇文章补课哦。

其实呢,关于CATransform3D你只要会使用以上api对图层做3维坐标转换就够了。不过上述这些变换默认情况下都是不具备透视效果的,因为你所看到的都是图层在x轴y轴上的投影,那想要透视效果怎么办呢?两个办法,CATranformLayer,以及M34。

M34

老司机说过,CATransform3D不过是一个4 X 4的矩阵。那么其实这16个数字中,每一个数字都有自己可以控制的转换,这是纯数学知识啊,自己领悟=。=不过老司机可以单独说说M34这个数。这个数是用来控制图层变换后的景深效果的,也就是透视效果

M34

上面的图片分别展示了具有透视效果的旋转及动画。

代码上的体现就是

    CALayer * staticLayerA = [CALayer layer];
    staticLayerA.bounds = CGRectMake(0, 0, 100, 100);
    staticLayerA.position = CGPointMake(self.view.center.x - 75, self.view.center.y - 100);
    staticLayerA.backgroundColor = [UIColor redColor].CGColor;
    [self.view.layer addSublayer:staticLayerA];
    
    CATransform3D transA = CATransform3DIdentity;
    transA.m34 = - 1.0 / 500;
    transA = CATransform3DRotate(transA, M_PI / 3, 1, 0, 0);
    staticLayerA.transform = transA;

使用上很简单,代码里M34 = - 1.0 / 500 的意思就是图层距离屏幕向里500的单位。如果向外则是M34 = 1.0 / 500。这个距离至一般掌握至500~1000这个范围会取得不错的效果。

这里需要注意的是M34的赋值一定要写在矩阵变换前面,具体为什么说实话老司机也不知道。


CATransformLayer

老司机上面提到过,CALayer做矩阵变换你能看到的只是他在XY轴上的投影,这时你若想看到透视效果,就需要使用到M34或CATransformLayer。其实他两个又有一些区别,CATransformLayer是让你看到的不只是其在XY轴上的投影。

说起来不好懂,看下面的图吧。

TransformLayer

CATransformLayer可以让其的子视图各自现实自身的真实形状,而不是其在父视图的投影

你可能还不懂,其实你看的正方体是六个CALayer经过矩阵变换拼成的实实在在的正方体。

    //create cube layer
    CATransformLayer *cube = [CATransformLayer layer];
    
    //add cube face 1
    CATransform3D ct = CATransform3DMakeTranslation(0, 0, 50);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    //add cube face 2
    ct = CATransform3DMakeTranslation(50, 0, 0);
    ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    //add cube face 3
    ct = CATransform3DMakeTranslation(0, -50, 0);
    ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    //add cube face 4
    ct = CATransform3DMakeTranslation(0, 50, 0);
    ct = CATransform3DRotate(ct, -M_PI_2, 1, 0, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    //add cube face 5
    ct = CATransform3DMakeTranslation(-50, 0, 0);
    ct = CATransform3DRotate(ct, -M_PI_2, 0, 1, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    //add cube face 6
    ct = CATransform3DMakeTranslation(0, 0, -50);
    ct = CATransform3DRotate(ct, M_PI, 0, 1, 0);
    [cube addSublayer:[self faceWithTransform:ct]];
    
    //center the cube layer within the container
    CGSize containerSize = self.containerView.bounds.size;
    cube.position = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
- (CALayer *)faceWithTransform:(CATransform3D)transform
{
    //create cube face layer
    CALayer *face = [CALayer layer];
    face.bounds = CGRectMake(0, 0, 100, 100);
    //apply a random color
    CGFloat red = (rand() / (double)INT_MAX);
    CGFloat green = (rand() / (double)INT_MAX);
    CGFloat blue = (rand() / (double)INT_MAX);
    face.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
    face.transform = transform;
    return face;
}

使用起来就是这么简单,把各个变换后的layer加入到CATransformLayer中就可以了。

本身CATransformLayer不具有任何其他属性,其实他更像是一个容器。它本身至渲染其子图层,自身没有任何layer的属性。

最重要的一点是,当图层加入到CATransformLayer中以后,hitTest和convertPoint两个方法就失效了,请注意这点。


CAGradientLayer

CAGradientLayer本身的属性也比较少,而且完全是针对于过渡颜色来的。

  • colors

图层显示的所有颜色的数组


  • locations

每个颜色对应的位置。注意,这个位置指的是颜色的位置,而不是过渡线的位置。


  • startPoint
  • endPoint

是颜色过渡的方向,会沿着起点到终点的向量进行过渡。


  • type

过渡模式,当前苹果给我们暴露的只有一种模式,kCAGradientLayerAxial。

需要说明的是,CAGradientLayer只能做矩形的渐变图层

你要怎么做?

所以说这个效果要如何实现呢?其实啊,这只是一个错觉,看这个。

矩形渐变层

所以说看到这你就知道了吧,两个拼一起的CAGradientLayer,然后用一个shapeLayer做了一个mask就成了环形的过渡层了。这一招老司机早就做了过,还记得么,歌词显示那一章

- (void)viewDidLoad {
    [super viewDidLoad];

    UIBezierPath * path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(50, 50) radius:45 startAngle:- 7.0 / 6 * M_PI endAngle:M_PI / 6 clockwise:YES];
    
    [self.view.layer addSublayer:[self createShapeLayerWithPath:path]];
    
    CAGradientLayer * leftL = [self createGradientLayerWithColors:@[(id)[UIColor redColor].CGColor,(id)[UIColor yellowColor].CGColor]];
    leftL.position = CGPointMake(25, 40);
    
    CAGradientLayer * rightL = [self createGradientLayerWithColors:@[(id)[UIColor greenColor].CGColor,(id)[UIColor yellowColor].CGColor]];
    rightL.position = CGPointMake(75, 40);
    
    CALayer * layer = [CALayer layer];
    layer.bounds = CGRectMake(0, 0, 100, 80);
    layer.position = self.view.center;
    [layer addSublayer:leftL];
    [layer addSublayer:rightL];
    [self.view.layer addSublayer:layer];
    
    CAShapeLayer * mask = [self createShapeLayerWithPath:path];
    mask.position = CGPointMake(50, 40);
    layer.mask = mask;
    mask.strokeEnd = 0;
    self.mask = mask;
}

-(CAShapeLayer *)createShapeLayerWithPath:(UIBezierPath *)path
{
    CAShapeLayer * layer = [CAShapeLayer layer];
    layer.path = path.CGPath;
    layer.bounds = CGRectMake(0, 0, 100, 75);
    layer.position = self.view.center;
    layer.fillColor = [UIColor clearColor].CGColor;
    layer.strokeColor = [UIColor colorWithRed:33 / 255.0 green:192 / 255.0 blue:250 / 255.0 alpha:1].CGColor;
    layer.lineCap = @"round";
    layer.lineWidth = 10;
    return layer;
}

-(CAGradientLayer *)createGradientLayerWithColors:(NSArray *)colors
{
    CAGradientLayer * gradientLayer = [CAGradientLayer layer];
    gradientLayer.colors = colors;
    gradientLayer.locations = @[@0,@0.8];
    gradientLayer.startPoint = CGPointMake(0, 1);
    gradientLayer.endPoint = CGPointMake(0, 0);
    gradientLayer.bounds = CGRectMake(0, 0, 50, 80);
    return gradientLayer;
}

CAReplicatorLayer

CAReplicatorLayer官方的解释是一个高效处理复制图层的中间层。他能复制图层的所有属性包括动画

使用起来很简单,从他的属性一一看:

  • instanceCount

实例数,复制后的实例数。

  • preservesDepth

这是一个bool值,默认为No,如果设为Yes,将会具有3维透视效果。

  • instanceDelay
  • instanceTransform
  • instanceColor
  • instanceRedOffset
  • instanceGreenOffset
  • instanceBlueOffset
  • instanceAlphaOffset

这一排属性都是每一个实例与上一个实例对应属性的偏移量。

还是上代码吧,直观点。

//     Do any additional setup after loading the view.
        CALayer * layer = [CALayer layer];
        layer.bounds = CGRectMake(0, 0, 30, 30);
        layer.position = CGPointMake(self.view.center.x - 50, self.view.center.y - 50);
        layer.backgroundColor = [UIColor redColor].CGColor;
        layer.cornerRadius = 15;
        [self.view.layer addSublayer:layer];
    
        CABasicAnimation * animation1 = [CABasicAnimation animationWithKeyPath:@"opacity"];
        animation1.fromValue = @(0);
        animation1.toValue = @(1);
        animation1.duration = 1.5;
    //    animation1.autoreverses = YES;
    
        CABasicAnimation * animation2 = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
        animation2.toValue = @(1.5);
        animation2.fromValue = @(0.5);
        animation2.duration = 1.5;
    //    animation2.autoreverses = YES;
    
        CAAnimationGroup * ani = [CAAnimationGroup animation];
        ani.animations = @[animation1,animation2];
        ani.duration = 1.5;
        ani.repeatCount = MAXFLOAT;
        ani.autoreverses = YES;
    
        [layer addAnimation:ani forKey:nil];
    
        CAReplicatorLayer * rec = [CAReplicatorLayer layer];
        [rec addSublayer:layer];
        rec.instanceCount = 3;
        rec.instanceDelay = 0.5;
        rec.instanceTransform = CATransform3DMakeTranslation(50, 0, 0);
        [self.view.layer addSublayer:rec];
    
        CAReplicatorLayer * rec2 = [CAReplicatorLayer layer];
        [rec2 addSublayer:rec];
        rec2.instanceCount = 3;
        rec2.instanceDelay = 0.5;
        rec2.instanceTransform = CATransform3DMakeTranslation(0, 50, 0);
        [self.view.layer addSublayer:rec2];

正如你所看到的,CAReplicatorLayer支持嵌套使用。它的效果是如下这个样子的。

ReplicatorLayer

啧啧啧,没想到今天的内容就这么讲完了。

额,内容比较少,的确是今天讲的这几个比较简单。我知道只是这样你们是不会放过我的。那我就放一个这几个属性联合起来的一个小应用吧。

Mirror.gif

忽略倒影的层次感吧,截图问题,正常是一个梯度渐变下去的。

其实拿到一个需求,我们先分析一下想要实现他的步骤,这个过程对于开发其实是很重要的。

首先来说,我们看到的倒影,我们应该可以考虑CAReplicator做一个复制图层,配合instranceTransform属性做出倒影效果

然后来说,我们看到了倒影渐变效果,我们应该想到的是使用CAGradientLayer去实现过渡效果。

最后一些细节的参数我们可以根据需求去进行相关设置。

分析过后其实思路还是挺清晰的,一步步实现就好。
实现起来很简单,代码量也不多,我就直接放代码就好了。

#pragma mark ---tool method---

-(void)handleMirrorDistant:(CGFloat)distant
{
    CAReplicatorLayer * layer = (CAReplicatorLayer *)self.layer;
    CATransform3D transform = CATransform3DIdentity;
    transform = CATransform3DTranslate(transform, 0, distant + self.bounds.size.height, 0);
    transform = CATransform3DScale(transform, 1, -1, 0);
    layer.instanceTransform = transform;
}

-(NSArray *)getMaskLayerLocations
{
    CGFloat height = self.bounds.size.height * 2 + self.mirrorDistant;
    CGFloat mirrowScale = self.bounds.size.height * (1 + self.mirrorScale) + self.mirrorDistant;
    return @[@0,@((self.bounds.size.height + self.mirrorDistant) / height),@(mirrowScale / height)];
}

-(CGFloat)safeValueBetween0And1:(CGFloat)value
{
    if (value > 1) {
        value = 1;
    } else if (value < 0) {
        value = 0;
    }
    return value;
}

-(void)valueInit
{
    self.mirrorDistant = 0;
    self.mirrorScale = 0.5;
    self.mirrored = YES;
    self.dynamic = YES;
    self.mirrorAlpha = 0.5;
}

#pragma mark ---override---

-(instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self valueInit];
    }
    return self;
}

-(void)awakeFromNib
{
    [super awakeFromNib];
    [self valueInit];
}

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

-(void)drawRect:(CGRect)rect
{
    [super drawRect:rect];
    CAReplicatorLayer * layer = (CAReplicatorLayer *)self.layer;
    if (self.mirrored) {
        if (self.dynamic) {
            [self.mirrorImageView removeFromSuperview];
            self.mirrorImageView = nil;
            layer.instanceCount = 2;
            if (CATransform3DEqualToTransform(layer.instanceTransform, CATransform3DIdentity)) {
                [self handleMirrorDistant:self.mirrorDistant];
            }
        }
        else
        {
            layer.instanceCount = 1;
            CGSize size = CGSizeMake(self.bounds.size.width, self.bounds.size.height * self.mirrorScale);
            if (size.height > 0.0f && size.width > 0.0f)
            {
                UIGraphicsBeginImageContextWithOptions(size, NO, 0.0f);
                CGContextRef context = UIGraphicsGetCurrentContext();
                CGContextScaleCTM(context, 1.0f, -1.0f);
                CGContextTranslateCTM(context, 0.0f, -self.bounds.size.height);
                [self.layer renderInContext:context];
                self.mirrorImageView.image = UIGraphicsGetImageFromCurrentImageContext();
                UIGraphicsEndImageContext();
            }
            self.mirrorImageView.alpha = self.mirrorAlpha;
            self.mirrorImageView.frame = CGRectMake(0, self.bounds.size.height + self.mirrorDistant, size.width, size.height);
        }
        self.layer.mask = self.maskLayer;
    }
    else
    {
        layer.instanceCount = 1;
        [self.mirrorImageView removeFromSuperview];
        self.mirrorImageView = nil;
        self.layer.mask = nil;
    }
    
}

#pragma mark ---setter/getter---

-(void)setMirrored:(BOOL)mirrored
{
    _mirrored = mirrored;
    [self setNeedsDisplay];
}

-(void)setDynamic:(BOOL)dynamic
{
    _dynamic = dynamic;
    [self setNeedsDisplay];
}

-(void)setMirrorAlpha:(CGFloat)mirrorAlpha
{
    _mirrorAlpha = [self safeValueBetween0And1:mirrorAlpha];
    if (self.mirrored) {
        if (self.dynamic) {
            CAReplicatorLayer * layer = (CAReplicatorLayer *)self.layer;
            layer.instanceAlphaOffset = self.mirrorAlpha - 1;
        }
        else
        {
            [self setNeedsDisplay];
        }
    }
    
}

-(void)setMirrorScale:(CGFloat)mirrorScale
{
    _mirrorScale = [self safeValueBetween0And1:mirrorScale];
    if (self.mirrored) {
        self.maskLayer.locations = [self getMaskLayerLocations];
        if (!self.dynamic) {
            [self setNeedsDisplay];
        }
    }
}

-(void)setMirrorDistant:(CGFloat)mirrorDistant
{
    _mirrorDistant = mirrorDistant;
    if (self.mirrored) {
        self.maskLayer = nil;
        [self handleMirrorDistant:mirrorDistant];
        [self setNeedsDisplay];
    }
}

-(CAGradientLayer *)maskLayer
{
    if (!_maskLayer) {
        _maskLayer = [CAGradientLayer layer];
        _maskLayer.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height * 2 + self.mirrorDistant);
        _maskLayer.startPoint = CGPointMake(0, 0);
        _maskLayer.endPoint = CGPointMake(0, 1);
        _maskLayer.locations = [self getMaskLayerLocations];
        _maskLayer.colors = @[(id)[UIColor blackColor].CGColor,(id)[UIColor blackColor].CGColor,(id)[UIColor clearColor].CGColor];
    }
    return _maskLayer;
}

-(UIImageView *)mirrorImageView
{
    if (!_mirrorImageView) {
        _mirrorImageView = [[UIImageView alloc] initWithFrame:self.bounds];
        _mirrorImageView.contentMode = UIViewContentModeScaleToFill;
        _mirrorImageView.userInteractionEnabled = NO;
        [self addSubview:_mirrorImageView];
    }
    return _mirrorImageView;
}

因为本身只作为一个容器存在,不需要对外界留一些接口,所以总共才200行代码,不过实现的效果还是可以的。


今天的内容告一段落了=。=老司机更的速度呢的确是有点慢,忙是一方面,是另一方面。

不过老司机会把剩下的几个类在下一期说完的=。=

demo老司机放在了网盘里,你可以来这里找。

至于镜像控件,老司机封装好了单独放在了一个仓库,你可以来这里找。

最后,如果你喜欢老司机的文章,点个关注点个喜欢吧~

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

推荐阅读更多精彩内容