1. 前言
上一篇文章主要讲解了CALayer的一些基础动画,本篇文章将要研究一下有关图层旋转、放缩以及平移或倾斜所用的CGAffineTransform,还有可以将扁平物体转换成三维空间对象的CATransform3D。
2. CGAffineTransform
An affine transformation matrix for use in drawing 2D graphics.
一个用于绘制二维图形的仿射变换矩阵。
CGAffineTransform类型属于Core Graphics框架,Core Graphics是一个2D绘图API,并且CGAffineTransform仅仅对2D变换有效。
代码结构如下:
public struct CGAffineTransform {
public var a: CGFloat
public var b: CGFloat
public var c: CGFloat
public var d: CGFloat
public var tx: CGFloat
public var ty: CGFloat
public init()
public init(a: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat, tx: CGFloat, ty: CGFloat)
}
一个仿射变换矩阵是一个3×3的矩阵,如下图所示:
因为第三列总是(0,0,1),所以CGAffineTransform数据结构只包含前两列的值。
如果一个仿射变换矩阵乘以一个代表图形上一点(x, y)的行向量,则会生成一个新的向量表示对应的点(x', y'),公式表示如下:
至于是如何计算的,看下面公式:
至于如何计算,大概了解一下就可以,真正开发中是不用自己计算的,苹果提供了很多好用的API,具体如下:
方法 | 说明 |
---|---|
func rotated(by: CGFloat) -> CGAffineTransform | 返回一个通过旋转当前仿射变换结构而得到仿射变换矩阵。 |
func scaledBy(x: CGFloat, y: CGFloat) -> CGAffineTransform | 返回一个通过放缩当前仿射变换结构而得到仿射变换矩阵。 |
func translatedBy(x: CGFloat, y: CGFloat) -> CGAffineTransform | 返回一个通过平移当前仿射变换结构而得到仿射变换矩阵。 |
func concatenating(CGAffineTransform) -> CGAffineTransform | 返回一个由两个现有仿射变换组合而成的仿射变换矩阵。 |
使用上面前三个方法的时候,我们不用再单独创建一个CGAffineTransform对象,而是由当前图形对象的AffineTransform对象调用上面方法得到一个新的仿射变换矩阵,然后再赋值给当前图形对象的transform属性。代码如下:
func methodTest1() {
// 通过imageView的仿射变换矩阵扩大1.5倍,得到一个新的仿射变换矩阵,并赋值给imageView.transform。
let scaleTransform = imageView.transform.scaledBy(x: 1.5, y: 1.5)
imageView.transform = scaleTransform
// 通过imageView的仿射变换矩阵沿y轴移动50,得到一个新的仿射变换矩阵,并赋值给imageView.transform。
let translateTransform = imageView.transform.translatedBy(x: 0, y: 50)
imageView.transform = translateTransform
// 通过imageView的仿射变换矩阵旋转45度,得到一个新的仿射变换矩阵,并赋值给imageView.transform。
let rotateTransform = imageView.transform.rotated(by: CGFloat(Double.pi/4))
imageView.transform = rotateTransform
}
上面methodTest1方法执行后,imageView放大、下移、旋转同时执行。效果如下图,左边是原图,右边是执行了methodTest1方法之后的效果:
除了通过上面的方法,还可以直接创建一个仿射变换矩阵,再赋值给当前图形对象的transform属性,比如:
func methodTest2() {
// 创建一个基于单位矩阵的1.5倍的仿射变换矩阵,并赋值给imageView.transform。
let scaleTransform = CGAffineTransform(scaleX: 1.5, y: 1.5)
imageView.transform = scaleTransform
// 创建一个基于单位矩阵沿着y轴移动50的仿射变换矩阵,并赋值给imageView.transform。
let translateTransform = CGAffineTransform(translationX: 0, y: 50)
imageView.transform = translateTransform
// 创建一个基于单位矩阵旋转45度的仿射变换矩阵,并赋值给imageView.transform。
let rotateTransform = CGAffineTransform(rotationAngle: CGFloat(Double.pi/4))
imageView.transform = rotateTransform
}
上面methodTest2方法执行后,imageView只会旋转45度,没有放缩和平移效果,因为代码依次执行后,最后imageView.transform只得到了一个旋转仿射矩阵,而methodTest1方法每次赋值都是基于上一次imageView的transform。效果如下图,左边是原图,右边是执行了methodTest2方法之后的效果:
所以methodTest2方法只是说明可以直接创建一个仿射变换矩阵,再赋值给当前图形对象的transform属性。
下面将methodTest2改版一下,即methodTest3:
func methodTest3() {
// 创建一个基于单位矩阵的1.5倍的仿射变换矩阵
let scaleTransform = CGAffineTransform(scaleX: 1.5, y: 1.5)
// 创建一个基于单位矩阵沿着y轴移动50的仿射变换矩阵
let translateTransform = CGAffineTransform(translationX: 0, y: 150)
// 创建一个基于单位矩阵旋转45度的仿射变换矩阵
let rotateTransform = CGAffineTransform(rotationAngle: CGFloat(Double.pi/4))
// 三个矩阵相乘得到一个新的矩阵,,并赋值给imageView.transform。
let finalTransform = scaleTransform.concatenating(translateTransform).concatenating(rotateTransform)
imageView.transform = finalTransform
}
methodTest3中,先创建了三个仿射变换矩阵,然后通过concatenating方法将三个矩阵组合为一个矩阵,最终赋值给imageView.transform。
效果如下图,左边是原图,右边是执行了methodTest3方法之后的效果:
那么
methodTest3和methodTest1的执行效果是否一样呢?答案是不一样的,执行结果图已经给出了答案。
2. CATransform3D
CATransform3D是在Core Animation框架中使用的标准变换矩阵,它能够让图层在3D空间内移动、旋转或者放缩。CATransform3D 是一个可以在3维空间内做变换的4x4的矩阵。
一个三维空间上的点(x, y, z)乘以一个CATransform3D矩阵,则会得到一个对应的三维空间上的点(x', y' z')。
Core Animation中提供了一系列的方法用来创建和组合CATransform3D类型的矩阵,与2D不同的是,3D多了一个z轴,在旋转上可以分别绕x轴、y轴或者z轴旋转。
回顾一下iOS设备上的三维坐标系以及分别绕x轴、y轴和z轴旋转的示意图:
x轴和y轴分别是向右和向下为正方向,而z轴分别垂直于x轴和y轴(也就是垂直于屏幕),指向视角外(屏幕外靠近用户)为正方向。
沿x轴或者y轴旋转,则视图与屏幕平面会发生一定的角度,看起来像是倾斜了。
沿z轴旋转则类似于仿射变换中的旋转。
说了这么多,下面看一下CATransform3D常用的方法:
全局方法或变量 | 说明 |
---|---|
let CATransform3DIdentity: CATransform3D | 3D单位矩阵,[1 0 0 0; 0 1 0 0; 0 0 1 0; 0 0 0 1] |
func CATransform3DIsIdentity(_ t: CATransform3D) -> Bool | 判断矩阵t是否是单位矩阵。 |
func CATransform3DEqualToTransform(_ a: CATransform3D, _ b: CATransform3D) -> Bool | 判断矩阵a和矩阵b是否是相同的矩阵。 |
func CATransform3DMakeTranslation(_ tx: CGFloat, _ ty: CGFloat, _ tz: CGFloat) -> CATransform3D | 返回一个由tx、ty、tz平移变化的矩阵,平移矩阵。 t' = [1 0 0 0; 0 1 0 0; 0 0 1 0; tx ty tz 1]. |
func CATransform3DMakeScale(_ sx: CGFloat, _ sy: CGFloat, _ sz: CGFloat) -> CATransform3D | 返回一个由sx、sy、sz放缩变化的矩阵,放缩矩阵。 t' = [sx 0 0 0; 0 sy 0 0; 0 0 sz 0; 0 0 0 1]. |
func CATransform3DMakeRotation(_ angle: CGFloat, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat) -> CATransform3D | 返回一个由向量(x, y, z)旋转angle弧度的矩阵,旋转矩阵。 |
func CATransform3DTranslate(_ t: CATransform3D, _ tx: CGFloat, _ ty: CGFloat, _ tz: CGFloat) -> CATransform3D | 将矩阵t分别在x轴、y轴、z轴平移tx、ty、tz得到一个新的矩阵。 |
func CATransform3DScale(_ t: CATransform3D, _ sx: CGFloat, _ sy: CGFloat, _ sz: CGFloat) -> CATransform3D | 将矩阵t分别在x轴、y轴、z轴放缩sx、sy、sz得到一个新的矩阵。 |
func CATransform3DRotate(_ t: CATransform3D, _ angle: CGFloat, _ x: CGFloat, _ y: CGFloat, _ z: CGFloat) -> CATransform3D | 将矩阵t沿着向量(x, y, z)旋转angle弧度得到一个新的矩阵。 |
func CATransform3DConcat(_ a: CATransform3D, _ b: CATransform3D) -> CATransform3D | 将矩阵a和矩阵b组合,得到一个新的矩阵。 |
func CATransform3DMakeAffineTransform(_ m: CGAffineTransform) -> CATransform3D | 放回一个和仿射变换矩阵m相同效果的3D矩阵。 |
func CATransform3DIsAffine(_ t: CATransform3D) -> Bool | 判断3D矩阵是否可以通过一个仿射变换矩阵表示出来。 |
func CATransform3DGetAffineTransform(_ t: CATransform3D) -> CGAffineTransform | 返回由t表示的仿射变换矩阵。如果't'不能用仿射变换精确表示,则返回值是未定义的。 |
func CATransform3DInvert(_ t: CATransform3D) -> CATransform3D | 反转t并返回结果。如果t没有逆矩阵,返回原始矩阵。 |
上面的方法中,常用的也就是平移、放缩、旋转等方法。
常用代码示例如下:
func transform3DMethods1() {
// 将单位矩阵移动后得到一个新的平移后的矩阵。
let translateTransform = CATransform3DTranslate(CATransform3DIdentity, 10, 50, 100)
// 将imageLayer移后得到的矩阵再放缩得到一个新的矩阵。
let scaleTransform = CATransform3DScale(translateTransform, 1.5, 1.5, 1.5)
// 将imageLayer移放缩后得到的矩阵再旋转得到一个新的矩阵。
let rotateTransform = CATransform3DRotate(scaleTransform, CGFloat(Double.pi/4), 0, 0, 1)
// 将最终得到的变换矩阵赋值给imageLayer.transform
imageLayer.transform = rotateTransform
}
func transform3DMethods2() {
// 创建一个平移矩阵
let translateTransform = CATransform3DMakeTranslation(10, 50, 100)
// 创建一个放缩矩阵
let scaleTransform = CATransform3DMakeScale(1.5, 1.5, 1.5)
// 创建一个旋转矩阵
let rotateTransform = CATransform3DMakeRotation(CGFloat(Double.pi/4), 0, 0, 1)
// 将平移矩阵与放缩矩阵结合得到一个新矩阵
let tempTransform = CATransform3DConcat(translateTransform, scaleTransform)
// 将上一步得到的新矩阵与旋转矩阵结合得到一个新矩阵
let finalTransform = CATransform3DConcat(tempTransform, rotateTransform)
// 将最终得到的混合矩阵赋值给imageLayer.transform
imageLayer.transform = finalTransform
}
将上面两个方法放到touchesBegan的方法中执行,运行结果如下图:
方法1和方法2的执行结果是不一样的,变换矩阵组合的顺序不同,执行出来的效果是不一样的。
上面的两个方法里面,都对imageLayer进行了z轴方向的移动的放缩,但是从执行的结果看,却看不出来,如果想有一些效果,还需要加入透视效果。
如果将上面的图沿着y轴旋转45度,那么左边的边会靠近我们,右边的边会远离我们,按照近大远小的效果,靠近我们的边应该大于远离我们的边,看一下代码和效果:
func transfrom3DRotateY() {
var rotateTransform = CATransform3DIdentity
rotateTransform = CATransform3DRotate(rotateTransform, CGFloat(Double.pi/4), 0, 1, 0)
imageLayer.transform = rotateTransform
}
实际的执行效果并不是按照上面所理解的那样,而且看起来,图片在水平方向上被压缩了。如果想达到上面说到的近大远小的透视效果,请看下面的透视投影。
3. 透视投影
为了做一些修正,我们需要引入投影变换(又称作z变换)来对除了旋转之外的变换矩阵做一些修改,Core Animation并没有给我们提供设置透视变换的函数,因此我们需要手动修改矩阵值,CATransform3D的透视效果通过矩阵中的一个元素来控制: m34。
m34 的默认值是0,我们可以通过设置m34为(-1.0 / d)来应用透视效果,d代表了想象中视角相机和屏幕之间的距离,以像素为单位,代码中通常500-1000就可以了。一个非常微小的值会让它看起来更加失真,而一个非常大的值会让它基本失去透视效果。
func transfrom3DRotateY() {
var rotateTransform = CATransform3DIdentity
rotateTransform.m34 = -1.0/500.0
rotateTransform = CATransform3DRotate(rotateTransform, CGFloat(Double.pi/4), 0, 1, 0)
imageLayer.transform = rotateTransform
}
执行结果如下,是不是有一些立体感了呢。
func sublayerTransformTest() {
var transform = CATransform3DIdentity
transform.m34 = -1.0/500
self.view.layer.sublayerTransform = transform
let imageView1Transform = CATransform3DMakeRotation(CGFloat(Double.pi/4), 0, 1, 0)
imageView1.layer.transform = imageView1Transform
let imageView2Transform = CATransform3DMakeRotation(CGFloat(-Double.pi/4), 0, 1, 0)
imageView2.layer.transform = imageView2Transform
}

举个例子,创建4个imageView,都放到屏幕中间,此时4个imageView位置重叠,然后将其想4个角落移动,代码如下:
func sublayerTransformTest() {
var transform = CATransform3DIdentity
transform.m34 = -1.0/500
self.view.layer.sublayerTransform = transform
var imageView1Transform = CATransform3DIdentity
imageView1Transform = CATransform3DTranslate(imageView1Transform, -80, -100, 0)
imageView1Transform = CATransform3DRotate(imageView1Transform,CGFloat(Double.pi/4), 0, 1, 0)
imageView1.layer.transform = imageView1Transform
var imageView2Transform = CATransform3DIdentity
imageView2Transform = CATransform3DTranslate(imageView2Transform, 80, -120, 0)
imageView2Transform = CATransform3DRotate(imageView2Transform,CGFloat(-Double.pi/4), 0, 1, 0)
imageView2.layer.transform = imageView2Transform
var imageView3Transform = CATransform3DIdentity
imageView3Transform = CATransform3DTranslate(imageView3Transform, -90, 120, 0)
imageView3Transform = CATransform3DRotate(imageView3Transform,CGFloat(Double.pi/4), 0, 1, 0)
imageView3.layer.transform = imageView3Transform
var imageView4Transform = CATransform3DIdentity
imageView4Transform = CATransform3DTranslate(imageView4Transform, 100, 140, 0)
imageView4Transform = CATransform3DRotate(imageView4Transform,CGFloat(-Double.pi/4), 0, 1, 0)
imageView4.layer.transform = imageView4Transform
}
执行结果图如下:
4个图都移动到了屏幕的不同位置,因为是从中心位置移动出去的,所以他们共享一个灭点,刚才移动的代码没有在z轴移动,现在把4个图沿着z轴远离视角,将z轴值0改为-100000,则得到下面的图,越远则图越小,4个图越居于一点。
func rotate180() {
var rotateTransform = CATransform3DIdentity
rotateTransform = CATransform3DRotate(rotateTransform,CGFloat(Double.pi), 0, 1, 0)
imageView.layer.transform = rotateTransform
}
4. 总结
本篇文章主要讲述了CALayer的2D仿射变换和3D变化,比如平移、放缩以及旋转,同时列举了常用的方法和方法示例,效果示例等等。随后又讲解了透视投影,灭点,以及关闭背面绘制等。
文中如果有阐述不正确的地方,还请路过的朋友指正。
本篇文章出自https://blog.csdn.net/guoyongming925的博客,如需转载,请标明出处。