IOS核心动画高级五:变换

在第四章“视觉效果”中,我们研究了一些增强图层和它的内容显示效果的一些技术,在这一章中,我们将要研究可以用来对图层旋转、摆放或者扭曲的CGAffineTransform。以及可以将扁平物体转换成三维空间对象的CATransform3D。

仿射变换

在第三章【图层几何学】中我们使用了UIView的transform属性旋转了钟表的指针,但是并没有解释背后运作的原理,实际上UIView的transform属性是一个CGAffineTransfom类型,用于在二维空间做旋转、缩放和平移。CGAffineTransform其实是一个可以和二维空间向量(例如CGPoint)做乘法的 3x2的矩阵

用矩阵表示CGAffineTransform和CGPoint

用CGPoint的每一列和CGAffineTransform矩阵的每一行对应元素相乘再求和,就形成了一个新的CGPoint类型的结果要解释一下图中显示灰色的元素,为了能让矩阵做乘法,左边矩阵的列数一定要和右边矩阵的行数个数相同,所以要给矩阵填充一些标志值,使得既可以让矩阵做乘法,又不改变运算结果,并且没必要存储这些添加的值,因为它们的值不会发生变化,但是要用来作运算

因此,通常会用3 x 3(而不是2 X 3 )的矩阵来做二维变换,你可能会见到 3 行 2 列格式的矩阵,这是所谓的以列为主的格式,上图所示的是以行为主的矩阵,只要能保持一致,用哪种格式都无所谓。

当对图层应用变换矩阵,图层矩形内的每一个点都被相应地做变换,从而形成一个新的四边形的形状CGAffineTransform中的“仿射”的意思是无论变换矩阵用什么值,图层中平行的两条线在变换之后仍然保持平行,CGAffineTransform可以做出任意符合上述标注 的变换
一些仿射变换和非仿射变换。

5.2.jpeg

创建一个CGAffineTransform

对矩阵数学做一个全面的阐述就超出我们本书的讨论范围了,不过如果你对矩阵完全不熟悉的话,矩阵变换可能会使你感到恐惧。幸运的是,CoreGraphics提供了一系列函数,对完全没有数学基础的开发者也能够简单的做一些变换。如下几个函数都创建一个CGAffineTransform实例:

CGAffineTransformMakeRotation(CGFloat angle); //旋转
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy); //缩放
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty); //平移

旋转和缩放变换都可以很好解释:分别旋转或者缩放一个向量的值。平移变换是指每一个点都移动了向量指定的x或者y的值,所以如果向量代表了一个点,那它就平移了这个点的距离**。

我们用一个简单的项目来做一个demo,把一个原始视图旋转45度。

使用仿射变换旋转了45度的视图

UIView可以通过设置transform属性做变换,但实际上它只是封装了内部图层的变换。

CALayer同样也有一个transform属性,但是它的类型是CATransform3D。而不是CGAffineTransform,本章后续将会详细解释。

CALayer对应于UIView的CGAffineTransform的属性叫做affineTransform。

视图代码如下

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];


UIImage *image = [UIImage imageNamed:@"tesla.jpg"];

UIView *imgView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
imgView.backgroundColor = [UIColor grayColor];
imgView.layer.contents = (__bridge id)image.CGImage;
imgView.layer.contentsGravity = kCAGravityResizeAspect;
CGAffineTransform t = CGAffineTransformMakeRotation(M_PI_4);
//    imgView.transform = CGAffineTransformRotate(t, M_PI_4);
imgView.transform = t;


[self.view addSubview:imgView];
}

图层代码如下:

UIView *imgView2 = [[UIView alloc] initWithFrame:CGRectMake(100, 400, 200, 200)];
imgView2.backgroundColor = [UIColor yellowColor];
imgView2.layer.contents = (__bridge id) image.CGImage;
imgView2.layer.contentsGravity = kCAGravityResize;
imgView2.layer.affineTransform = t;

[self.view addSubview:imgView2];

效果如下:

图层和视图的仿射变换

注意我们使用的旋转常量是M_PI_4,而不是你想象的45,因为IOS的变换函数使用弧度而不是角度作为单位弧度用数学常量中的PI的倍数来表示。一个PI代表180度。所以4分之一的PI 就是45度。**

C的数学函数库(IOS 会自动引入)提供了PI的一些简便的换算。M_PI_4于是就是PI的四分之一。如果对换算不太清楚的话,可以用如下的宏来换算。

#define RADIANS_TO_DEGREES(x) ((x)/M_PI180.0)*

混合变换

CoreGraphics提供了一系列的函数可以在一个变换的基础上做更深层次的变换。如果做一个既要旋转又要缩放的变换,这就会非常有用了,例如下面几个函数:

CGAffineTransformRotate(CGAffineTransform t , CGFloat angle);
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy);
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty);

当操纵一个变换的时候,初始生成一个什么都不做的变换很重要,也就是创建一个CGAffineTransform类型的空值,矩阵论中称为单位矩阵,CoreGraphics同样也提供了一个方便的变量。

CGAffineTransformIdentity

最后,如果需要混合两个已经存在的变换矩阵,就可以使用如下方法:在两个变换的基础上创建一个新的变换

CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2); //合并

我们来用这些函数组合一个更加复杂的变换。先缩小50 %, 再旋转30度,最后向右移动200个像素,
代码如下:

 - (void)transform{

CGAffineTransform transform = CGAffineTransformIdentity;

transform = CGAffineTransformScale(transform, 0.5, 0.5); //缩放50%
transform = CGAffineTransformRotate(transform, M_PI_4); //旋转45度
transform = CGAffineTransformTranslate(transform, 200, 0); //平移

_layerView.layer.affineTransform = transform;

 }
顺序应用多个仿射变换之后的结果.png

在上图中有些需要注意的地方:图片向右边发生了平移,但并没有指定距离那么远(200像素),另外它还有点向下发生了平移。原因在于当你按顺序做了变换,上一个变换的结果将会影响之后的变换,所以200像素的向右平移同样也被旋转了45度,缩小了50%,所以它实际上是斜向移动了100像素。

这意味着变换的顺序会影响最终的结果,也就是说旋转之后的平移和平移之后的旋转结果可能不同。

3D变换

CG的前缀告诉我们,CGAffineTransfom类型属于Core Graphics框架,Core Graphics实际上是一个严格意义上的2D绘图API,并且CGAffineTransform仅仅对2D变换有效果

在第三章中,我们提到了zPosition属性,可以用来让图层靠近或者远离相机(用户视角),transform(CATransform3D)属性可以真正做到这点,即让图层在3D空间内移动或者旋转

和CGAffineTransform类似,CATransform3D也是一个矩阵,但是和2 x 3的矩阵不同,CATransform3D是一个可以在三维空间内做变换的4 X 4的矩阵。
下图对一个3D像素点做CATransform3D矩阵变换。


5.6.png

和CGAffineTransform矩阵类似,Core Animation框架提供了一系列的方法用来创建和组合CATransform3D类型的矩阵,和Core Graphics的函数类似,但是3D的的平移和旋转多出了一个z参数,并且旋转函数除了angle之外多了x, y, z三个参数,分别决定了每个坐标轴方向上的旋转。

CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z);
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz);
CATransform3DMakeTranslation(CGFloat tx, CGFloat ty, CGFloat tz);

你应该对X轴和Y轴比较熟悉了,分别以右和下为正方向(回忆第三章,这是ios上的标准结构,在MacOS上,以Y轴的朝上为正方向,) Z轴和这两个轴分别垂直,指向视角外为正方向**。

X, Y, Z轴以及围绕它们旋转的方向

由图可见,绕Z轴的旋转等同于之前二维空间中的仿射变换——旋转,但是X轴和Y轴的旋转就突破了屏幕的二维空间,并且在用户的视角看来发生了倾斜

举个例子,代码使用CATransform3DMakeRotation对视图内的图层绕Y轴做45度角的旋转,我们可以是视图向右倾斜,这样会看的更清晰。

self.view.backgroundColor = [UIColor whiteColor];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"tesla.jpg"];
imageView.layer.transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
[self.view addSubview:imageView];

效果并不像我们期待的那样

绕Y轴进行旋转.png

看起来图层并没有旋转,而是仅仅在水平方向上的一个压缩。是哪里出问题了呢?
其实完全没错,视图看起来更窄实际上是因为我们在一个倾斜的角度上看它,而不是透视。

透视投影

在真实世界中,当物体远离我们的时候,由于视角的原因看起来会变小,理论上说远离我们的视图边要比靠近视角的边更短,但实际上并没有发生,和我们当前的视角是等距离的。也就是在3D变换中仍然保持平行,和之前提到的仿射变换类似。

在等距投影中,远处的物体和近处的物体保持同样的缩放比例,这种投影也有它自己的用处(例如建筑绘图、颠倒、和伪3D视频),但当前我们并不需要。

为了做一些修正,我们需要引入投影变换(又称作z变换)来对除了旋转之外的变换矩阵做一些修改。Core Animation并没有给我们提供透视变换的函数,因此需要我们手动修改矩阵值,幸运的是,很简单**。

CATransform3D的透视效果通过一个矩阵中一个很简单的元素来控制:m34m34用来按比例缩放x和y的值来计算到底要离视角多远**。

CATransform3D变换矩阵中的m34元素,用来做透视

m34默认值为0,我们可以通过设置 m34为:-1.0/d来应用透视效果d代表了想象中视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢 ?实际上并不需要,大概估算一下就好了。

因为视角相机实际上并不存在,所以可以根据屏幕上的显示效果来自由决定它的放置的位置,通常500 - 1000就已经很好了但对于特定的图层有时候更小或者更大的值会看起来更舒服减少距离的值会增强透视效果,所以一个非常微小的值会让它看起来更加失真,然而一个非常大的值会让它基本失去透视效果

对变换应用透视效果代码:

 CATransform3D transform3D = CATransform3DIdentity;

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
transform3D.m34 = -1.0/500;
transform3D = CATransform3DRotate(transform3D, M_PI_4, 0, 1, 0);
imageView.layer.transform = transform3D;
imageView.image = [UIImage imageNamed:@"tesla.jpg"];
[self.view addSubview:imageView];

透视效果:

应用透视效果之后再对图层做旋转

灭点

当在透视角度绘图的时候,远离相机视角的物体将会越远越小,当远离到一个极限距离,它们可能就缩成了一个点,于是所有的物体最后都汇聚消失在同一点上

在现实中,这个点通常是视图的中心,于是为了在应用中创建拟真效果的透视,这个点应该聚在屏幕中点,或者至少是包含所有3D对象视图中点。

灭点

CoreAnimation定义了这个点位于变换图层的anchorPoint(锚点,通常是位于图层中心,但也有例外,见第三章),这就是说,当图层发生变换时,这个点永远位于图层发生变换之前anchorPoint的位置

当改变了一个图层的position,你也改变了它的灭点,做3D变换的时候要时刻记住这点。当你试图通过调整m34来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置(而不是直接改变它的position值),这样所有的3D图层都共享一个点**。

sublayerTransform属性

如果有多个视图或是图层,每个都做3D变换,那就需要分别设置相同的m34值,并且确保在变换之前都在屏幕中央共享同一个position。如果用一个函数封装这些操作的确会更加方便,但仍然有限制(例如:你不能在interface builder中摆放视图)这里有一个更好的方法。

CALayer有一个属性叫做sublayerTransform,它也是CATransform3D类型,但和对一个图层的变换不同,它影响到所有的子图层。这意味着你可以一次性对包含这些图层的容器做变换,于是所有的子图层都自动继承了这个变换方法**。

相较而言,通过在一个地方设置透视变换会更方便,同时它会带来一个更显著的优势:灭点被设置在容器图层的中点,从而不需要再对子视图分别设置了。这意味着你可以随意使用position和frame来放置子图层了,而不需要把他们放置到屏幕中点,然后为了保证这个统一的灭点而做平移变换。**

我们来用一个demo来举例说明,我们并排放置两个视图,然后通过设置它们容器视图的透视变换,我们可以保证它们有相同的透视和灭点。

应用sublayerTransform代码如下:

 - (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
UIImageView *imgview1 = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
imgview1.image = [UIImage imageNamed:@"tesla.jpg"];



UIImageView *imgview2 = [[UIImageView alloc] initWithFrame:CGRectMake(200, 20, 200, 200)];
imgview2.image = imgview1.image;

UIView *containtsview = [[UIView alloc] initWithFrame:CGRectMake(0,200,  [UIScreen mainScreen].bounds.size.width,  [UIScreen mainScreen].bounds.size.width)];
containtsview.backgroundColor = [UIColor grayColor];

[containtsview addSubview:imgview1];
[containtsview addSubview:imgview2];


//应用sublayerTransform

CATransform3D transformv = CATransform3DIdentity;
transformv.m34 = -1.0/500.0;
containtsview.layer.sublayerTransform = transformv;

CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
imgview2.layer.transform = transform2;
imgview1.layer.transform = transform1;


[self.view addSubview:containtsview];

}

应用sublayerTransform的变换效果:

通过相同的透视效果分别对视图做变换

背面

我们既然可以在3D场景下旋转图层,那么也可以从背面去观察它,如果我们把它的旋转(围绕Y轴进行旋转)角度改成M_PI (180度),而不是当前的M_PI_4(45度)。那么将会把图层完全旋转一个半圈,于是完全背对了相机视角
那么从背部看图层是什么样子的呢?

如你所见,图层是双面绘制的,反面显示的是正面的一个镜像图片。

但这并不是一个很好特性,因为如果图层包含文字或者其他控件,那用户看到这些内容的镜像图片当然会感到困惑。另外也有可能造成资源的浪费:想象用这些图层形成一个不透明的固态立方体,既然永远都看不到这些图层的背面,那为什么浪费GPU来绘制他们呢?

背面

CALayer有一个叫做doubleSided的属性来控制图层的背面是否要被绘制。****这是一个bool类型, 默认为yes,如果设置为NO,那么当图层正面从相机视角消失的时候,他将不会被绘制**。

代码如下:

 self.view.backgroundColor = [UIColor whiteColor];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
imageView.backgroundColor = [UIColor grayColor];
imageView.image = [UIImage imageNamed:@"tesla.jpg"];

imageView.layer.transform = CATransform3DMakeRotation(M_PI, 0, 1, 0);
imageView.layer.doubleSided = NO;


[self.view addSubview:imageView];

效果如下:

背面不进行绘制.png

扁平化图层

如果对包含已经做过变换的图层的图层做反方向的变换将会发生什么呢?是不是有点困惑:
如下图:反方向变换的嵌套图层

5.15.jpeg

注意做了-45度旋转的内部图层是怎样抵消旋转45度的图层,从而恢复正常状态的。

如果内部图层相对外部图层做了相反的变换(这里是绕Z轴的旋转),那么按照逻辑这两个变换将会被抵消。

验证一下:

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];

UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 400, 400)];
view1.backgroundColor = [UIColor grayColor];

[self.view addSubview:view1];

UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
view2.backgroundColor = [UIColor blueColor];
[view1 addSubview:view2];

CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);
view1.layer.transform = transform1;

CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1);
view2.layer.transform = transform2;

}

效果:

旋转后的效果

运行结果和我们预期的一样,现在我们在3D情况下再试一次。修改代码,让内外两个视图绕Y轴旋转而不是Z轴,再加上透视效果,以便我们观察。注意不能使用sublayerTransform属性,因为内部的图层并不直接是容器图层的子图层,所以这里分别对图层设置透视变换。

围绕Y进行旋转 + 透视

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];

UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 400, 400)];
view1.backgroundColor = [UIColor grayColor];

[self.view addSubview:view1];

UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
view2.backgroundColor = [UIColor blueColor];
[view1 addSubview:view2];

CATransform3D transform1 = CATransform3DIdentity;
transform1.m34 = -1.0/500.0;
transform1 = CATransform3DRotate(transform1, M_PI_4, 0, 1, 0);
view1.layer.transform = transform1;

CATransform3D transform2 = CATransform3DIdentity;
transform2.m34 = -1.0/500.0;
transform2 = CATransform3DRotate(transform2, -M_PI_4, 0, 1, 0);

view2.layer.transform = transform2;

}

预期的效果如下:


5.17.jpeg

但其实这并不是我们所看到的,相反,我们所看到的效果如下图所示,
效果:


围绕Y轴做旋转的真是结果

发生了什么呢?内部的图层仍然向左旋转,并且发生了扭曲,但按道理说它应该保持正面朝上,并且显示正常的方块。

这是由于尽管Core Animation图层存在于3D空间之内,但它们并不存在于同一个3D空间。每个图层的3D场景其实都是扁平化的,当你从正面观察一个图层,看到的实际上是由子图层创建的想象出来的3D场景,但当你倾斜这个图层,你会发现实际上这个3D场景仅仅是被被绘制在图层的表面。

类似的,当你在玩一个3D游戏,实际上仅仅是把屏幕做了一次倾斜,或许在游戏中可以看见一面墙在你面前,但是倾斜屏幕并不能够看见墙里面的东西所有场景里面绘制的东西并不会随着你观察它的角度改变而发生变化 ,图层也是同样的道理。

这使得用CoreAnimation创建非常复杂的3D场景变得十分困难,你不能够使用图层树去创建一个3D结构的层级关系——在相同场景下的任何3D表面必须和同样的图层保持一致,这是因为每个的父图层都把它的子视图扁平化了。

至少当你用正常的CALayer的时候是这样。CALayer有一个叫做CATransformLayer的子类来解决这个问题。具体在第六章中“特殊的图层”中将会具体讨论。

固体对象

现在你懂得了在3D空间的一些图层布局的基础,现在我们来试着创建一个固态的3D对象(实际上是一个技术上所谓的空洞对象,但它以固态呈现)**。我们用六个独立的视图来构建一个立方体的各个面。

在这个例子中,立方体的六个面,我们当然可以用代码来写,但是用interfaceBuilder创建的好处是可以方便的在每一个面上添加子视图。记住这些面仅仅是包含视图和控件的普通的用户界面元素,它们完全是我们界面交互的部分,并且当把它们折成一个正方体之后也不会改变这个性质。

5.19.jpeg

这些面视图并没有放置到主视图中,而是松散的排列在根nib文件里面。我们并不关心在这个容器中如何摆放它们的位置。因为后续将会用图层的transform对它们进行重新布局。并且用interfaceBuilder在容器视图之外摆放他们可以让我们容易看清楚他们的 内容。如果把他们一个叠着一个都塞进主视图,将会变得很难看。

我们把一个有颜色的UILabel放到视图内部,是为了清楚的辨别他们之间的关系,并且UIButton被放置在第三个视图里面,后面会做简单解释。

把视图组织成立方体的代码。

   @interface LYCubeViewController ()

   @property(nonatomic, strong) NSMutableArray *viewArray;
   @property(nonatomic, strong) UIView *containerView;

   @end

   @implementation LYCubeViewController

 - (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor grayColor];
//绘制六个视图
UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(0, 60, 100, 100)];
view1.backgroundColor = [UIColor whiteColor];
UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(150, 60, 100, 100)];
view2.backgroundColor = [UIColor whiteColor];
UIView *view3 = [[UIView alloc] initWithFrame:CGRectMake(0, 200, 100, 100)];
view3.backgroundColor = [UIColor whiteColor];
UIView *view4 = [[UIView alloc] initWithFrame:CGRectMake(150, 200, 100, 100)];
view4.backgroundColor = [UIColor whiteColor];
UIView *view5 = [[UIView alloc] initWithFrame:CGRectMake(0, 350, 100, 100)];
view5.backgroundColor = [UIColor whiteColor];
UIView *view6 = [[UIView alloc] initWithFrame:CGRectMake(150, 350, 100, 100)];
view6.backgroundColor = [UIColor whiteColor];

UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label1.text = @"1";
label1.textAlignment = NSTextAlignmentCenter;
label1.textColor = [UIColor blackColor];
UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label2.text = @"2";
label2.textAlignment = NSTextAlignmentCenter;
label2.textColor = [UIColor redColor];
UILabel *label3 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label3.text = @"3";
label3.textAlignment = NSTextAlignmentCenter;
label3.textColor = [UIColor blueColor];
UILabel *label4 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label4.text = @"4";
label4.textAlignment = NSTextAlignmentCenter;
label4.textColor = [UIColor yellowColor];
UILabel *label5 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label5.text = @"5";
label5.textAlignment = NSTextAlignmentCenter;
label5.textColor = [UIColor greenColor];
UILabel *label6 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label6.text = @"6";
label6.textAlignment = NSTextAlignmentCenter;
label6.textColor = [UIColor orangeColor];

[view1 addSubview:label1];
[view2 addSubview:label2];
[view3 addSubview:label3];
[view4 addSubview:label4];
[view5 addSubview:label5];
[view6 addSubview:label6];

_viewArray = [[NSMutableArray alloc] init];
[_viewArray addObject:view1];
[_viewArray addObject:view2];
[_viewArray addObject:view3];
[_viewArray addObject:view4];
[_viewArray addObject:view5];
[_viewArray addObject:view6];


self.containerView = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];

[self.view addSubview:self.containerView];
[self cubeInit];
}

- (void)cubeInit
 {
//折合立方体
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0/500.0;
self.containerView.layer.sublayerTransform = perspective;  //为所有的子图层设置透视变换

//第一面视图
CATransform3D transform = CATransform3DMakeTranslation( 0, 0, 50);//特别注意变换是一个崭新的变换对象
[self addFace:0 withTransform:transform];
//第二面视图
transform = CATransform3DMakeTranslation(50, 0, 0); //特别注意变换是一个崭新的变换对象
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
[self addFace:1 withTransform:transform];
//第三面视图
transform = CATransform3DMakeTranslation(-50, 0, 0);//特别注意变换是一个崭新的变换对象
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1 ,0);
[self addFace:2 withTransform:transform];
//第四面视图
transform = CATransform3DMakeTranslation( 0, 50, 0);//特别注意变换是一个崭新的变换对象
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
[self addFace:3 withTransform:transform];

//第五面视图
transform = CATransform3DMakeTranslation( 0, -50, 0);//特别注意变换是一个崭新的变换对象
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
[self addFace:4 withTransform:transform];

//第六面视图
transform = CATransform3DMakeTranslation( 0, 0, -50);//特别注意变换是一个崭新的变换对象
transform = CATransform3DRotate(transform, M_PI, 1, 0, 0);
[self addFace:5 withTransform:transform];
 }


   - (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
{
//get the face view and add it to the container
UIView *face = _viewArray[index];
[self.containerView addSubview:face];
//因为有透视效果,需要先设置视图的中心点是父视图的重点
CGSize containerSize = self.containerView.bounds.size;
face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
// 应用变换
face.layer.transform = transform;
}

立方体效果

立方体.png

从这个角度看立方体并不是很明显。看起来只是一个方块,为了更好的欣赏它,我们将更换一个更好的角度。

旋转这个立方体会显得很笨重,因为我们需要单独对每一个视图进行旋转,另一个简单的方案是通过调整容器的sublayerTransform去旋转照相机。

添加如下几行去旋转containerView图层的perspective变换矩阵。

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

这就对相机(或者相对相机的整个场景,你也可以这么认为)围绕Y轴旋转了45度,并且围绕X轴旋转了45度,现在从另一个角度去观察立方体,就能看出它的真实面貌。

调整后的代码:

   @interface LYCubeViewController ()

    @property(nonatomic, strong) NSMutableArray *viewArray;
    @property(nonatomic, strong) UIView *containerView;

     @end

      @implementation LYCubeViewController

 - (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor grayColor];
//绘制六个视图
UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(0, 60, 100, 100)];
view1.backgroundColor = [UIColor whiteColor];
UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(150, 60, 100, 100)];
view2.backgroundColor = [UIColor whiteColor];
UIView *view3 = [[UIView alloc] initWithFrame:CGRectMake(0, 200, 100, 100)];
view3.backgroundColor = [UIColor whiteColor];
UIView *view4 = [[UIView alloc] initWithFrame:CGRectMake(150, 200, 100, 100)];
view4.backgroundColor = [UIColor whiteColor];
UIView *view5 = [[UIView alloc] initWithFrame:CGRectMake(0, 350, 100, 100)];
view5.backgroundColor = [UIColor whiteColor];
UIView *view6 = [[UIView alloc] initWithFrame:CGRectMake(150, 350, 100, 100)];
view6.backgroundColor = [UIColor whiteColor];

UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label1.text = @"1";
label1.textAlignment = NSTextAlignmentCenter;
label1.textColor = [UIColor blackColor];
UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label2.text = @"2";
label2.textAlignment = NSTextAlignmentCenter;
label2.textColor = [UIColor redColor];
UILabel *label3 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label3.text = @"3";
label3.textAlignment = NSTextAlignmentCenter;
label3.textColor = [UIColor blueColor];
UILabel *label4 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label4.text = @"4";
label4.textAlignment = NSTextAlignmentCenter;
label4.textColor = [UIColor yellowColor];
UILabel *label5 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label5.text = @"5";
label5.textAlignment = NSTextAlignmentCenter;
label5.textColor = [UIColor greenColor];
UILabel *label6 = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 60, 60)];
label6.text = @"6";
label6.textAlignment = NSTextAlignmentCenter;
label6.textColor = [UIColor orangeColor];

[view1 addSubview:label1];
[view2 addSubview:label2];
[view3 addSubview:label3];
[view4 addSubview:label4];
[view5 addSubview:label5];
[view6 addSubview:label6];

_viewArray = [[NSMutableArray alloc] init];
[_viewArray addObject:view1];
[_viewArray addObject:view2];
[_viewArray addObject:view3];
[_viewArray addObject:view4];
[_viewArray addObject:view5];
[_viewArray addObject:view6];


self.containerView = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];

[self.view addSubview:self.containerView];
[self cubeInit];
 }

- (void)cubeInit
 {
//折合立方体
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.containerView.layer.sublayerTransform = perspective;  //为所有的子图层设置透视变换


//第一面视图
CATransform3D transform = CATransform3DMakeTranslation( 0, 0, 50);//特别注意变换是一个崭新的变换对象
[self addFace:0 withTransform:transform];
//第二面视图
transform = CATransform3DMakeTranslation(50, 0, 0); //特别注意变换是一个崭新的变换对象
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
[self addFace:1 withTransform:transform];
//第三面视图
transform = CATransform3DMakeTranslation(-50, 0, 0);//特别注意变换是一个崭新的变换对象
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1 ,0);
[self addFace:2 withTransform:transform];
//第四面视图
transform = CATransform3DMakeTranslation( 0, 50, 0);//特别注意变换是一个崭新的变换对象
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
[self addFace:3 withTransform:transform];

//第五面视图
transform = CATransform3DMakeTranslation( 0, -50, 0);//特别注意变换是一个崭新的变换对象
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
[self addFace:4 withTransform:transform];

//第六面视图
transform = CATransform3DMakeTranslation( 0, 0, -50);//特别注意变换是一个崭新的变换对象
transform = CATransform3DRotate(transform, M_PI, 1, 0, 0);
[self addFace:5 withTransform:transform];
 }


 - (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
 {
//get the face view and add it to the container
UIView *face = _viewArray[index];
[self.containerView addSubview:face];
//因为有透视效果,需要先设置视图的中心点是父视图的重点
CGSize containerSize = self.containerView.bounds.size;
face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
// 应用变换
face.layer.transform = transform;
}

从一个边角观察到的立方体效果:

从一个边角观察到的立方体

光亮和阴影

现在它看起来更像一个立方体没错了,但是对每个面之间的连接还是很难分辨。Core Animation可以用3D显示图形,但是它对光线并没有概念。如果想让这个立方体看起来更加真实,需要自己做一个阴影效果。你可以通过改变每个面的背景颜色或者直接用带光亮效果的图片来调整。

如果需要动态的创建光线效果,你可以根据每个视图的方向应用不同的alpha值做出半透明的阴影图层,但是为了计算阴影图层的半透明度,你需要得到每个面的正太向量(垂直于表面的向量),然后根据一个想象的光源计算出两个向量叉乘的结果。叉乘代表了光源和图层之间的角度,从而决定了他有多大程度上的光亮**。

如下代码实现了一个这样的结果,我们用GLKit框架来做向量的运算(你需要引入GLKit库来运行代码),每个面的CATransform3D都被转换成了GLKMatrix4。然后通过GLKMatrix4GetMatrix3函数得到一个3X3的旋转矩阵,这个旋转矩阵指定了图层的方向,然后可以用它来得到正太向量的值 。

试着通过调整LIGHT_DIRECTION和AMBIENT_LIGHT的值来切换光线效果

//添加光线效果

- (void)applyLightingToFace:(CALayer *)face
{
//add lighting layer
CALayer *layer = [CALayer layer];
layer.frame = face.bounds;
[face addSublayer:layer];
//convert the face transform to matrix
//(GLKMatrix4 has the same structure as CATransform3D)
//译者注:GLKMatrix4和CATransform3D内存结构一致,但坐标类型有长度区别,所以理论上应该做一次float到CGFloat的转换,感谢[@zihuyishi](https://github.com/zihuyishi)同学~
CATransform3D transform = face.transform;
GLKMatrix4 matrix4 = *(GLKMatrix4 *)&transform;
GLKMatrix3 matrix3 = GLKMatrix4GetMatrix3(matrix4);
//get face normal
GLKVector3 normal = GLKVector3Make(0, 0, 1);
normal = GLKMatrix3MultiplyVector3(matrix3, normal);
normal = GLKVector3Normalize(normal);
//get dot product with light direction
GLKVector3 light = GLKVector3Normalize(GLKVector3Make(LIGHT_DIRECTION));
float dotProduct = GLKVector3DotProduct(light, normal);
//set lighting layer opacity
CGFloat shadow = 1 + dotProduct - AMBIENT_LIGHT;
UIColor *color = [UIColor colorWithWhite:0 alpha:shadow];
layer.backgroundColor = color.CGColor;
}


 - (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
{
//get the face view and add it to the container
UIView *face = _viewArray[index];
[self.containerView addSubview:face];
//因为有透视效果,需要先设置视图的中心点是父视图的重点
CGSize containerSize = self.containerView.bounds.size;
face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
// 应用变换
face.layer.transform = transform;
 [self applyLightingToFace:face.layer];
 }

动态计算光线效果之后的立方体

5.22.jpeg

点击事件

你应该能注意到现在可以在第5️⃣个表面顶部看到按钮了,点击它什么都没有发生,为什么呢?

这并不是因为ios在3D场景下不能正确的响应事件,实际上是可以做到的。问题在于视图顺序,在第三章中我们简要提到过,点击事件的处理是由视图在父视图中的顺序决定的,并不是3D空间中的Z轴顺序当给立方体添加视图的时候,我们实际上是按照一个顺序添加的,所以按照视图/图层顺序来说,1,2,3,4,都在5的前面**。

既然我们看不到3,4,6的表面(因为被1,2,5遮住了)ios在事件响应上仍然按照之前的顺序,当试图点击5表面的按钮时,表面1,2,3,4截断了点击事件(取决于点击的位置),这就和普通的2D布局在按钮上覆盖物体一样

你也许认为把doubleSided设置成NO可以解决这个问题,因为它不再渲染视图背面的内容,但实际上并不起作用。因为背对相机隐藏的视图仍然会响应点击事件(这和通过设置hidden为yes,alpha为0而隐藏的视图不同,那两种方式都不会响应事件)。所以即使禁止了双面渲染仍然不能解决这个问题(由于性能问题,还是需要将它设置成NO)。

这里有几种正确的方案:把除了5视图之外的其他视图的userInteractionEnable设置成NO来禁止事件传递。或者简单通过代码将视图5覆盖到视图3,4上,无论怎样都可以点击按钮了。

如图:


响应事件.png

总结

这一章涉及了一些2D和3D的变换,你学习了一些矩阵计算的基础,以及如何用CoreAnimation创建3D场景。你看到了图层背后到底是如何呈现的。并且知道了不能把扁平的图片做成真实的立体效果。最后我们用Demo说明了触摸事件的处理,视图中图层添加的层级顺序会比屏幕上显示的顺序更有意义。

在第六章中我们研究一下CoreAnimation提供的具有不同功能的具体的CALayer;

推荐阅读更多精彩内容