欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

玩转iOS开发:5.《Core Animation》CALayer的Transforms

程序员文章站 2022-03-16 17:11:46
...

文章转至我的个人博客: https://cainluo.github.io/14777052484078.html


作者感言

之前我们所了解的CALayer都是比较抽象化, 好在《Core Animation》CALayer的视觉效果解决我们这些视觉动物的学东西的枯燥, 今天我们就来讲讲Transforms, 也就是CALayerTransforms.

**最后:** **如果你有更好的建议或者对这篇文章有不满的地方, 请联系我, 我会参考你们的意见再进行修改, 联系我时, 请备注**`Core Animation`**如果觉得好的话, 希望大家也可以打赏一下~嘻嘻~祝大家学习愉快~谢谢~**

简介

CALayer Transforms讲得是CALayer一些我们能够看得见的东西, 这些知识点在我们日常开发中也会有用到的, 比如Affine Transforms, 3D Transforms, Solid Objects等等, 待我们一一去讲解.


Affine Transforms

Affine Transforms的中文意思叫做仿射转换, 在前一篇文章的时候我们就使用过transform来旋转UIView, 但那时候我们只是简单的使用罢了, 并没有说明它的原理. 实际上UIView里的transformCAAffineTransform类型, 用于做二维空间的旋转, 缩放, 平移等操作, 而且CAAffineTransform可以和一个二维空间的向量, 比如CGPoint3x2的矩阵. 大概的运算原理就是, 用CGPoint的每一列和CGAffineTransform矩阵的每一列对应的元素进行相乘再求和, 这样子就会形成一个新的CGPoint. 说到这里, 应该会有人有疑惑, CGAffineTransformCGPoint完全都不是一样东西, 怎么能做运算呢? 其实并不是的, 当你使用它们两个进行运算的时候, 系统会自动补上一些缺少的元素, 使得CGAffineTransformCGPoint进行一一对应, 但运算完之后, 这些填充值就会被抛弃掉, 不会进行保存, 仅仅只是用来做运算罢了. 所以我们通常遇到的二维变换都是使用3x3, 而不是刚刚所说到的2x3, 但在某些情况下我们也会遇到2x3的格式矩阵, 这就是所谓的以列为主(这个等下用事例来查看吧), 但无论如何都好, 只要能够保持一致, 用什么格式又何妨呢? 当对图层进行矩阵变换时, 图层矩形内的每一个点都被相应的做变换, 从而形成一个新的四边形的形状, CGAffineTransform中的"仿射"的意思是无论你如何去改变矩阵的值, 图层中平行的两条线在变换之后仍然保持平行, 这就是CGAffineTransform的"仿射".

Creating a CGAffineTransform - 创建一个CGAffineTransform

其实对矩阵数学的阐述早就超过了Core Animation的讨论范围了, 如果你是对矩阵数学一点都不了解的话, 那你就要哭晕在厕所了, 不过还好, Core Graphics提供了一系列的API, 对完全没有数学基础的开发者来讲也能够做一些简单的变换, 比如:

    CGAffineTransformMakeRotation(CGFloat angle);
    CGAffineTransformMakeScale(CGFloat sx, CGFloat sy);
    CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty);
复制代码

UIView可以通过设置transform属性进行变换, 但实际上还是对CGLayer进行了一些图层转变的封装. CALayer同样也有一个transform属性, 它叫做affineTransform, 但它的类型是CATransform3D, 而不是CGAffineTransform, 这个后面再解释一下神马是CATransform3D. 直接来看Demo吧:

- (void)viewTransform {
    
    self.view.backgroundColor = [UIColor grayColor];
    
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    
    imageView.image = [UIImage imageNamed:@"expression"];
    
    [self.view addSubview:imageView];
    
    // 旋转
    CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
    imageView.layer.affineTransform = transform;
    
    // 缩放
//    CGAffineTransform scaleTransform = CGAffineTransformMakeScale(0.5, 0.5);
//    imageView.layer.affineTransform = scaleTransform;
    
    // 平移
//    CGAffineTransform translationTransform = CGAffineTransformMakeTranslation(50, 50);
//    imageView.layer.affineTransform = translationTransform;
}
复制代码

注意一下, 我们在这里使用的是M_PI_4, 而不是我们自己输入的神马45之类的数字, 因为在iOS当中, 使用的的是弧度单位, 而不是角度单位, 弧度用数学常量是表示为pi, 一个pi就为180°, 而四分之一度就是45°了. 但这里会有一个问题, 这些宏都是系统提供给我们的, 如果你要自己去加载更多或者是扩展的话, 可以自己手动去写一个API.

Combining Transforms - 混合变换

Core Graphics提供了一系列的API可以在一个transform的基础上做更深层次的transform, 比如说缩放之后再旋转, 比如下面几个API:

    CGAffineTransformRotate(CGAffineTransform t, CGFloat angle);
    CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy);
    CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)
复制代码

当你操纵一个transform的时候, 需要先创建一个CGAffineTransform类型的空值, 直接把CGAffineTransformIdentity赋值过去就好了, 这个称为单位矩阵. 如果你需要把两个已经写好的transform合成为一个的话, 你可以使用系统提供的API:

CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);
复制代码

不说那么多废话了, 直接来看Demo吧:

- (void)viewCombiningTransforms {
    
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    
    imageView.image = [UIImage imageNamed:@"expression"];
    
    [self.view addSubview:imageView];
    
    CGAffineTransform transform = CGAffineTransformIdentity;
    
    // 旋转
    transform = CGAffineTransformRotate(transform, M_PI_4);
    // 缩放
    transform = CGAffineTransformScale(transform, 0.5f, 0.5f);
    // 平移
    transform = CGAffineTransformTranslate(transform, 200, 0);
    
    imageView.layer.affineTransform = transform;
}
复制代码

看到图片的时候, 你会发现结果好像和想象有些差异, 为什么会平移了那么多? 原因是在于当你按顺序做了transform, 上一个transform会影响到下一个transform, 所以平移之后, 你会发现同样被缩放和旋转了, 这就是意味着, 你在旋转之后的平移和平移之后的旋转讲会得到两种不同的结果, 这个大家需要注意一下.


3D Transforms

在之前, 我们有提及过zPosition这个属性, 可以从用户角度的来让让图层远离或者是靠近,CATransform类型的transform可以真正做到让图层在3D空间内平移或者旋转. 和CGAffineTransform类似,CATransform3D也是一个矩阵, 但和之间所说的2x3矩阵不一样,CATransform3D是一个可以在3D空间内做变换的4x4矩阵. 和CGAffineTransform矩阵类似, Core Animation也提供了一系列的使用方法, 用来创建和组合CATransform3D矩阵, 于Core Graphics的函数相比, 也只是在3D的平移和旋转中多出了一个z参数, 而旋转的API除了有angle参数之外, 还多出了x, y, z等三个参数, 分别决定了每个坐标轴方向上的旋转, 比如:


CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z);
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz);
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz);
复制代码

在之前的文章里, 我们都应该了解了在iOS当中, 原点**{0, 0}是在左上角, x轴正方向为右边, y轴正方向为下边, 在Mac OS当中则是和iOS相反, 但是Z轴呢, 则是分别和x**, y轴分别垂直, 指向视角外为正方向, 说那么多, 直接来看代码吧:

- (void)viewTransforms3D {
    
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    
    imageView.image = [UIImage imageNamed:@"expression"];
    
    [self.view addSubview:imageView];

    CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
    imageView.layer.transform = transform;
}
复制代码

Perspective Projection

所谓的Perspective Projection就是透视投影, 这里需要普及一些知识(虽然我也看不太懂). 在现实生活中, 当物体远离我们的时候, 会由于视角的问题, 物体看起来会变小, 理论上说远离我们的视图边要比靠近视角边更短, 但实际上, 我们的视角是等距离的, 也就是在3D Transform中仍然保持平行, 和之前提到的仿射变换有些类似. 所以为了做一些修正, 我们需要引入投影变换, 又称为z变换, 来对一些做了变换的矩阵做一些修改, 旋转的除外, Core Animation, 当中并没有给我们提供直接设置透视变换的函数, 所以我们需要手动去修改矩阵值, 但很庆幸的是, 这个修改是很简单的, 直接来看代码吧:

- (void)viewPerspectiveProjection {
    
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    
    imageView.image = [UIImage imageNamed:@"expression"];
    
    [self.view addSubview:imageView];
    
    CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
    imageView.layer.transform = transform;
    
    CATransform3D transform3DIdentity = CATransform3DIdentity;
    transform3DIdentity.m34 = - 1.0 / 500.0;
    transform3DIdentity = CATransform3DRotate(transform3DIdentity, M_PI_4, 0, 1, 0);
    imageView.layer.transform = transform3DIdentity;
}
复制代码

CATransform3D中, 有一个m34的元素, 它是用于按比例来缩放XY的值, 从而来计算离视角的距离. m34的默认值为0, 我们可以通过设置m34来应用透视效果, 公式是**-1.0/d**, d代表了想象中视角相机和屏幕之间的距离, 以像素为单位, 通常设置500-1000之间, 但是对于一些特殊视图, 设置的值要小一些, 或者大一些要比500-1000要好一些, 所以这些值并不是固定的, 最好是根据需求来调节, 不然会出现湿疹, 或者是失去透视效果.

The Vanishing Point

The Vanishing Point翻译过来叫做消失点, 意思是当在透视角度绘图时, 原理视觉角度的物体将会变小变远, 远离到一个极限的时候, 所有物体最后都会汇聚并且消失在同一个点. 在现实生活中, 这个点通常都是视图的中心, 如果要在应用中创建拟真效果的透视, 这个点一般是在屏幕的重点, 至少是所有3D对象的视图中点. 在Core Animation中, 这个点是位于变换图层的anchorPoint(当然也有一些特殊的情况), 也就是说, 当图层发生变换的时候, 这个点永远位于图层变换钱的anchorPoint位置. 当我们改变一个图层的position时, 也同时改变了它的消失点, 所以在我们做3D变换的时候要记住. 当我们去调整视图的m34来让视图更加有3D效果, 通常要把它放置在屏幕的*, 然后通过平移来把它移动到指定的位置, 这样子做, 就可以让所有的3D图层都有同一个消失点.

Sublayer Transform

如果在开发中, 我们有多个视图或者多个图层, 而且他们都要做3D变换, 那我们就要对这些视图或者图层每个都设置相同的m34值, 并且还要确保在变换钱都在屏幕*都有一个相同的position, 当然, 我们可以自己封装一下, 但这样子也非常的蛋疼, 那该怎么做呢? 在CALayer中有一个属性叫做sublayerTransform, 它也是CATransform3D类型, 但和我们一个一个的去设置图层不同, 它将会影响所有的子图层, 这就是说明了, 我们只要使用sublayerTransform, 就可以一次性的把所有子图层都改变. 这也可以提供另一个好处, 就是当我们使用sublayerTransform属性时, 我们就不需要再对子图层挨个挨个的去设置消失点, 因为消失点将会被设置在容器图层的中心点, 那我们就可以随意设置positionframe来放置子图层, 还是直接来看Demo吧:

- (void)viewSublayerTransform {
    
    UIImageView *imageViewOne = [[UIImageView alloc] initWithFrame:CGRectMake(80, 100, 100, 100)];
    
    imageViewOne.image = [UIImage imageNamed:@"expression"];
    
    UIImageView *imageViewTwo = [[UIImageView alloc] initWithFrame:CGRectMake(250, 100, 100, 100)];
    
    imageViewTwo.image = [UIImage imageNamed:@"expression"];

    [self.view addSubview:imageViewOne];
    [self.view addSubview:imageViewTwo];
    
    CATransform3D perspective = CATransform3DIdentity; perspective.m34 = - 1.0 / 500.0;
    
    self.view.layer.sublayerTransform = perspective;
    
    CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
    
    imageViewOne.layer.transform = transform1;
    
    CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
    
    imageViewTwo.layer.transform = transform2;
}
复制代码

Backfaces

我们既然可以在3D场景下旋转图层, 当然也可以从背面去观察它, 比如我们把翻转的角度设置为M_PI, 那么就会显示一个镜像的图层, 我们来看看代码:

- (void)viewBackfaces {
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    
    imageView.image = [UIImage imageNamed:@"expression"];
    
    [self.view addSubview:imageView];
        
    CATransform3D transform3DIdentity = CATransform3DIdentity;
    transform3DIdentity.m34 = - 1.0 / 500.0;
    transform3DIdentity = CATransform3DRotate(transform3DIdentity, M_PI, 0, 1, 0);
    imageView.layer.transform = transform3DIdentity;
}
复制代码

Layer Flattening

有人会问, 如果我们对已经做过变换的图层做反方向的会发生啥事? 在理论上来讲, 我们如果对内部图层做了一个-45度的旋转, 如果要恢复正常, 则要做相反的变换, 才能相互抵消, 为了验证一下, 我们先试试:

- (void)viewLayerFlattening {
    
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
    view.backgroundColor = [UIColor blueColor];
    
    [self.view addSubview:view];
    
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
    
    imageView.image = [UIImage imageNamed:@"expression"];
    
    [view addSubview:imageView];
    
    CATransform3D outer = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);
    view.layer.transform = outer;
    
    CATransform3D inner = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1);
    imageView.layer.transform = inner;
}
复制代码

看结果, 和我们想象的一样, 再试试再3D变化的情况下能不能抵消, 继续看代码:

    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
    view.backgroundColor = [UIColor blueColor];
    
    [self.view addSubview:view];
    
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
    
    imageView.image = [UIImage imageNamed:@"expression"];
    
    [view addSubview:imageView];
    
//    CATransform3D outer = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);
//    view.layer.transform = outer;
//    
//    CATransform3D inner = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1);
//    imageView.layer.transform = inner;
    
    // 3D Trans
    CATransform3D outer = CATransform3DIdentity; outer.m34 = -1.0 / 500.0;
    outer = CATransform3DRotate(outer, M_PI_4, 0, 1, 0); view.layer.transform = outer;
    
    CATransform3D inner = CATransform3DIdentity; inner.m34 = -1.0 / 500.0;
    inner = CATransform3DRotate(inner, -M_PI_4, 0, 1, 0); imageView.layer.transform = inner;
复制代码

这里我并没有使用sublayerTransform属性, 因为这里面的图层并不是容器图层直接的子图层, 所以这里分别对图层设置了Perspective Projection. 结果也是和我们所预期的不太一样, 虽然按道理来讲是显示正常的方块, 但实际上并不是的. 在Core Animation当中, 3D图层存在于3D空间之内, 但它们并不是存在同一个, 其实每一个图层的3D场景都是扁平化的, 当我们正面观察一个图层时, 看到的图层其实是由子图层创建的3D场景, 当你倾斜这个图层时, 会发现这个3D场景只是被绘制在图层的表面罢了. 总之一句话说完, 用Core Animation创建非常负责的3D场景是很蛋疼的, 因为我们不能直接创建一个个图层的去套, 然后构建成一个3D结构的图层关系, 刚刚也说了, 在相同场景下任何3D表面必须和同样的图层保持一致, 这是因为每一个父视图都把它的子视图扁平化了. 那这个有办法解决吗? 当然有, 使用CALayer就可以啦, 在CALayer中, 有一个叫做CATransformLayer的子类就可以解决这个问题, 这个后面再说吧.


Solid Objects

Solid Objects翻译过来就叫做固体对象, 前面我们懂得了一丢丢的3D空间图层布局, 现在我们尝试着来创建一个固态的3D对象(也就是我们所谓的骰子), 直接来看代码吧:

- (void)viewSolidObjects {
    
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -1.0 / 500.0;

    perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
    perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);

    self.view.layer.sublayerTransform = perspective;
    
    CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);

    for (NSInteger i = 0; i < 6; i++) {
        
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
        
        label.backgroundColor = [UIColor whiteColor];
        label.textColor = [UIColor redColor];
        label.layer.borderColor = [UIColor blackColor].CGColor;
        label.layer.borderWidth = 0.5;
        label.tag = i;
        label.text = [NSString stringWithFormat:@"%ld", i + 1];
        label.font = [UIFont systemFontOfSize:30];
        label.textAlignment = NSTextAlignmentCenter;
        
        switch (label.tag) {
            case 0: {
                
                [self addLabel:label withTransform:transform];
            }
                break;
            case 1: {
                transform = CATransform3DMakeTranslation(100, 0, 0);
                transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
                [self addLabel:label withTransform:transform];
            }
                break;
            case 2: {
                transform = CATransform3DMakeTranslation(0, -100, 0);
                transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
                [self addLabel:label withTransform:transform];
            }
                break;
            case 3: {
                transform = CATransform3DMakeTranslation(0, 100, 0);
                transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
                [self addLabel:label withTransform:transform];
            }
                break;
            case 4: {
                transform = CATransform3DMakeTranslation(-100, 0, 0);
                transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
                [self addLabel:label withTransform:transform];
            }
                break;
            case 5: {
                transform = CATransform3DMakeTranslation(0, 0, -100);
                transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
                [self addLabel:label withTransform:transform];
            }
                break;
            default:
                break;
        }
    }
}

- (void)addLabel:(UILabel *)label withTransform:(CATransform3D)transform {
    
    [self.view addSubview:label];
    
    CGSize containerSize = self.view.bounds.size;
    label.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
    label.layer.transform = transform;
}
复制代码

Light and Shadow

刚刚我们弄了一个看上去像是立方体的, 但是它们之前的每一个面之间的连接压根就分辨不出, 虽然在Core Animation可以用3D显示图层, 但它并没有光线的概念, 如果要让这个立方体看起来更加的真实, 那我们就要手动给它加个阴影效果, 这个就根据自己的需求来看了. 这里我们简单的来看看事例:

- (void)addLightingToLabel:(CALayer *)labelLayer {
    
    CALayer *layer = [CALayer layer];
    layer.frame = labelLayer.bounds;
    
    [labelLayer addSublayer:layer];
    
    CATransform3D transform = labelLayer.transform;
    
    GLKMatrix4 matrix4 = [self matrixFrom3DTransformation:transform];
    GLKMatrix3 matrix3 = GLKMatrix4GetMatrix3(matrix4);
    
    GLKVector3 normal = GLKVector3Make(0, 0, 1);
    normal = GLKMatrix3MultiplyVector3(matrix3, normal);
    normal = GLKVector3Normalize(normal);
    
    GLKVector3 light = GLKVector3Normalize(GLKVector3Make(LIGHT_DIRECTION));
    CGFloat dotProduct = GLKVector3DotProduct(normal, light);
    
    CGFloat shadow = 1 + dotProduct - AMBIENT_LIGHT;
    UIColor *color = [UIColor colorWithWhite:0 alpha:shadow];
    
    layer.backgroundColor = color.CGColor;
}

- (GLKMatrix4)matrixFrom3DTransformation:(CATransform3D)transform {
    GLKMatrix4 matrix = GLKMatrix4Make(transform.m11, transform.m12, transform.m13, transform.m14,
                                       transform.m21, transform.m22, transform.m23, transform.m24,
                                       transform.m31, transform.m32, transform.m33, transform.m34,
                                       transform.m41, transform.m42, transform.m43, transform.m44);
    
    return matrix;
}
复制代码

Touch Events

虽然说我们现在用的是UILabel, 如果我们把3, 4, 5, 6换成UIButtonUIView的组合, 那4, 5, 6点击按钮是无法触发点击事件的. 这是因为由于视图的顺序, 在之前我们就说过, 点击事件的处理是由视图再父视图中的顺序决定的, 并不是在3D空间Z轴顺序上. 但在这个例子当中, 我们的视图的确是按照顺序来添加的, 那为什么把4, 5, 6换成UIButtonUIView之后就无法处理点击事件了呢? 那是因为被前面的三个视图挡住了, 在表面上截断了4, 5, 6的点击事件, 这个是和普通的2D布局在按钮上覆盖物体是一样的. 我们可以把除了3视图之外的视图userInteractionEnabled属性都设置成NO, 这样子就可以禁止事件传递, 或者通过简单的代码, 把视图3覆盖在视图6上, 那这样子无论你如何点, 都可以点击到按钮了.


总结

总结一下:

  • AffineTransforms的使用
  • AffineTransforms的混合变换
  • 3D Transforms的Perspective Projection
  • 3D Transforms的The Vanishing Point
  • 3D Transforms的Sublayer Transform
  • 3D Transforms的Backfaces
  • 3D Transforms的Layer Flattening
  • 最后再来一丢丢的Solid Objects

工程地址

项目地址: https://github.com/CainRun/CoreAnimation


最后

码字很费脑, 看官赏点饭钱可好

上一篇: Core Animation

下一篇: Core Animation