iOS Core Animation---变换

来源:互联网 发布:男神执事团类似网络剧 编辑:程序博客网 时间:2024/05/21 14:05

变换这一章将介绍两个方面的内容:仿射变换、3D变换,然后介绍利用3D变换实现的固体对象

1、放射变换
相对于下文的3D变换,仿射变换属于2D变换,函数开头CG意味着使用Core Graphics的API,而且CGAffineTransform仅仅对2D变换有效。

(1)介绍
UIView的transform属性是CGAffineTransform类型的,用于在二维空间做旋转、缩放和平移。这是因为CGAffineTransform是一个可以和CGPoint等二维空间向量做乘法的人3*2的矩阵。UIView的transform属性实际上是封装了内部图层的变换

这里写图片描述
用矩阵表示的CGAffineTransform和CGPoint

  图片中矩阵的灰色值是为了让矩阵能够做乘法(左边矩阵的列数和右边矩阵的行数相等)而添加的一些标志值,实际上没有存储这些值。

同样的,对图层应用变换矩阵,图层中的每个点都被相应的变换,从而形成新的四边形,CGAffineTransform仿射的意思是无论怎样变换,图层中平行的人线变换后仍然平行(做仿射变换的特点)

(2)图层的仿射
图层CALayer的仿射的属性为affineTransform,它是CGAffineTransform类型。对应于UIView的transform属性

(3)创建一个CGAffineTransform,也就是创建各种变换矩阵
使用下列Core Graphics函数创建能够创建CGAffineTransform实例,可以实现旋转、缩放以及平移。

CGAffineTransformMakeRotation(CGFloat angle)CGAffineTransformMakeScale(CGFloat sx,CGFloat sy)//缩放一个向量的值CGAffineTransformMakeTranslation(CGFloat tx,CGFloat ty)//每个点都已都移动了向量指定的值

注意:iOS中变换函数使用的是弧度。
度数转换弧度的宏定义:#define RADIANS_TO_DEGREES(X) ((X)/M_PI*180.0)

2、混合变换
(1)混合变换:使用Core Graphics还提供的函数将旋转、缩放以及平移混合在一起,其实质是这些函数在一个CGAffineTransform的基础上再增加一些变换,进而组合出复杂的变换

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

(2)一般步骤
——>先创建一个CGAffineTransform类型的空值,类似单位矩阵,可以使用Core Graphics的常量CGAffineTransformIndentity.
——>使用下面函数,将对图层的变换分成几步,一步步操作

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

——>如果需要混合两个已经存在的变换矩阵,可以使用下面方法,在两个变换的基础上创建新的变换

 CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);

下面的例子是,对图层缩放50%,然后再旋转30度

 //设置view    UIView *view = [[UIView alloc]initWithFrame:CGRectMake(kGetViewWidth(self.view)/2, 100, 80, 80)];    view.backgroundColor = [UIColor orangeColor];    [self.view addSubview:view];    //创建一个单位变换矩阵    CGAffineTransform transform = CGAffineTransformIdentity;    //缩放50%    transform = CGAffineTransformScale(transform, 0.5, 0.5);    //再旋转30度    transform = CGAffineTransformRotate(transform, RADIANS_TO_DEGREES(30));    view.layer.affineTransform = transform;

变换前后的结果
这里写图片描述

3、3D变换
(1)使用Core Animation提供的系列函数,类型是CATransform3D,它是可在3维空间做变换的4*4矩阵。
这里写图片描述
上图是对一个3D像素点做CATransform3D矩阵变换

(2)旋转、缩放以及平移
Core Animation提供一些方法用来创建和组合CATransform3D类型的矩阵,只不过函数的参数多了一个z参数,这些函数的返回值是CATransform3D类型的

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

(3)基于一个CATransform3D进行的再次变换,也就是组合复杂的变换,使用下面的函数

 CATransform3DTranslate (CATransform3D t, CGFloat tx, CGFloat ty, CGFloat tz)CATransform3DScale (CATransform3D t, CGFloat sx, CGFloat sy, CGFloat sz)CATransform3DRotate (CATransform3D t, CGFloat angle, CGFloat x, CGFloat y, CGFloat z)

(4)结合两个CATransform3D类型的矩阵,即结合两个变换

CATransform3D CATransform3DConcat (CATransform3D a, CATransform3D b)

附:X、Y、Z轴的正方向,以及围绕他们旋转的方向。绕z轴旋转等同于之前的二维空间的仿射旋转,但是绕x、y轴旋转就突破了屏幕的二维空间
这里写图片描述

例子,绕Y轴旋转45度

   //设置view    UIView *view = [[UIView alloc]initWithFrame:CGRectMake(kGetViewWidth(self.view)/2-50, 150, 80, 150)];    [self.view addSubview:view];    UIImage *image       = [UIImage imageNamed:@"1.jpg"];    view.layer.contents  = (__bridge id)image.CGImage;    //绕Y轴旋转45度    CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);    view.layer.transform    = transform;

结果:旋转后,图片显示窄了,其实是突破了二维空间,造成的视觉效果

这里写图片描述

4、透视投影
现实世界中,物体远离我们时。由于视角原因远离的物体看起来会变小,我们使用投影Z变换来对矩阵做一些修改
Core Animation没有函数提供透视投影,为达到透视投影我们需要引入投影变换(Z变换)来对除了旋转之外的变换矩阵做一些修改,*因为CATransform3D的透视效果是通过一个矩阵中的m34元素来控制的,所以要想有透视效果,可以手动修改矩阵中的m34的值,一般将值设为-1.0/d,d代表视角相机和屏幕间的距离,自己设定就好,一般d在500~1000之间。***m34用于按比例缩放X和Y的值来计算与视角的距离,通常m34的默认值为0,
这里写图片描述

例子:修改CATransform3D的m34元素来做透视

   //创建单位juzhen    CATransform3D transform = CATransform3DIdentity;    //应用透视perspective,改变单位矩阵的m34的值    transform.m34 = -1.0/50.0;    //顺着Y轴旋转45度    transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);    view.layer.transform = transform;

结果:变换前是上面例子中的”旋转前的效果”
这里写图片描述

介绍一个灭点的名词

在透视绘图中,当物体远离到极限时,就变成一个点,于是所有的物体最后都会聚到一个点,通常这个点是视图的中心。所以在应用中灭点应该是屏幕中心或是包含所有3D对象的视图中点。
这里写图片描述

Core Animation将灭点定在变换图层的anchorPoint,当改变图层的position时,也就改变了他的灭点。所以一定要谨记,在改变m34产生3D效果时,一定先将它放置到屏幕中央,然后通过平移将它一到指定位置(而不是直接改变它的position),这样所有的3D图层都共享一个灭点。

5、sublayerTransform属性

     CALayer的属性sublayerTransform,是CATransform3D类型,他影响所有的子图层,好处是一次性对设置包含这些图层的容器做变换,于是所有的子图层都自动继承这个变换方法

例如有很多视图或图层,每个都需要做3D变换,那就需要确保在变换前他们都共享屏幕中央的position,然后在分别修改m34的值,如果使用一个图层容器包含这些视图或图层,然后对容器做m34的改变,在使用sublayerTransform属性,就可以一改全部应用了

例子:

    //设置容器view    UIView *view = [[UIView alloc]initWithFrame:CGRectMake(10, 150, kGetViewWidth(self.view)-20, 400)];    [self.view addSubview:view];    //设置view1    UIView *view1 = [[UIView alloc]initWithFrame:CGRectMake(5, 10, kGetViewWidth(self.view)/2-20, 380)];    [view addSubview:view1];    UIImage *image        = [UIImage imageNamed:@"1.jpg"];    view1.layer.contents  = (__bridge id)image.CGImage;    //设置view2    UIView *view2 = [[UIView alloc]initWithFrame:CGRectMake(kGetViewWidth(self.view)/2+5, 10, kGetViewWidth(self.view)/2-30,  380)];    [view addSubview:view2];    UIImage *image1       = [UIImage imageNamed:@"1.jpg"];    view2.layer.contents  = (__bridge id)image1.CGImage;    //对父图层即图层容器应用perspective    CATransform3D transform      = CATransform3DIdentity;    //改变transform的m34的值    transform.m34 = -1.0/250.0;    view.layer.sublayerTransform = transform;    //对view1沿y轴旋转45度    CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);    view1.layer.transform    = transform1;    //对view2沿y轴旋转45度    CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);    view2.layer.transform    = transform2;

结果:
这里写图片描述

6、背面
将角度该为M_PI,就可以看到图层背面,而且可以通过CALayer的doubleSided属性可以设置图层背面是否要被绘制

7、扁平化图层
如果对包含已经做过变换的图层的图层做反方向的变换将会发什么什么呢?是不是有点困惑?意思是:图层A包含图层B,对图层A做正方向的45度旋转,然后对图层B做逆方向的45度旋转,结果会怎样?

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

例子:

 //布局,两个view,view1以及其子视图view11,view2以及其子视图view22    UIView *view1  = [[UIView alloc]initWithFrame:CGRectMake(80, 200, 200,200)];    UIView *view11 = [[UIView alloc]initWithFrame:CGRectMake(60, 60, 80, 80)];      view1.backgroundColor  = [UIColor whiteColor];    view11.backgroundColor = [UIColor lightGrayColor];    [self.view addSubview:view1];    [view1 addSubview:view11];    //view1外图层绕Z轴旋转45度    CATransform3D outerTransform = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);    view1.layer.transform = outerTransform;    //view11外图层绕Z轴旋转45度    CATransform3D interTransform = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1);    view11.layer.transform = interTransform;

结果:
这里写图片描述

绕Y轴旋转,
在3D情况下,让内外两个视图,外视图绕Y轴旋转,并且加上透视;内视图绕Y轴旋转,也加上透视;照上面的推测结果应该是里面的view11不变,但实际结果是下面这样的

这里写图片描述

这是由于尽管Core Animation图层存在于3D空间之内,但它们并不都存在同一个3D空间。每个图层的3D场景其实是扁平化的,当你从正面观察一个图层,看到的实际上由子图层创建的想象出来的3D场景,但当你倾斜这个图层,你会发现实际上这个3D场景仅仅是被绘制在图层的表面
类似的,当你在玩一个3D游戏,实际上仅仅是把屏幕做了一次倾斜,或许在游戏中可以看见有一面墙在你面前,但是倾斜屏幕并不能够看见墙里面的东西。所有场景里面绘制的东西并不会随着你观察它的角度改变而发生变化;图层也是同样的道理。
这使得用Core Animation创建非常复杂的3D场景变得十分困难。你不能够使用图层树去创建一个3D结构的层级关系–在相同场景下的任何3D表面必须和同样的图层保持一致,这是因为每个的父视图都把它的子视图扁平化了。
至少当你用正常的CALayer的时候是这样,CALayer有一个叫做CATransformLayer的子类来解决这个问题。具体在第六章“特殊的图层”中将会具体讨论。

8、固体对象

(1)创建一个立方体

现在来试着创建一个固态的3D对象(实际上是一个技术上所谓的空洞对象,但它以固态呈现)。我们用六个独立的视图来构建一个立方体的各个面。

 //添加containnerView    self.containerView = [[UIView alloc]initWithFrame:CGRectMake(0, 100, kGetViewWidth(self.view), kGetViewWidth(self.view))];    [self.view addSubview:self.containerView];    self.faces = [[NSMutableArray alloc]init];    [self creatFacesArray];    //使用sublayerTransform给containerView的子图层的增加透视    CATransform3D perspective = CATransform3DIdentity;    perspective.m34 = -1.0/500.0;  //注意:旋转代码加入后,在应用到layer的sublayerTransform上否则无用    self.containerView.layer.sublayerTransform = perspective;    //增加立方体面1    CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);    [self addFace:0 withTransform:transform];    //增加立方体面2    transform = CATransform3DMakeTranslation(100, 0, 0);    transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);    [self addFace:1 withTransform:transform];    // 增加立方体面3    transform = CATransform3DMakeTranslation(0, -100, 0);    transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);    [self addFace:2 withTransform:transform];    // 增加立方体面4    transform = CATransform3DMakeTranslation(0, 100, 0);    transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);    [self addFace:3 withTransform:transform];    // 增加立方体面5    transform = CATransform3DMakeTranslation(-100, 0, 0);    transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);    [self addFace:4 withTransform:transform];    // 增加立方体面6    transform = CATransform3DMakeTranslation(0, 0, -100);    transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);    [self addFace:5 withTransform:transform];

结果:

这里写图片描述

只有一面根本看不出来是立方体,所以我们旋转一下。但是,这个立方体是6个面组成的,正常逻辑就是旋转立方体,这样就必须要旋转6个面,我们还有另外一个简单的方法:调整容器视图的sublayerTransform去旋转照相机,为containerView的layer的perspective增加两行代码。但是谨记下面的代码必须放在 self.containerView.layer.sublayerTransform = perspective; 之前,否则没有旋转效果

    //只显示1面,所以旋转六面体,但必须旋转6个面;调整容器的sublayerTransform    // 绕X轴旋转45度    perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);    // 绕Y轴旋转45度    perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);

结果

这里写图片描述

(2)为立方体加上光亮和阴影

现在它看起来更像是一个立方体了,但是每个面之间的连接处很难分辨,这是因为没有阴影和光照,显得很不真实。如果想让立方体看起来更加真实,需要自己做一个阴影效果。

方法:可以通过改变每个面的背景颜色或者直接用带光亮效果的图片来调整。
如果要动态创建光线效果,可以根据每个视图的方向应用不同的alpha值做出半透明的阴影图层,但为了计算阴影图层的不透明度,就需要得到每个面垂直于表面的向量,然后根据一个你自己假设的光源来计算出两个向量叉乘结果。叉乘代表了光源和图层之间的角度,从而决定了它有多大程度上的光亮。

下面的例子用GLKit框架来做向量的计算(你需要引入GLKit库来运行代码),每个面的CATransform3D都被转换成GLKMatrix4,然后通过GLKMatrix4GetMatrix3函数得出一个3×3的旋转矩阵。这个旋转矩阵指定了图层的方向,然后可以用它来得到这个垂直与表面的向量的值。

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

-(void)applyLightingToFace:(CALayer *)face{    //添加光亮图层    CALayer *layer = [CALayer layer];    layer.frame = face.frame;    [face addSublayer:layer];    //将face的transform转换成GLKMatrix4矩阵    CATransform3D transform = face.transform;    GLKMatrix4 matrix4 = *(GLKMatrix4 *)&transform;    //获得3×3的旋转矩阵    GLKMatrix3 matrix3 = GLKMatrix4GetMatrix3(matrix4);    //获得表面向量的值    GLKVector3 normal = GLKVector3Make(0, 0, 1);    normal = GLKMatrix3MultiplyVector3(matrix3, normal);    normal = GLKVector3Normalize(normal);    //获得与光方向的叉乘    GLKVector3 light = GLKVector3Normalize(GLKVector3Make(LIGHT_DIRECTION));    float dotProduct = GLKVector3DotProduct(light, normal);    //设置光亮图层的opacity    CGFloat shadow = 1 + dotProduct - AMBIENT_LIGHT;    UIColor *color = [UIColor colorWithWhite:0 alpha:shadow];    layer.backgroundColor = color.CGColor;}

(3)事件的点击
目前这个立方体点击了没反应,这不是因为响应事件没被处理,而是在于视图的顺序。

之前提过,点击事件的处理由视图在父视图中的顺序决定的,而不是3D空间中的Z轴顺序。当给立方体添加视图的时候,按照视图/图层顺序来说,4,5,6在3的前面(因为4,5,6是后加入的)。即使我们看不见4,5,6的表面(因为被1,2,3遮住了),iOS在事件响应上仍然保持之前的顺序。当试图点击表面3上的按钮,表面4,5,6截断了点击事件(取决于点击的位置),这就和普通的2D布局在按钮上覆盖物体一样。

注意:先添加的视图在下方,因为被后添加的视图
i = 0,UIView.frame={{87.5, 87.5}, {200, 200}}
i = 1,UIView.frame={{287.5, 87.5}, {0, 200}}
i = 2,UIView.frame={{87.5, 87.5}, {200, 0}}
i = 3,UIView.frame={{87.5, 287.5}, {200, 0}}
i = 4,UIView.frame={{87.5, 87.5}, {0, 200}}
i = 5,UIView.frame={{87.5, 87.5}, {200, 200}}

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

   但是有几种正确的方案: 1、把除了表面3的其他视图userInteractionEnabled属性都设置成NO来禁止事件传递。 2、简单通过代码把视图3覆盖在视图6上。

知识点:
控件的层级关系和你加入到父视图的顺序有关,也就是先addsubview至父视图的,层级越低,会被后加入的遮盖。
可以通过以下函数改变子视图的层级:

//将UIView显示在最前面:- (void)bringSubviewToFront:(UIView *)view;//将UIView显示在下面:-(void)sendSubviewToBack:(UIView *)view;

demo
(1)将红色的view1加入到view中,然后在将view2加入到view中,结果view2会覆盖view1

  UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(100, 150, 100, 100)];    view1.backgroundColor = [UIColor redColor];    [self.view addSubview:view1];    UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(150, 200, 100, 100)];    view2.backgroundColor = [UIColor greenColor];    [self.view addSubview:view2];NSLog(@“self.view.subview=%@",self.view.subviews);

结果:self.view.subview=(
frame = (100 150; 100 100);
frame = (150 200; 100 100);
)

这里写图片描述

使用sendSubviewToBack:和bringSubviewToFront修改层级

      // 添加如下修改层级的代码      // 将view2放在最下方       [ self.view  sendSubviewToBack: view2];      // 将view1放在最上方      [  self.view  bringSubviewToFront: view1];NSLog(@“self.view.subview=%@",self.view.subviews);

结果:
self.view.subview=(
frame = (150 200; 100 100);
frame = (100 150; 100 100);
)

这里写图片描述

所以,后加入的视图会遮挡先加入的视图

1 0
原创粉丝点击