CG的前缀告诉我们,CGAffineTransform类型属于Core Graphic框架,Core Graphic实际上是一个严格意义上的2D绘图API,并且CGAffineTransform仅仅对2D变换有效。
在前面,咱们说过zPosition属性,可以用来让图层靠近或者远离相机,transform(CATransform3D类型)属性可以真正做到这点,即让图层在3D空间内移动或者旋转。和CGAffineTransform类似,CATransform3D也是一个矩阵,但是它是一个可以在3维空间内做变换的4x4的矩阵:
CATransform3D也有一系列的方法用来创建和组合矩阵,3D的平移和旋转多了一个 z 参数,并且旋转函数除了angle之外多出了x,y,z三个参数,分别决定了每个坐标轴方向上的旋转:
CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)
由图所见,绕Z轴的旋转等同于之前二维空间的仿射旋转,但是绕X轴和Y轴的旋转就突破了屏幕的二维空间,并且在用户视角看来发生了倾斜。
透视投影
在现实中,当物体远离我们的时候,由于视角的原因看起来会变小,理论上说远离我们的视图的边要比靠近视角的边短,但实际上并没有。而我们当前的视角是等距离的,也就是在3D变换中仍然保持平行,和之前的仿射变换类似。 在等距投影中,远处的物体和近处的物体保持同样的缩放比例。
为了做一些修正,我们需要引入投影变换(又称作z变换)来对除了旋转之外的变换矩阵做一些修改,Core Animation并没有给我们提供设置透视变换的函数,因此我们需要手动修改矩阵值,庆幸的是,CATransform3D的透视效果通过矩阵中一个简单的元素来控制:m34。它用于按比例缩放X和Y的值来计算到底要离视角多远。
m34的默认值是0,我们可以通过设置m34为-1.0/d来应用透视效果,d代表了想象中视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢?实际上用一个大概的估算就好了。
因为视角相机实际上并不存在,所以可以根据屏幕上的显示效果自由决定它的位置。通常500-1000就已经很好了,但对于特定的图层有时候更小后者更大的值会看起来更舒服,减少距离的值会增强透视效果,所以一个非常微小的值会让它看起来更加失真,然而一个非常大的值会让它基本上失去透视效果:
@implementation ViewController
- (void)viewDidLoad
{
[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;
}
@end
灭点
当在透视角度绘图的时候,远离相机视角的物体将会变小变远,当远离到一个极限距离,它们可能就缩成一个点,于是所有的物体最后都汇聚消失在同一个点。 在现实中,这个点通常是视图的中心,于是为了在应用中创建拟真效果的透视,这个点应该聚在屏幕中点,或者至少是包含所有3D视图中点:
Core Animation定义了这个点位于变换图层的anchorPoint这也就是说当图层发生变换时,这个点永远位于图层变换之前anchorPoint的位置。
当改变一个图层的position,你也改变了它的灭点,做3D变换的时候要时刻记住这一点,当你视图通过调整m34来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置,这样所有的3D图层都共享一个灭点。
sublayerTransform属性
如果有多个视图或者图层,每个都做3D变换,那就需要分别设置相同的m34值,并且确保在变换之前都在屏幕中央共享同一个position,如果一个函数封装这些操作的确会更加方便,但仍然有限制。下面给你说一个更好的方法:
CALayer有一个属性叫做sublayerTransform。它也是CATransform3D类型,但和对一个图层的变换不同,它影响到所有的子图层。这意味着你可以一次性对包含这些图层的容器做变换,于是所有的子图层都自动继承了这个变换方法。
相较而言,通过在一个地方设置透视变换会很方便,同时它会带来另一个显著的优势:灭点被设置在容器图层的中点,从而不需要再对子图层分别设置了。这意味着你可以随意使用position和frame来放置子图层,而不需要把它们放置在屏幕中点,然后为了保证统一的灭点用变换来做平移。
下面举个🌰: 我们在容器里并排放置两个视图如下:
代码如下:
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;
@end
@implementation ViewController
- (void)viewDidLoad
{
[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;
}
得到的结果: