5.1 仿射变换
最后更新于:2022-04-01 10:08:57
# 仿射变换
在第三章“图层几何学”中,我们使用了`UIView`的`transform`属性旋转了钟的指针,但并没有解释背后运作的原理,实际上`UIView`的`transform`属性是一个`CGAffineTransform`类型,用于在二维空间做旋转,缩放和平移。`CGAffineTransform`是一个可以和二维空间向量(例如`CGPoint`)做乘法的3X2的矩阵(见图5.1)。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-24_567bc1f7614a0.png)
图5.1 用矩阵表示的`CGAffineTransform`和`CGPoint`
用`CGPoint`的每一列和`CGAffineTransform`矩阵的每一行对应元素相乘再求和,就形成了一个新的`CGPoint`类型的结果。要解释一下图中显示的灰色元素,为了能让矩阵做乘法,左边矩阵的列数一定要和右边矩阵的行数个数相同,所以要给矩阵填充一些标志值,使得既可以让矩阵做乘法,又不改变运算结果,并且没必要存储这些添加的值,因为它们的值不会发生变化,但是要用来做运算。
因此,通常会用3×3(而不是2×3)的矩阵来做二维变换,你可能会见到3行2列格式的矩阵,这是所谓的以列为主的格式,图5.1所示的是以行为主的格式,只要能保持一致,用哪种格式都无所谓。
当对图层应用变换矩阵,图层矩形内的每一个点都被相应地做变换,从而形成一个新的四边形的形状。`CGAffineTransform`中的“仿射”的意思是无论变换矩阵用什么值,图层中平行的两条线在变换之后任然保持平行,`CGAffineTransform`可以做出任意符合上述标注的变换,图5.2显示了一些仿射的和非仿射的变换:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-24_567bc1f77c2a1.png)
### 创建一个`CGAffineTransform`
对矩阵数学做一个全面的阐述就超出本书的讨论范围了,不过如果你对矩阵完全不熟悉的话,矩阵变换可能会使你感到畏惧。幸运的是,Core Graphics提供了一系列函数,对完全没有数学基础的开发者也能够简单地做一些变换。如下几个函数都创建了一个`CGAffineTransform`实例:
~~~
CGAffineTransformMakeRotation(CGFloat angle)
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)
~~~
旋转和缩放变换都可以很好解释--分别旋转或者缩放一个向量的值。平移变换是指每个点都移动了向量指定的x或者y值--所以如果向量代表了一个点,那它就平移了这个点的距离。
我们用一个很简单的项目来做个demo,把一个原始视图旋转45度角度(图5.3)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-24_567bc1f7a5849.png)
图5.3 使用仿射变换旋转45度角之后的视图
`UIView`可以通过设置`transform`属性做变换,但实际上它只是封装了内部图层的变换。
`CALayer`同样也有一个`transform`属性,但它的类型是`CATransform3D`,而不是`CGAffineTransform`,本章后续将会详细解释。`CALayer`对应于`UIView`的`transform`属性叫做`affineTransform`,清单5.1的例子就是使用`affineTransform`对图层做了45度顺时针旋转。
清单5.1 使用`affineTransform`对图层旋转45度
~~~
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//rotate the layer 45 degrees
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
self.layerView.layer.affineTransform = transform;
}
@end
~~~
注意我们使用的旋转常量是`M_PI_4`,而不是你想象的45,因为iOS的变换函数使用弧度而不是角度作为单位。弧度用数学常量pi的倍数表示,一个pi代表180度,所以四分之一的pi就是45度。
C的数学函数库(iOS会自动引入)提供了pi的一些简便的换算,`M_PI_4`于是就是pi的四分之一,如果对换算不太清楚的话,可以用如下的宏做换算:
~~~
#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)
~~~
## 混合变换
Core Graphics提供了一系列的函数可以在一个变换的基础上做更深层次的变换,如果做一个既要*缩放*又要*旋转*的变换,这就会非常有用了。例如下面几个函数:
~~~
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)
~~~
当操纵一个变换的时候,初始生成一个什么都不做的变换很重要--也就是创建一个`CGAffineTransform`类型的空值,矩阵论中称作*单位矩阵*,Core Graphics同样也提供了一个方便的常量:
~~~
CGAffineTransformIdentity
~~~
最后,如果需要混合两个已经存在的变换矩阵,就可以使用如下方法,在两个变换的基础上创建一个新的变换:
~~~
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);
~~~
我们来用这些函数组合一个更加复杂的变换,先缩小50%,再旋转30度,最后向右移动200个像素(清单5.2)。图5.4显示了图层变换最后的结果。
清单5.2 使用若干方法创建一个复合变换
~~~
- (void)viewDidLoad
{
[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;
}
~~~
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-24_567bc1f7cbd0a.png)
图5.4 顺序应用多个仿射变换之后的结果
图5.4中有些需要注意的地方:图片向右边发生了平移,但并没有指定距离那么远(200像素),另外它还有点向下发生了平移。原因在于当你按顺序做了变换,上一个变换的结果将会影响之后的变换,所以200像素的向右平移同样也被旋转了30度,缩小了50%,所以它实际上是斜向移动了100像素。
这意味着变换的顺序会影响最终的结果,也就是说旋转之后的平移和平移之后的旋转结果可能不同。
~~~
#define DEGREES_TO_RADIANS(x) ((x)/180.0*M_PI)
~~~