用 CGPoint 的每一列和 CGAffineTransform 矩阵的每一行对应元素相乘再求 和,就造成了一个新的 CGPoint 类型的结果。要解释一下图中显示的灰色元素, 为了能让矩阵作乘法,左边矩阵的列数必定要和右边矩阵的行数个数相同,因此要 给矩阵填充一些标志值,使得既可让矩阵作乘法,又不改变运算结果,而且没必 要存储这些添加的值,由于它们的值不会发生变化,可是要用来作运算。数组
UIView 的 transform 属性是一 个 CGAffineTransform 类型,用于在二维空间作旋转,缩放和平移。 CGAffineTransform 是一个能够和二维空间向量(例如 的3X2的矩阵(见图5.1)。app
CALayer 一样也有一个 不是 transform CGAffineTransform 属性,但它的类型是 CATransform3D ,而 ,本章后续将会详细解释。 CALayer 对应 于 UIView 的 transform 属性叫作 affineTransform框架
注意咱们使用的旋转常量是 M_PI_4 ,而不是你想象的45,由于iOS的变换函数使 用弧度而不是角度做为单位。弧度用数学常量pi的倍数表示,一个pi表明180度,所 以四分之一的pi就是45度ide
Core Graphics提供了一系列的函数能够在一个变换的基础上作更深层次的变换,函数
若是作一个既要缩放又要旋转的变换,这就会很是有用了。例以下面几个函数:布局
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle) CGAffineTransformScale(CGAffineTransform t, CGFloat sx,CGFloat sy) CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat
当操纵一个变换的时候,初始生成一个什么都不作的变换很重要--也就是建立一 个 CGAffineTransform 类型的空值,矩阵论中称做单位矩阵,Core Graphics一样也提供了一个方便的常量:CGAffineTransformIdentity学习
最后,若是须要混合两个已经存在的变换矩阵,就可使用以下方法,在两个变换 的基础上建立一个新的变换:CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2.....ui
举个🌰:
咱们来用这些函数组合一个更加复杂的变换,先缩小50%,再旋转30度,最后向右 移动200个像素(清单5.2)。图5.4显示了图层变换最后的结果。3d
[super viewDidLoad]; //create a new transform CGAffineTransform transform = CGAffineTransformIdentity; //scale by 50% transform = CGAffineTransformScale(transform, 0.5, 0.5); //rotate by 30 degrees transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0); //translate by 200 points transform = CGAffineTransformTranslate(transform, 200, 0); //apply transform to layer self.layerView.layer.affineTransform = transform;
中有些须要注意的地方:图片向右边发生了平移,但并无指定距离那么远 (200像素),另外它还有点向下发生了平移。缘由在于当你按顺序作了变换,上 一个变换的结果将会影响以后的变换,因此200像素的向右平移一样也被旋转了30 度,缩小了50%,因此它其实是斜向移动了100像素。code
这意味着变换的顺序会影响最终的结果,也就是说旋转以后的平移和平移以后的旋 转结果可能不一样。
CG的前缀告诉咱们,CGAffineTransform类型属于CoreGraphics框架,CoreGraphics其实是一个严格意义上的2D绘图API,而且 CGAffineTransform 仅仅 对2D变换有效。
transform 属性(CATransform3D类型)能够真正作到这点,即让图层在3D空间内移动或者旋转
CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z) CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz) CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)
xy轴是垂直于手机屏幕的,
绕Z轴的旋转等同于以前二维空间的仿射旋转,可是绕X轴和Y轴的旋转就突破了屏幕的二维空间,而且在用户视角看来发生了倾斜。
,此时并未真正实现3D的展现效果,也是因为此因此有了透视
在真实世界中,当物体远离咱们的时候,因为视角的缘由看起来会变小,理论上说 远离咱们的视图的边要比靠近视角的边跟短,但实际上并无发生,而咱们当前的 视角是等距离的,也就是在3D变换中任然保持平行,和以前提到的仿射变换相似。
在等距投影中,远处的物体和近处的物体保持一样的缩放比例,这种投影也有它自 己的用处(例如建筑绘图,颠倒,和伪3D视频),但当前咱们并不须要。
m34的默认值是0,咱们能够经过设置m34为-1.0/d来应用透视效果,d表明了想象中视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离 呢?实际上并不须要,大概估算一个就行了。
由于视角相机实际上并不存在,因此能够根据屏幕上的显示效果自由决定它的防止 的位置。一般500-1000就已经很好了,但对于特定的图层有时候更小后者更大的值 会看起来更舒服,减小距离的值会加强透视效果,因此一个很是微小的值会让它看 起来更加失真,然而一个很是大的值会让它基本失去透视效果,对视图应用透视的
[super viewDidLoad]; //create a new transform CATransform3D transform = CATransform3DIdentity; //apply perspective transform.m34 = - 1.0 / 500.0; //rotate by 45 degrees along the Y axis transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0); //apply to layer self.layerView.layer.transform = transform;
当在透视角度绘图的时候,远离相机视角的物体将会变小变远,当远离到一个极限 距离,它们可能就缩成了一个点,因而全部的物体最后都汇聚消失在同一个点
在现实中,这个点一般是视图的中心(图5.11),因而为了在应用中建立拟真效果 的透视,这个点应该聚在屏幕中点,或者至少是包含全部3D对象的视图中点。
这就是说,当图层发生变换时,这个点永远位于图 层变换以前 anchorPoint 的位置。
当改变一个图层的position,你也改变了它的灭点,作3D变换的时候要时刻记 住这一点,当你视图经过调整m34来让它更加有3D效果,应该首先把它放置于屏幕中央,而后经过平移来把它移动到指定位置(而不是直接改变它的position),这样全部的3D图层都共享一个灭点。
若是有多个视图或者图层,每一个都作3D变换,那就须要分别设置相同的m34值,并 且确保在变换以前都在屏幕中央共享同一个 position ,若是用一个函数封装这些 操做的确会更加方便,但仍然有限制(例如,你不能在Interface Builder中摆放视 图),这里有一个更好的方法。
CALayer有一个属性叫作sublayerTransform它也是CATransform3D类
型,但和对一个图层的变换不一样,它影响到全部的子图层。这意味着你能够一次性 对包含这些图层的容器作变换,因而全部的子图层都自动继承了这个变换方法。
灭点被设置在容器图层的中点,从而不须要再对子图层分别设置了。这意味着 你能够随意使用 position 和 frame 来放置子图层,而不须要把它们放置在屏幕中点,而后为了保证统一的灭点用变换来作平移。
[super viewDidLoad]; //apply perspective transform to container CATransform3D perspective = CATransform3DIdentity; perspective.m34 = - 1.0 / 500.0; self.containerView.layer.sublayerTransform = perspective; //rotate layerView1 by 45 degrees along the Y axis CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0); self.layerView1.layer.transform = transform1; //rotate layerView2 by 45 degrees along the Y axis CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0); self.layerView2.layer.transform = transform2;
如你所见,图层是双面绘制的,反面显示的是正面的一个镜像图片。
但这并非一个很好的特性,由于若是图层包含文本或者其余控件,那用户看到这 些内容的镜像图片固然会感到困惑。另外也有可能形成资源的浪费:想象用这些图 层造成一个不透明的固态立方体,既然永远都看不见这些图层的背面,那为何浪 费GPU来绘制它们呢?
CALayer有一个叫作doubleSided的属性来控制图层的背面是否要被绘制。这 BOOL 类型,默认为YES,若是设置为NO,那么当图层正面从相机视角是一个 消失的时候,它将不会被绘制。
[super viewDidLoad]; //rotate the outer layer 45 degrees CATransform3D outer = CATransform3DMakeRotation(M_PI_4, 0, 0, 1); self.outerView.layer.transform = outer; //rotate the inner layer -45 degrees CATransform3D inner = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1); self.innerView.layer.transform = inner;
[super viewDidLoad]; //rotate the outer layer 45 degrees CATransform3D outer = CATransform3DIdentity; outer.m34 = -1.0 / 500.0; outer = CATransform3DRotate(outer, M_PI_4, 0, 1, 0); self.outerView.layer.transform = outer; //rotate the inner layer -45 degrees CATransform3D inner = CATransform3DIdentity; inner.m34 = -1.0 / 500.0; inner = CATransform3DRotate(inner, -M_PI_4, 0, 1, 0); self.innerView.layer.transform = inner;
但其实这并非咱们所看到的,相反,咱们看到的结果如图5.18所示。发什么了什 么呢?内部的图层仍然向左侧旋转,而且发生了扭曲,但按道理说它应该保持正面 朝上,而且显示正常的方块。
这是因为尽管CoreAnimation图层存在于3D空间以内,但它们并不都存在同一个 3D空间。每一个图层的3D场景实际上是扁平化的,当你从正面观察一个图层,看到的实际上由子图层建立的想象出来的3D场景,但当你倾斜这个图层,你会发现实际上这个3D场景仅仅是被绘制在图层的表面。(仍是因为Z轴的变换实际上是有xy共同做用代替的,没有真实的Z)
相似的,当你在玩一个3D游戏,实际上仅仅是把屏幕作了一次倾斜,或许在游戏中 能够看见有一面墙在你面前,可是倾斜屏幕并不可以看见墙里面的东西。全部场景 里面绘制的东西并不会随着你观察它的角度改变而发生变化;图层也是一样的道 理。
这使得用Core Animation建立很是复杂的3D场景变得十分困难。你不可以使用图层 树去建立一个3D结构的层级关系--在相同场景下的任何3D表面必须和一样的图层保 持一致,
这是由于每一个的父视图都把它的子视图扁平化了。
至少当你用正常的CALayer的时候是这样,
CALayer 有一个叫作CATransformLayer的子类来解决这个问题。具体在第六章“特殊的图层”中将会具体讨论
- (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform { //get the face view and add it to the container UIView *face = self.faces[index]; [self.containerView addSubview:face]; //center the face view within the container CGSize containerSize = self.containerView.bounds.size; face.center = CGPointMake(containerSize.width / 2.0, containerSize.heigh // apply the transform face.layer.transform = transform; } - (void)viewDidLoad { [super viewDidLoad]; //set up the container sublayer transform CATransform3D perspective = CATransform3DIdentity; perspective.m34 = -1.0 / 500.0; self.containerView.layer.sublayerTransform = perspective; //add cube face 1 CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100); [self addFace:0 withTransform:transform]; //add cube face 2 transform = CATransform3DMakeTranslation(100, 0, 0); transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0); [self addFace:1 withTransform:transform]; //add cube face 3 transform = CATransform3DMakeTranslation(0, -100, 0); transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0); [self addFace:2 withTransform:transform]; //add cube face 4 transform = CATransform3DMakeTranslation(0, 100, 0); transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0); [self addFace:3 withTransform:transform]; //add cube face 5 transform = CATransform3DMakeTranslation(-100, 0, 0); transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0); [self addFace:4 withTransform:transform]; //add cube face 6 transform = CATransform3DMakeTranslation(0, 0, -100); transform = CATransform3DRotate(transform, M_PI, 0, 1, 0); [self addFace:5 withTransform:transform]; } @end
添加以下几行去旋转containerView 图层的
perspective变换矩阵:
perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0); perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);
Core Animation能够用3D显示图层,可是它对光线并无概念。若是想让立方体看 起来更加真实,须要本身作一个阴影效果。你能够经过改变每一个面的背景颜色或者 直接用带光亮效果的图片来调整。
若是须要动态地建立光线效果,你能够根据每一个视图的方向应用不一样的alpha值作 出半透明的阴影图层,但为了计算阴影图层的不透明度,你须要获得每一个面的正太 向量(垂直于表面的向量),而后根据一个想象的光源计算出两个向量叉乘结果。 叉乘表明了光源和图层之间的角度,从而决定了它有多大程度上的光亮。
咱们用GLKit框架来作向量的计算(你须要引入 GLKit库来运行代码),每一个面的 CATransform3D 都被转换成 GLKMatrix4 ,然 后经过 GLKMatrix4GetMatrix3 函数得出一个3×3的旋转矩阵。这个旋转矩阵指
定了图层的方向,而后能够用它来获得正太向量的值
,点击事件的处理由视图在父视图中的顺 序决定的,并非3D空间中的Z轴顺序。当给立方体添加视图的时候,咱们实际上 是按照一个顺序添加,因此按照视图/图层顺序来讲,4,5,6在3的前面。
即便咱们看不见4,5,6的表面(由于被1,2,3遮住了),iOS在事件响应上仍然 保持以前的顺序。当试图点击表面3上的按钮,表面4,5,6截断了点击事件(取决 于点击的位置),这就和普通的2D布局在按钮上覆盖物体同样。
这里有几种正确的方案:把除了表面3的其余视图 userInteractionEnabled 属性 都设置成 NO 来禁止事件传递。或者简单经过代码把视图3覆盖在视图6上。不管样均可以点击按钮了(图5.23)
这一章涉及了一些2D和3D的变换。你学习了一些矩阵计算的基础,以及如何用 Core Animation建立3D场景。你看到了图层背后究竟是如何呈现的,而且知道了不 能把扁平的图片作成真实的立体效果,最后咱们用demo说明了触摸事件的处理, 视图中图层添加的层级顺序会比屏幕上显示的顺序更有意义。