《iOS 三问》 -- CALayer 常用几何变换 transform

CALayer 知识点脑图

和 UIView 一样,CALayer 也可以设置 Transform(那当然,因为 uiview 的绘图就是靠 layer 来实现的),而且 layer 的 transform 比 view 来说要强大太多了。

1 仿射变换

在介绍 UIView 的几何变形时,我们提到了通过赋值 view 的 transform 属性,可以设置 view 的几何变换。我们之所以称之为 “几何” 变形,就是因为对图象的变型操作实际上是利用了几何的知识。

比如我们常用的,CGAffineTransform 中的 affine,就是 仿射变换,用于在二维空间对图象进行几何变换(缩放、平移、旋转等)。他是利用将图象和一个变换矩阵相乘以得出的结果矩阵来实现变换的。

在应用的时候,我们无需理解其中复杂的几何计算,因为 CALayer 提供了简单的 api 供我们使用,我们只需要改变 layer 的affineTransform属性。

常用的仿射变换方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
CGAffineTransformIdentity :【常量】什么都不做的变换(初始变换)

・旋转变换: CGAffineTransformMakeRotation (CGFloat angle)

・缩放变换: CGAffineTransformMakeScale (CGFloat sx, CGFloat sy)

・平移变换: CGAffineTransformMakeTranslation (CGFloat tx, CGFloat ty)

・复合变换
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)

CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);

下面有一个输出一个指南针图层的实例,利用变形知识画一个指南针:
实现效果可以在我的 iOSOneDemo 中对应这篇文章的章节查看。

指南针图层仿射变换示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

- (CALayer *)compassLayer
{
CALayer *outerLayer = [CALayer new];
outerLayer.frame = CGRectMake(0, 0, 300, 300);

// 表底盘 (渐变 CAGradientLayer)
CAGradientLayer *outletLayer = [CAGradientLayer new];
outletLayer.contentsScale = [UIScreen mainScreen].scale;
outletLayer.frame = outerLayer.bounds;
outletLayer.colors = @[(id)[UIColor greenColor].CGColor, (id)[UIColor redColor].CGColor];
outletLayer.locations = @[@0.0f, @1.0f];
[outerLayer addSublayer:outletLayer];

// 表面 (CAShapeLayer)
CAShapeLayer *surfaceLayer = [CAShapeLayer new];
surfaceLayer.bounds = outerLayer.bounds;
surfaceLayer.position = CGPointMake(CGRectGetMidX(outerLayer.bounds),CGRectGetMidY(outerLayer.bounds));
surfaceLayer.contentsScale = [UIScreen mainScreen].scale;
surfaceLayer.lineWidth = 3.0;
surfaceLayer.fillColor = XXXX_COLOR_HEX(0xf5f5f5).CGColor;
surfaceLayer.strokeColor = [UIColor blueColor].CGColor;
CGMutablePathRef p = CGPathCreateMutable();
CGPathAddEllipseInRect(p, nil, CGRectInset(outerLayer.bounds, 3, 3));
surfaceLayer.path = p;
[outerLayer addSublayer:surfaceLayer];

// 四个方向的文字 (CATextLayer)
NSArray* pts = @[@"北", @"东", @"南", @"西"];
for (int i = 0; i < 4; i++) {
CATextLayer *textLayer = [CATextLayer new];
textLayer.contentsScale = [UIScreen mainScreen].scale;
textLayer.string = pts[i];
textLayer.bounds = CGRectMake(0,0,50,50);
textLayer.position = CGPointMake(CGRectGetMidX(surfaceLayer.bounds), CGRectGetMidY(surfaceLayer.bounds));
CGFloat vert = CGRectGetMidY(surfaceLayer.bounds) / CGRectGetHeight(textLayer.bounds);
textLayer.anchorPoint = CGPointMake(0.5, vert);
textLayer.alignmentMode = kCAAlignmentCenter;
textLayer.foregroundColor = [UIColor blackColor].CGColor;
textLayer.affineTransform = CGAffineTransformMakeRotation(i*M_PI/2.0);
[surfaceLayer addSublayer:textLayer];
}

// the arrow
CALayer *arrow = [CALayer new];
arrow.contentsScale = [UIScreen mainScreen].scale;
arrow.bounds = CGRectMake(0, 0, 10, 130);
arrow.backgroundColor = [UIColor blackColor].CGColor;
arrow.position = CGPointMake(CGRectGetMidX(outerLayer.bounds), CGRectGetMidY(outerLayer.bounds));
arrow.anchorPoint = CGPointMake(0.5, 1);
arrow.affineTransform = CGAffineTransformMakeRotation(M_PI/5.0);
[outerLayer addSublayer:arrow];

return outerLayer;
}

2 3D 变换

CG 的前缀告诉我们, CGAffineTransform 类型属于 Core Graphics 框架,Core Graphics 实际上是一个严格意义上的 2D 绘图 API,并且 CGAffineTransform 仅仅对 2D 变换有效。

CATransform3D 可以对图层在 3D 空间内进行移动或转动。和 CGAffineTransform 类似, CATransform3D 也是一个矩阵,但是和 3x3 的矩阵不同, CATransform3D 是一个可以在 3 维空间内做变换的 4x4 的矩阵

CATransform3D

2.1 3D 变换主要方法

3D 变换主要方法
1
2
3
4
5

CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)

oneDemo 中 3D 变换的示例

绕 x 轴旋转
1
2
//slider.value 为 0-100
_transormLayer.transform = CATransform3DMakeRotation(M_PI * 2 / 100.0 * slider.value, 1, 0, 0);

2.2 背面绘制

CALayer 有一个叫做doubleSided的属性来控制图层的背面是否要被绘制。是 BOOL 类型,默认为 YES ,如果设置为 NO ,那么当图层正面从相机视角消失的时候,它将不会被绘制。

2.3 透视投影

在真实世界中,当物体远离我们的时候,由于视角的原因看起来会变小,理论上说远离我们的视图的边要比靠近视角的边跟短,但实际上,如果你什么都不设置,这样的情况并没有发生。

  • m34

CATransform3D 的透视效果通过一个矩阵中一个很简单的元素来控制 – m34, 可以按比例缩放 X 和 Y 的值来计算到底要离视角多远。

透视投影
1
2
3
4
5
6

CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1 / 500.0;
transform = CATransform3DRotate(transform, M_PI * 2 / 100.0 * slider.value, 0, 0, 1);
_projectLayer.transform = transform;

2.4 灭点

当在透视角度绘图的时候,远离相机视角的物体将会变小变远,当远离到一个极限距离,它们可能就缩成了一个点,于是所有的物体最后都汇聚消失在同一个点。

在现实中,这个点通常是视图的中心,于是为了在应用中创建拟真效果的透视,这个点应该聚在屏幕中点,或者至少是包含所有 3D 对象的视图中点。

灭点

Core Animation 定义了这个点位于变换图层的 anchorPoint . 这就是说,当图层发生变换时,这个点永远位于图层变换之前 anchorPoint 的位置。

当改变一个图层的 position ,你也改变了它的灭点,做 3D 变换的时候要时刻记住这一点,当你视图通过调整 m34 来让它更加有 3D 效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置(而不是直接改变它的 position ),这样所有的 3D 图层都共享一个灭点。

3 sublayerTransform 属性

如果有多个视图或者图层,每个都做 3D 变换,那就需要分别设置相同的 m34 值,并且确保在变换之前都在屏幕中央共享同一个 position 。使用 CALayer 的 sublayerTransform 属性会让父层的改变影响到所有的子图层,这意味着你可以一次性对包含这些图层的容器做变换,也就是所有子图层都自动继承了父图层的变换方法。

比如,我们

sublayerTransform 属性
1
2
3
4
5
6
7
8
9
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = - 1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective; // 设置父层的 sublayerTransform!子图层自动继承父图层此变换方法,设置同一个来点

// 然后再分别转动 layer1 与 layer2
CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
self.layerView1.layer.transform = transform1;
CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
self.layerView2.layer.transform = transform2;

4 示例 - 做一个立方体骰子

实现效果可以在我的 iOSOneDemo 中对应这篇文章的章节查看。

一个立方体骰子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

layerFrame = [UIView new];
layerFrame.backgroundColor = [UIColor greenColor];
layerFrame.flexSize = CGSizeMake(300, 300);
_cube = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];
[layerFrame addSubview:_cube];
[demo3 flex_addSubview:layerFrame];

// 设置所有子层共同灭点
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
layerFrame.layer.sublayerTransform = perspective;

for (int i = 1; i <= 6; i++) {
UIView *face = self.face[i-1];
face.backgroundColor = XXXX_RANDOM_COLOR();
face.frame = CGRectMake(0, 0, 100, 100);
face.center = CGPointMake(150, 150);
[_cube addSubview:face];
XXXXLabel *faceLabel = [XXXXLabel labelWithType:XXXXLabelTypeDefault
text:[NSString stringWithFormat:@"%d", i]
font: [UIFont systemFontOfSize:30]
color:[UIColor blackColor]];
faceLabel.frame = CGRectMake(0, 0, 50, 50);
faceLabel.center = CGPointMake(50, 50);
faceLabel.textAlignment = NSTextAlignmentCenter;
faceLabel.verticalAlignment = XXXXLabelVerticalAlignmentMiddle;
[face addSubview:faceLabel];

CALayer *layer = face.layer;
CATransform3D transform = CATransform3DIdentity;

if (i == 1) {
transform = CATransform3DMakeTranslation(0, 0, 50);
}
else if (i == 2) {
transform = CATransform3DMakeTranslation(-50, 0, 0);
transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
}
else if (i == 3) {
transform = CATransform3DMakeTranslation(0, -50, 0);
transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
}
else if (i == 4) {
transform = CATransform3DMakeTranslation(50, 0, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
}
else if (i == 5) {
transform = CATransform3DMakeTranslation(0, 50, 0);
transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
}
else if (i == 6) {
transform = CATransform3DMakeTranslation(0, 0, -50);
}
layer.transform = transform;
}

// 根据 slider 的值旋转立方体
- (void)rotateCube
{
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;

CATransform3D x = CATransform3DRotate(perspective, M_PI * 2 / 100.0 * _cubeRotateX, 1, 0, 0);
CATransform3D y = CATransform3DRotate(perspective, M_PI * 2 / 100.0 * _cubeRotateY, 0, 1, 0);
CATransform3D z = CATransform3DRotate(perspective, M_PI * 2 / 100.0 * _cubeRotateZ, 0, 0, 1);
CATransform3D transform = CATransform3DConcat(x, y);
transform = CATransform3DConcat(transform, z);
_cube.layer.sublayerTransform = transform;
}

5 引用

【1】[Matt Neuburg - 《Programming iOS deep into views,view controllers and frameworks》]
【2】[Nick LockWood - 《iOS Core Animation: Advanced Techniques》]