Core Animation 三 : CALayer的仿射变换,
什么是仿射变换
CALayer的变换和Core Graphics变换没什么区别,本篇主要讲一些关于3D变换的内容.
3D变换的平移和缩放与平面的变换完全一样,只是增加一个坐标轴的参数,但是3D的旋转就不一样了,重点来看这个
CAGradientLayer *gl = [CAGradientLayer layer];
gl.startPoint = CGPointMake(0, 0);
gl.endPoint = CGPointMake(1, 1);
gl.colors = @[(__bridge id)UIColor.redColor.CGColor,(__bridge id)UIColor.orangeColor.CGColor,(__bridge id)UIColor.yellowColor.CGColor];
gl.locations = @[@0.2,@.6,@1.0];
gl.type = kCAGradientLayerAxial;
gl.frame = (CGRect){75,100,ScreenWidth-150, ScreenWidth-150};
[self.view.layer addSublayer:gl];
初始状态
3D视角下Z轴是垂直于屏幕的,不管是2d变换还是3d变换,其实z轴是一直存在的,
想象一下,2d变换的旋转,就是Z轴位中心进行旋转,并且是以自身的中心为旋转中心,即layer的中心是三维坐标轴的(0,0,0),与anchorPoint锚点无关,不管锚点在哪,原点都在layer的中心.
也就是说,可以认为是围绕一个(0,0,1)的单位向量旋转的,当然也可以是(0,0,-1)
gl.affineTransform = CGAffineTransformRotate(gl.affineTransform, M_PI_4);
2d旋转
所以CGAffineTransformRotate这个方法是以z轴为中心旋转,如果不是的话,就需要增加参数了
/* Rotate 't' by 'angle' radians about the vector '(x, y, z)' and return
* the result. If the vector has zero length the behavior is undefined:
* t' = rotation(angle, x, y, z) * t.
CA_EXTERN CATransform3D CATransform3DRotate (CATransform3D t, CGFloat angle,
CGFloat x, CGFloat y, CGFloat z)
API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
*/
gl.transform = CATransform3DRotate(gl.transform, -M_PI_4, 1, 0, 0);
gl.contents = (id)[UIImage imageNamed:@"avatar"].CGImage;
所以CATransform3DRotate除了角度之外还有3个参数,并且不能都为0,因为需要一个向量作为轴,上面这个就是以(1,0,0)为轴,实质就是x轴.
image.png
为了看起来方便,加了个contents,结果看起来只是变的扁了,这是因为变换本身并没有透视效果,没有近大远小,当围绕x轴旋转时,远离我们的一端应该看起来更小,靠近的一端应该看起来更大;
struct CATransform3D
{
CGFloat m11, m12, m13, m14;
CGFloat m21, m22, m23, m24;
CGFloat m31, m32, m33, m34;
CGFloat m41, m42, m43, m44;
};
/* The identity transform: [1 0 0 0; 0 1 0 0; 0 0 1 0; 0 0 0 1]. */
这是CATransform3D的结构和单位向量;再提一下,这个和前面的什么是仿射变换,里面略有区别,里面的齐次坐标把点定义为单列矩阵(或者说是列向量),而Apple把点定义为单行矩阵(行向量);
想象一下带有透视的旋转,它的投影应该是一个梯形,也就是每个点的x坐标是要发生变化的,这里直接说结论,最简单的方法是在旋转变换前,先修改m34的值
CATransform3D t = gl.transform;
t.m34 = .0015f;
t = CATransform3DRotate(t, -M_PI_4, 1, 0, 0);
//t = (CATransform3D){t.m11,t.m12,t.m13,t.m14,t.m21,t.m22,t.m23,-0.001,t.m31,t.m32,t.m33,0.001,t.m41,t.m42,t.m43,t.m44};
gl.transform = t;
设置了m34之后再旋转,现在就有了透视效果
修改m34之后再旋转
可以打印出变换矩阵看一看,和单位矩阵对比,发现有哪些元素发生了变化
没有经过变换的单位矩阵
旋转矩阵
修改m34之后再旋转的矩阵
但是这里m34的值还是有问题,后面说明
那么为什么m34和m24会影响到变换的结果,通过矩阵乘法能看出来明明xyz的值与最右边一列没有关系
这又得重新说到齐次坐标,帮助理解齐次坐标
在二维变换中,齐次坐标右下角的元素经常默认是1,而点的矩阵是(x,y,1),但是实际上它可以是任意值w,而点的矩阵变成了(x/w,y/w,1);这么一来,w直接影响到整个视图的比例,如果设置m44是2,则相当于做了一次缩小,宽高都变成原来的1/2,在iOS中,平面的仿射变换CGAffineTransform定义为6个值,也就是忽略了齐次坐标增加的一列,这一列在iOS中默认为(0,0,1),但是CATransform3D是16个值,m44是可以不为1的;
还没完,除了m44可以不是1之外,x和y也不是看起来的x和y,因为在3d变换中,最终的显示效果都是光栅化的结果,也就是在xy平面的投影,当m34发生变化时,变换后的m44就不是原来的m44了,所以变换后的w也不再是原来的w(如果一开始m44是1的话,w也是1),x和y也就发生了变化.
修改m34 -> 点(x,y,1)变换 -> m44变成w -> m44改成1 -> 变换后的点(x/w,y/w,1)
t = CATransform3DRotate(t, -M_PI_4, 0, 1, 0);
根据上面的理解,所以即便改成y轴旋转也是修改m34,绕z轴旋转就是平面变换了,m34不会影响结果
绕y轴旋转
t = CATransform3DRotate(t, -M_PI_4, 1, 0, 0);
t = CATransform3DRotate(t, -M_PI_4, 0, 1, 0);
x轴旋转之后再y轴旋转
image.png
-
关于灭点
灭点就是透视效果的视野消失点,在iOS中,它就是layer的锚点anchorPoint
image.png
所以修改锚点会对透视效果产生影响,想要平移图形应该使用变换而不是修改锚点和position
下面两张图,再没有变换的时候,都是居中的,同样的变换,因为anchorPoint不同效果也不同
anchorPoint是(.5,.5)
anchorPoint是(0,0)
-
sublayerTransform
这个属性是将子视图的变换同步起来,它通常是使用在统一子layer的灭点
例如,一个大layer A上有一个小的layer a,A的锚点是(0,0),a的锚点是自己的中心
当a设置了m34然后做3d变换之后是这样的,图1
图1
如果设置了A的sublayerTransform,然后a不再设置m34,直接做变换,是这样的,图2
CATransform3D t = CATransform3DIdentity;
A.m34 = .0015f;
A.sublayerTransform = t;
图2
这样,子layer的灭点就统一成了父layer的灭点,如果有很多个子layer的话,所有的子layer都统一了透视.
此时打印a的anchorPoint,发现还是0.500000,0.500000,完全没变,也就是说,设置了sublayerTransform之后,子layer的anchorPoint不再影响灭点,就可以随意的使用anchorPoint和position或者frame来布局.
这个属性非常强大,后面的例子会继续说明.
-
layer的背面
如果一个layer做3d变换,如果转到背面去了,会发生翻转
image.png
如果设置doubleSided = NO;就什么都看不到了,甚至gpu都不会去执行绘制.
下面通过一个demo来说明上面的内容
- 目标是绘制一个正方体,每个面都区分开来,实现透视效果,滑动屏幕可以使正方体转动
想象一下,用6个视图来构建正方体,想要正方体转动,每个面都有不同的变换,及其繁琐,这里就需要sublayerTransform出场了,6个面处在同一视觉系统内,有同一个灭点,正方体的立体效果就实现了.
同时,当进行变换的时候,修改sublayerTransform,六个面都会发生变化;
对于这个正方体,如果想在它转动的时候不会看起来一会儿远一会儿近,就应该把正方体的中心放在坐标原点(0,0,0)
正方体六个面大小都是(200,200),它的大小本身不会变化,但是当设置了m34之后,z值就会影响它在视觉上的大小.
首先第一个面,离我们最近,在z轴正方向设置为100;第六个面离我们最远,z是-100,看看效果
CATransform3D subt = CATransform3DIdentity;
subt.m34 = .0015f;
self.bottomLayer.sublayerTransform = subt;
CATransform3D t6 = CATransform3DMakeTranslation(0, 0, -100);
CATransform3D t1 = CATransform3DMakeTranslation(0, 0, 100);
image.png
结果发现反而1小,6大,这里就是之前说的,想要达到合适的透视效果,m34其实应该是负值
m34改成负值
这样就对了,不过还有一个问题,第六个面并不是简单的往后移动100单位就行了,它其实应该是翻转180度,假如layer1有不透明度,我们看到的应该是layer6的背面,再假如如果layer6把doubleSided设置为NO,那我们应该看不到layer6.
t6 = CATransform3DRotate(t6, M_PI, 0, 1, 0);
所以这里应该翻转layer6
翻转
layer6关闭doubleSided就看不到了
说明了这些问题之后,直接上代码就可以了
@property (nonatomic, strong) CALayer *bottomLayer;
@property (nonatomic, strong) CALayer *layer1;
@property (nonatomic, strong) CALayer *layer2;
@property (nonatomic, strong) CALayer *layer3;
@property (nonatomic, strong) CALayer *layer4;
@property (nonatomic, strong) CALayer *layer5;
@property (nonatomic, strong) CALayer *layer6;
@property (nonatomic, assign) CGPoint touchPoint;
- (void)viewDidLoad {
[super viewDidLoad];
self.bottomLayer = [CALayer layer];
self.bottomLayer.frame = CGRectMake(0, 100, ScreenWidth, ScreenWidth);
self.bottomLayer.backgroundColor = [[UIColor hex:@"cccccc"] colorWithAlphaComponent:.7].CGColor;
[self.view.layer addSublayer:self.bottomLayer];
CATransform3D subt = CATransform3DIdentity;
subt.m34 = -.0015f;
self.bottomLayer.sublayerTransform = subt;
CATransform3D t6 = CATransform3DMakeTranslation(0, 0, -100);
t6 = CATransform3DRotate(t6, M_PI, 0, 1, 0);
self.layer6 = [self createLayer:6 color:[UIColor hex:@"A0522D"] transform:t6];
CATransform3D t5 = CATransform3DMakeTranslation(-100, 0, 0);
t5 = CATransform3DRotate(t5, -M_PI_2, 0, 1, 0);
self.layer5 = [self createLayer:5 color:[UIColor hex:@"DAA520"] transform:t5];
CATransform3D t4 = CATransform3DMakeTranslation(0, 100, 0);
t4 = CATransform3DRotate(t4, -M_PI_2, 1, 0, 0);
self.layer4 = [self createLayer:4 color:[UIColor hex:@"228B22"] transform:t4];
CATransform3D t3 = CATransform3DMakeTranslation(0, -100, 0);
t3 = CATransform3DRotate(t3, M_PI_2, 1, 0, 0);
self.layer3 = [self createLayer:3 color:[UIColor hex:@"5F9EA0"] transform:t3];
CATransform3D t2 = CATransform3DMakeTranslation(100, 0, 0);
t2 = CATransform3DRotate(t2, M_PI_2, 0, 1, 0);
self.layer2 = [self createLayer:2 color:[UIColor hex:@"4682B4"] transform:t2];
CATransform3D t1 = CATransform3DMakeTranslation(0, 0, 100);
self.layer1 = [self createLayer:1 color:[UIColor hex:@"708090"] transform:t1];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.touchPoint = CGPointZero;
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.view];
if(CGPointEqualToPoint(self.touchPoint, CGPointZero)){
self.touchPoint = currentPoint;
}
CGFloat deltax = currentPoint.x - self.touchPoint.x;
CGFloat deltay = currentPoint.y - self.touchPoint.y;
CGFloat delta = sqrt(pow((self.touchPoint.y - currentPoint.y), 2) + pow((self.touchPoint.x - currentPoint.x), 2));
self.touchPoint = currentPoint;
CATransform3D subt = self.bottomLayer.sublayerTransform;
subt = CATransform3DRotate(subt, M_PI/360.0*delta, -deltay, deltax, 0);
self.bottomLayer.sublayerTransform = subt;
}
- (CALayer *)createLayer:(NSInteger)index color:(UIColor *)color transform:(CATransform3D)transform{
CALayer *layer = [CALayer layer];
CGFloat wh = 200;
CGFloat xy = (ScreenWidth - 200)/2;
CGRect frame = CGRectMake(xy, xy, wh, wh);
layer.frame = frame;
layer.backgroundColor = [color colorWithAlphaComponent:.3].CGColor;
[self.bottomLayer addSublayer:layer];
// layer.doubleSided = NO;
layer.transform = transform;
CATextLayer *tl = [CATextLayer layer];
tl.contentsScale = UIScreen.mainScreen.scale;
tl.alignmentMode = kCAAlignmentCenter;
tl.doubleSided = NO;
UIFont *f = [UIFont systemFontOfSize:100 weight:UIFontWeightBold];
NSMutableAttributedString *att = [[NSMutableAttributedString alloc]initWithString:[NSString stringWithFormat:@"%ld",index]];
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
[att addAttributes:@{NSFontAttributeName:f,NSForegroundColorAttributeName:UIColor.whiteColor,NSParagraphStyleAttributeName:paragraph} range:NSMakeRange(0, att.length)];
tl.string = att;
tl.position = CGPointMake(wh/2, wh/2);
tl.bounds = CGRectMake(0, 0, f.lineHeight, f.lineHeight);
[layer addSublayer:tl];
return layer;
}
解释下touchesMoved里的内容
手指滑动时,bottomLayer围绕currentPoint和self.touchPoint组成的向量转动;delta是手指滑动的距离
6个面通过自身的变换,在空间上形成正方体,正方体的转动通过bottomLayer的sublayerTransform来实现.
最终效果如下