1. CGAffineTransform
-
CGAffineTransform基础
UIView有一个CGAffineTransform类型的属性,叫transform,用于在二维空间做旋转,缩放和平移。CALayer也有一个与之对应的属性叫affineTransform。CGAffineTransform所涉及的矩阵知识,这里有一篇文章说得很清楚: IOS矩阵之后的数学知识。CGPoint与CGAffineTransform的关系如图:
用CGPoint的每一列和CGAffineTransform矩阵的每一行对应元素相乘再求和,就形成了一个新的CGPoint类型的结果。要解释一下图中显示的灰色元素,为了能让矩阵做乘法,左边矩阵的列数一定要和右边矩阵的行数个数相同,所以要给矩阵填充一些标志值,使得既可以让矩阵做乘法,又不改变运算结果,并且没必要存储这些添加的值,因为它们的值不会发生变化,但是要用来做运算。具体运算过程是这样的:
当对图层应用变换矩阵,图层矩形内的每一个点都被相应地做变换,从而形成一个新的四边形的形状。CGAffineTransform中的“仿射”的意思是无论变换矩阵用什么值,图层中平行的两条线在变换之后任然保持平行,CGAffineTransform可以做出任意符合上述标注的变换。下图显示了一些仿射的和非仿射的变换:
-
CGAffineTransform应用
CGAffineTransform的前缀CG即Core Graphics,Core Graphics提供了一系列函数,对完全没有数学基础的开发者也能够简单地做一些变换。如下几个函数都创建了一个CGAffineTransform实例:
/* Return a transform which translates by `(tx, ty)':
t' = [ 1 0 0 1 tx ty ] */
CG_EXTERN CGAffineTransform CGAffineTransformMakeTranslation(CGFloat tx,
CGFloat ty) CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
/* Return a transform which scales by `(sx, sy)':
t' = [ sx 0 0 sy 0 0 ] */
CG_EXTERN CGAffineTransform CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
/* Return a transform which rotates by `angle' radians:
t' = [ cos(angle) sin(angle) -sin(angle) cos(angle) 0 0 ] */
CG_EXTERN CGAffineTransform CGAffineTransformMakeRotation(CGFloat angle)
CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
举个栗子,点击屏幕,将子视图顺时针旋转45度:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController{
UIView *layerView;
}
-(void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
layerView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 100.0f, 100.0f)];
layerView.backgroundColor = [UIColor purpleColor];
layerView.center = self.view.center;
[self.view addSubview:layerView];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[UIView animateWithDuration:1.25f animations:^{
CGAffineTransform transform = CGAffineTransformMakeRotation(45.0f/180.0f*M_PI);
layerView.layer.affineTransform = transform;
}];
}
@end
运行效果:
π是弧度单位,其对应的角度为180度,Core Animation使用弧度最为参数
弧度转角度:
#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)
角度转弧度:
#define DEGREES_TO_RADIANS(x) ((x)/180.0*M_PI)
-
复合变换
CGAffineTransformConcat方法可以混合两个已经存在的变换矩阵,在两个变换的基础上创建一个新的变换,举个栗子,修改上面的代码:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController{
UIView *layerView;
}
-(void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
layerView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 100.0f, 100.0f)];
layerView.backgroundColor = [UIColor purpleColor];
layerView.center = self.view.center;
[self.view addSubview:layerView];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[UIView animateWithDuration:1.25f animations:^{
//CGAffineTransform transform = CGAffineTransformMakeRotation(45.0f/180.0f*M_PI);
//layerView.layer.affineTransform = transform;
CGAffineTransform rotation = CGAffineTransformMakeRotation(45.0f/180.0f*M_PI);
CGAffineTransform scale = CGAffineTransformMakeScale(1.5f, 1.5f);
CGAffineTransform transform = CGAffineTransformConcat(rotation, scale);
layerView.layer.affineTransform = transform;
}];
}
@end
运行效果:
CGAffineTransformConcat方法只能接受两个参数,要做两种以上的复合变换的话,需要用到下面这些方法:
/* Translate `t' by `(tx, ty)' and return the result:
t' = [ 1 0 0 1 tx ty ] * t */
CG_EXTERN CGAffineTransform CGAffineTransformTranslate(CGAffineTransform t,
CGFloat tx, CGFloat ty) CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
/* Scale `t' by `(sx, sy)' and return the result:
t' = [ sx 0 0 sy 0 0 ] * t */
CG_EXTERN CGAffineTransform CGAffineTransformScale(CGAffineTransform t,
CGFloat sx, CGFloat sy) CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
/* Rotate `t' by `angle' radians and return the result:
t' = [ cos(angle) sin(angle) -sin(angle) cos(angle) 0 0 ] * t */
CG_EXTERN CGAffineTransform CGAffineTransformRotate(CGAffineTransform t,
CGFloat angle) CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
举个栗子,我们来用这些函数组合一个更加复杂的变换,先缩小50%,再顺时针旋转30度,最后向右移动200个像,继续修改上面的代码:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController{
UIView *layerView;
}
-(void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
layerView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 100.0f, 100.0f)];
layerView.backgroundColor = [UIColor purpleColor];
layerView.center = self.view.center;
[self.view addSubview:layerView];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[UIView animateWithDuration:1.25f animations:^{
//CGAffineTransform transform = CGAffineTransformMakeRotation(45.0f/180.0f*M_PI);
//layerView.layer.affineTransform = transform;
//CGAffineTransform rotation = CGAffineTransformMakeRotation(45.0f/180.0f*M_PI);
//CGAffineTransform scale = CGAffineTransformMakeScale(1.5f, 1.5f);
//CGAffineTransform transform = CGAffineTransformConcat(rotation, scale);
//layerView.layer.affineTransform = transform;
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformScale(transform, 0.5f, 0.5f);
transform = CGAffineTransformRotate(transform, 30.0f/180.0f*M_PI);
transform = CGAffineTransformTranslate(transform, 200.0f, 0.0f);
layerView.layer.affineTransform = transform;
}];
}
@end
运行效果:
有些需要注意的地方:图片向右边发生了平移,但并没有指定距离那么远(200像素),另外它还有点向下发生了平移。原因在于当你按顺序做了变换,上一个变换的结果将会影响之后的变换,所以200像素的向右平移同样也被旋转了30度,缩小了50%,所以它实际上是斜向移动了100像素。这意味着变换的顺序会影响最终的结果,也就是说旋转之后的平移和平移之后的旋转结果可能不同。
2. CATransform3D
-
CATransform3D基础
affineTransform属性只能用来做2D变换,CALayer还有一个用来做3D变换的属性叫transform,其类型是CATransform3D类型。和CGAffineTransform类似,CATransform3D也是一个矩阵,但是和2x3的矩阵不同,CATransform3D是一个可以在3维空间内做变换的4x4的矩阵:
我们对X轴和Y轴比较熟悉了,分别以右和下为正方向,Z轴和这两个轴分别垂直,指向视角外为正方向:
由图所见,绕Z轴的旋转等同于之前二维空间的仿射旋转,但是绕X轴和Y轴的旋转就突破了屏幕的二维空间,并且在用户视角看来发生了倾斜。
-
CATransform3D应用
CATransform3D类型的矩阵,和Core Graphics的函数类似,但是3D的平移和旋转多处了一个z参数,并且旋转函数除了angle之外多出了x,y,z三个参数,分别决定了每个坐标轴方向上的旋转:
/* Returns a transform that translates by '(tx, ty, tz)':
* t' = [1 0 0 0; 0 1 0 0; 0 0 1 0; tx ty tz 1]. */
CA_EXTERN CATransform3D CATransform3DMakeTranslation (CGFloat tx,
CGFloat ty, CGFloat tz)
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
/* Returns a transform that scales by `(sx, sy, sz)':
* t' = [sx 0 0 0; 0 sy 0 0; 0 0 sz 0; 0 0 0 1]. */
CA_EXTERN CATransform3D CATransform3DMakeScale (CGFloat sx, CGFloat sy,
CGFloat sz)
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
/* Returns a transform that rotates by 'angle' radians about the vector
* '(x, y, z)'. If the vector has length zero the identity transform is
* returned. */
CA_EXTERN CATransform3D CATransform3DMakeRotation (CGFloat angle, CGFloat x,
CGFloat y, CGFloat z)
CA_AVAILABLE_STARTING (10.5, 2.0, 9.0, 2.0);
举个栗子,点击屏幕将子视图旋转-45度:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController{
UIView *layerView;
}
-(void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
layerView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 150.0f, 150.0f)];
layerView.center = self.view.center;
layerView.layer.backgroundColor = [UIColor blueColor].CGColor;
[self.view addSubview:layerView];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[UIView animateWithDuration:1.25f animations:^{
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DRotate(transform, -(45.0f/180.0f*M_PI), 0.0f, 1.0f, 0.0f);
layerView.layer.transform = transform;
}];
}
@end
运行效果:
看起来好像只是宽度被压扁了而已,这是因为我们在用一个斜向的视角看它,而不是透视。
-
透视效果
CATransform3D的透视效果通过一个矩阵中一个很简单的元素来控制:m34。m34用于按比例缩放X和Y的值来计算到底要离视角多远,其默认值是0,我们可以通过设置m34为-1.0 / d来应用透视效果,d代表了想象中视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢?实际上并不需要如何计算,大概估算一个就好了,一般是500-1000。修改上面的代码:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController{
UIView *layerView;
}
-(void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
layerView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 150.0f, 150.0f)];
layerView.center = self.view.center;
layerView.layer.backgroundColor = [UIColor blueColor].CGColor;
[self.view addSubview:layerView];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[UIView animateWithDuration:1.25f animations:^{
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0f / 500.0f;
transform = CATransform3DRotate(transform, -(45.0f/180.0f*M_PI), 0.0f, 1.0f, 0.0f);
layerView.layer.transform = transform;
}];
}
@end
运行效果:
-
灭点
当在透视角度绘图的时候,远离相机视角的物体将会变小变远,当远离到一个极限距离,它们可能就缩成了一个点,于是所有的物体最后都汇聚消失在同一个点,这个点叫灭点。
Core Animation定义了灭点位于变换图层的anchorPoint,这就是说,当图层发生变换时,这个点永远位于图层变换之前anchorPoint的位置。当改变一个图层的position,你也改变了它的灭点,做3D变换的时候要时刻记住这一点,当你视图通过调整m34来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置(而不是直接改变它的position),这样所有的3D图层都共享一个灭点。
-
sublayerTransform
如果有多个子视图或者子图层,每个都做3D变换,那就需要分别设置相同的m34值,并且确保在变换之前都在屏幕中央共享同一个position,这样操作起来很麻烦,CALayer有一个叫做sublayerTransform的属性可以解决这个问题。
sublayerTransform也是CATransform3D类型,但和对一个图层的变换不同,它影响到所有的子图层,这意味着你可以一次性对包含这些图层的容器做变换。
使用sublayerTransform,灭点被设置在容器图层的中点,从而不需要再对子图层分别设置了。这意味着你可以随意使用position和frame来放置子图层,而不需要把它们放置在屏幕中点,然后为了保证统一的灭点用变换来做平移。
-
doubleSided
CALayer有一个叫做doubleSided的属性来控制图层的背面是否要被绘制,这是一个BOOL类型,默认为YES,如果设置为NO,那么当图层正面从相机视角消失的时候,它将不会被绘制。举个栗子:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController{
UIView *topView;
UIView *bottomView;
}
-(void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIImage *image = [UIImage imageNamed:@"3"];
topView = [[UIView alloc] initWithFrame:CGRectMake(85.0f, 100.0f, 150.0f, 150.0f)];
bottomView = [[UIView alloc] initWithFrame:CGRectMake(85.0f, 300.0f, 150.0f, 150.0f)];
bottomView.layer.doubleSided = NO;
[self configView:topView image:image];
[self configView:bottomView image:image];
}
-(void)configView:(UIView *)aView image:(UIImage *)aImage{
aView.layer.backgroundColor = [UIColor greenColor].CGColor;
aView.layer.contents = (__bridge id _Nullable)(aImage.CGImage);
aView.layer.contentsGravity = kCAGravityResizeAspect;
aView.layer.contentsScale = [UIScreen mainScreen].scale;
[self.view addSubview:aView];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[UIView animateWithDuration:5.0f animations:^{
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DRotate(transform, M_PI, 0.0f, 1.0f, 0.0f);
topView.layer.transform = transform;
bottomView.layer.transform = transform;
}];
}
@end
运行效果:
3. 固体对象
现在我们懂得了在3D空间的一些图层布局的基础,接下来试着创建一个固态的3D对象(实际上是一个技术上所谓的空洞对象,但它以固态呈现),我们用三个独立的视图来构建一个残缺的正方体
Interface Build布局如图:
代码:
#import "TestViewController.h"
@interface TestViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;
@end
@implementation TestViewController
- (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.height / 2.0);
// 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];
}
@end
运行效果:
从这个角度看正方体并不是很明显,看起来只是一个方块,为了更好地欣赏它,我们将更换一个不同的视角
但是旋转这个正方体将会显得很笨重,因为我们要单独对每个面做旋转,这时候我们之前说的sublayerTransform就派上了用场
添加两行代码去旋转containerView图层的perspective变换矩阵:
perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);
把相机(或者相对相机的整个场景)绕Y轴旋转45度,并且绕X轴旋转45度,现在从另一个角度去观察正方体,就能看出它的真实面貌:
效果出来了,但是我还想旋转一下这个正方体,看看其他的面,怎么办呢
so easy!绕y轴旋转咯
说干就干,修改代码:
#import "TestViewController.h"
@interface TestViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;
@end
@implementation TestViewController{
CATransform3D rotateTransform;
}
- (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.height / 2.0);
// apply the transform
face.layer.transform = transform;
}
- (void)viewDidLoad{
[super viewDidLoad];
//set up the container sublayer transform
rotateTransform = CATransform3DIdentity;
rotateTransform.m34 = -1.0f / 500.0f;
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 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];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[NSTimer scheduledTimerWithTimeInterval:0.01f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}
-(void)timerAction{
static CGFloat angle = 1.0f;
rotateTransform = CATransform3DRotate(rotateTransform, angle/180.0f*M_PI, 0.0f, 1.0f, 0.0f);
self.containerView.layer.transform = rotateTransform;
}
@end
跑起来:
纳尼,这是什么鬼👻
这是由于尽管Core Animation图层存在于3D空间之内,但它们并不都存在同一个3D空间。每个图层的3D场景其实是扁平化的,当你从正面观察一个图层,看到的实际上由子图层创建的想象出来的3D场景,但当你倾斜这个图层,你会发现实际上这个3D场景仅仅是被绘制在图层的表面。
总之一句话,图层是扁平的,直接把superLayer绕y轴旋转的方法行不通
要想实现旋转正方体的效果,就得将所有的子图层全部挨个儿做变换
到最后还得用sublayerTransform
继续修改代码:
#import "TestViewController.h"
@interface TestViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;
@end
@implementation TestViewController{
// CATransform3D rotateTransform;
}
- (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.height / 2.0);
// apply the transform
face.layer.transform = transform;
}
- (void)viewDidLoad{
[super viewDidLoad];
//set up the container sublayer transform
// rotateTransform = CATransform3DIdentity;
// rotateTransform.m34 = -1.0f / 500.0f;
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 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];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[NSTimer scheduledTimerWithTimeInterval:0.01f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}
-(void)timerAction{
static CGFloat angle = 1.0f;
CATransform3D transform3d = self.containerView.layer.sublayerTransform;
transform3d = CATransform3DRotate(transform3d, angle/180.0f*M_PI, 0.0f, 1.0f, 0.0f);
self.containerView.layer.sublayerTransform = transform3d;
// rotateTransform = CATransform3DRotate(rotateTransform, angle/180.0f*M_PI, 0.0f, 1.0f, 0.0f);
// self.containerView.layer.transform = rotateTransform;
}
@end
最终运行效果: