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

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


做者感言

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

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

简介

CALayer Transforms讲得是CALayer一些咱们可以看得见的东西, 这些知识点在咱们平常开发中也会有用到的, 好比Affine Transforms, 3D Transforms, Solid Objects等等, 待咱们一一去讲解.github


Affine Transforms

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

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;
}
复制代码

1
2

注意一下, 咱们在这里使用的是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:ui

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

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

- (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;
}
复制代码

3
4

看到图片的时候, 你会发现结果好像和想象有些差别, 为何会平移了那么多? 缘由是在于当你按顺序作了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;
}
复制代码

5
6
7

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;
}
复制代码

8
9
10

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;
}
复制代码

11
12

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;
}
复制代码

13
14

看结果, 和咱们想象的同样, 再试试再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;
复制代码

15
16

这里我并无使用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;
}
复制代码

17
18

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;
}
复制代码

19
20

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


最后

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

微信

支付宝
相关文章
相关标签/搜索