老司机带你走进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老司机放在了网盘里,你可以来这里找。

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏非典型技术宅

OC绘制饼状图、柱状图和扇形图1. 绘制柱状图bar chart2. 绘制饼图Pie Chart3. 绘制进度条和进度扇形4. 神秘感增强器:IB_DESIGNABLE和IBInspectable5.

1344
来自专栏Coco的专栏

谈谈一些有趣的CSS题目(三)-- 层叠顺序与堆栈上下文知多少

1635
来自专栏HT

HTML5版的String Avoider小游戏

HTML5版的String Avoider小游戏 http://www.newgrounds.com/portal/view/300760 蛮简单也蛮考验耐心,...

2218
来自专栏進无尽的文章

浅汇-iOS 动画

        在iOS开发中,制作动画效果是最让开发者享受的环节之一。一个设计严谨、精细的动画效果能给用户耳目一新的效果,吸引他们的眼光 —— 这对于app而...

1382
来自专栏Guangdong Qi

iOS 连续动画效果(让APP动起来)

2233
来自专栏進无尽的文章

绘图-几种基本统计图的实现分析

在开发中我们会遇到各种统计图,或者各种绘图,本文通过对基本三大统计图:折线图、柱状图、扇形图的实现来掌握基本统计图的绘制,在下一篇文中会带来复杂一些的绘图案例分...

1941
来自专栏向治洪

React Native仿美团下拉菜单

在很多产品中都会涉及到下拉菜单选择功能,用的最好的当属美团了,其效果如下: ? 要实现上面的效果,在原生中比较好做,直接使用PopWindow组件即可。如果使...

7785
来自专栏陈满iOS

iOS动画专题·UIView二维形变动画与CAAnimation核心动画(transform动画,基础,关键帧,组动画,路径动画,贝塞尔曲线)

总的来说,从涉及类的形式来看,iOS动画有:基于UIView的仿射形变动画,基于CAAnimation及其子类的动画,基于CG的动画。这篇文章着重总结前两种动画...

4651
来自专栏何俊林

Android开发之贝塞尔曲线进阶篇(仿直播送礼物,饿了么购物车动画)

今天来自李晨玮分享的直播礼物效果Demo。对于直播中送车,点赞都有借鉴意义。李晨玮的简书为:http://www.jianshu.com/u/b2df0a5ea...

5348
来自专栏向治洪

React Native仿美团下拉菜单

在很多产品中都会涉及到下拉菜单选择功能,用的最好的当属美团了,其效果如下: ? 要实现上面的效果,在原生中比较好做,直接使用PopWindow组件即可。如...

55910

扫码关注云+社区

领取腾讯云代金券