iOS Core Animation - The Layer Beneath
前言
iOS Core Animation 读书笔记(一)The Layer Beneath
笔记
The Layer Tree
- Layer Kit: 高效的,硬件加速的混合框架, 用来替代以Quartz为基础的,相对低效的AppKit框架。它也就是Core Animation的基础。
- Core Animation 不仅仅只和动画有关,它是iOS的核心组件之一,你在屏幕上看到的所有东西都由它来power。
- Core Animation的主要工作是将各个可视化的layer显示在屏幕上面。也就是将layer tree显示出来。
- UIView handles touch events and supports Core Graphics-based drawing, affine transform, and simple animations such as sliding and fading.
- 但是UIView并不是亲自处理这些任务中的大部分。渲染,布局,动画…这些任务其实都是由Core Animation里面的CALayer来完成的。
- CALayer不负责用户交互(user interaction),和responder chain没有联系。
- 每一个UIView都有一个layer属性,也就是一个CALayer。实际上,UIView并没有做什么事情,关于在屏幕上的显示和动画基本上都是由CALayer来完成。UIView可以说只是一个CALayer的wrapper,负责一些手势处理,以及一些高层面的对于Core Animation的一些API的封装。
- 总结来说:UIView主要负责处理事件以及用户交互,而CALayer主要负责显示和动画。
The Backing Image
The Contents Image
CALayer可以包含一个图片;图片里面,你想显示啥就显示啥。这就是CALayer的backing image。
CALayer有一个contents属性,给这个contents属性赋值入锅不是CGImage的话,这个Layer还是什么都没有显示。要这般赋值:
1
layer.contents = (__bridge id)image.CGImage;
因为,要真正赋值给contents属性的应该是一个CGImageRef,它是一个指针,指向CGImage的结构体;而UIImage的CGImage属性就是一个CGImageRef,但由于不是一个OC对象,无法赋值给id,所以需要使用(__bridge id)来转换。
CALyer的contentsGravity属性类似于UIView的contentMode。
CALayer的contentsScale属性与Retina有关。(有点不懂)
contentsRect属性将backing image限制在当前CALayer矩形的一个子框框里面。它不是按点来计算的;按照统一的标尺,这个Rect的长和宽最小为0,最大为1。
iOS中用到3中坐标类型:
Points:最常用的坐标类型,point表示逻辑像素。在标准清晰度的设备中一个point代表一个pixel;在Retina设备中,一个point代表2x2个pixel。
Pixels: 不常用,一般和使用pixel作为度量的CGImage等一同使用。
Unit:就是contentsRect属性用到的坐标系。
example: todo 插入例子
contentsCenter:该属性是一个CGRect,它定义了一个可伸缩的区域。具体的见下图:(有点不清楚)
example: todo 插入图片
其中四个黄色区域组合起来是原图。黄色区域内部四个点其实原来是一个点,也就是原图的中心点。
Custom Drawing
除了将一个CGImage指派给一个CALayer的contents以外,还可以直接在CALayer的backing image上面绘图。先来看看整个CALayer生成backing image的过程。
有一个CALayerDelegate的协议,当CALayer需要对它的content进行填充时,会去请求该协议的方法。
CALayer先会请求下列方法:
1
- (void)displayLayer:(CALayer *)layer;
在这个方法里面可以直接设置CALayer的contents属性。如果实现了这个方法,那么CALayer不会再去请求其他的方法来填充它的内容。
如果上一步未实现该方法,CALayer会去请求下面的方法:
1
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
在请求这个方法之前,CALayer会创建一个空白的backing image,和一个匹配该backing image的context。然后调用这个方法进行绘图。
只有在调用了
[layer display];
方法之后,上面说的这些delegate才会被调用。
所以总结一下这个过程,首先可以直接对CALayer的contents进行设置;然后如果发现调用了[layer display];
方法,会调用- (void)displayLayer:(CALayer *)layer;
方法,在这个方法里面可以直接设置contents,如果没有设置就沿用之前的contents;但是如果这个方法没有实现的话,display方法会创建一个空白的backing image来取代之前设置的contents,然后再调用- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
方法。在这个方法里面进行绘图。
PS:CALayer的backgroundColor和contents没有一点关系,该怎么设置就怎么设置。
再PS:我们一般是不需要去调用或者实现上面说的所有方法的,如果我们通过直接设置UIView或者是CALayer的属性无法达到我们的目的的时候,我们需要做的是重写UIView的-drawRect:
方法。这个方法会帮我们在暗中处理上面说到的各种方法。
再再PS:如果不需要调用-drawRect:
方法,那么久不要去重写它;因为即使是一个空的-drawRect:
方法,程序也会花内存和CPU时间去创建一个新的,空的backing image。
Layer Geometry
Layout
- 对于设置UIView的frame,bounds以及center,其实就是调用对应CALayer的setter和getter函数,对CALayer的相对应的属性进行设置。
- CALayer对应的属性是:frame, bounds, positions 和 anchorPoint。其中frame和bounds好理解。position的值是父layer的坐标系中的,它表示layer的旋转中心。anchorPoint的值表示position的那个点在自己layer的bounds上的比例。比如position是中点,那么anchorPoint就是(0.5, 0.5);如果position在左上角,那么anchorPoint就是(0, 0)。
- 在iOS中,坐标系原点在左上角;在Mac OS中,坐标系原点在左下角。通过设置CALayer的geometryFlipped属性,可以改变坐标系从左上变到左下。只需将geometryFlipped设置为YES。
- UIView是严格的二维平面;但是,CALayer存在三维空间。CALayer有zPosition和anchorPointZ属性。除了用在CATransform3D外,比较有用的就是改变layer的现实顺序,zPosition越大,越靠前显示。
Hit Testing
- CALayer不知道responder chain的存在,不能直接处理用户的交互动作。只有几个函数和用户交互有关。比如
-hitTest:
。 -hitTest:
返回CGPoint的点在哪个CALayer。
Automatic Layout
- 你可以通过UIVIew的UIViewAutoresizingMask和NSLayoutConstraint相关的API来使用autolayout。但是,如果你想直接通过操作CALayer来使用autolayout,最简单的方法就是使用CALayerDelegate的
- (void)layoutSublayersOfLayer:(CALayer *)layer;
方法。但是它不好用,所以最好是直接操作UIView… - 上面是一个佐证:你最好使用带layer的view来组织你的界面,而不是使用单独的layer。
Visual Effects
- cornerRadius默认的只会影响background color,不对backing image产生影响。但是加上masksToBounds之后,layer就确保了又圆角的属性。
- CALayer的border是被花在CALayer的内部的,在任何的sublayer的上面。
- CALayer的shadow不同于border的是:它是基于backing image的边缘的,而不是bounds的边缘。有点厉害
- CALayer的shadow通常是画在bounds的外部的,所以会被masksToBounds给切掉。。。
- CALayer的shadowPath属性可以强制指定一个shadow的形状。该属性的值是一个CGPathRef。可以使用UIBezierPath。
- CALayer的mask属性也是一个CALayer。但是它的作用是定义这个父layer的可见的部分。这个mask layer最重要的就是它的轮廓(和上面的shadow的边缘有点类似),它的父layer只会显示轮廓内的部分。
- CALayer的minificationFilter和magnificationFilter属性用于指定图片放大缩小时图片的显示方式。专业一点说就是生成图片像素的算法。他们有3个选项:kCAFilterLinear,kCAFilterNearest和kCAFilterTrilinerar。其中,nearest适合用于小图片以及图片颜色变化很尖锐的;而其他两个用于图片颜色变化比较缓和的,也就是均匀的,不会两个相邻像素之间颜色发生突变。
Transforms
Affine Transforms
利用CFAffineTransform的时候,其实可以计算原来view的四个点对应的变化之后的四个点,来确定变化之后的view的形状以及位置。
UIView的transform属性是一个CGAffineTransform,CALayer的transform属性是一个CATransform3D。CALayer中和UIView的transform等同的一个属性叫做affineTransform。其实对UIView的transform属性进行操作,实际上也是对CALayer进行操作,它只是一个向上的包装而已。
通过如下代码:
1
2
3
4CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformScale(transform, 0.5, 0.5);
transform = CGAffineTransformRotate(transform, M_PI / 180 * 30);
transform = CGAffineTransformTranslate(transform, 200, 0);记住,这里最后一步向右平移200点,其实并没有平移200点。因为,这样连续的变化,向右平移200点的transform受到了之前两步transform的影响。它其实被scale了一半,然后被旋转了30度。可以想象成向右200的向量吧(不确定)。
还有一种变换无法用translate, rotate和scale来表示。那就是:原View是矩形,变换之后了View变成了平行四边形。有点类似投影。那样的话就得直接修改CGAffineTransform矩阵中的值了。如下:
1
2
3CGAffineTransform transform = CGAffineTransformIdentity;
transform.c = -x;
transform.b = y;
3D Transforms
- Core Graphics framework是一个2维的画图的库。而CALayer的transform属性是一个CATransform3D,3D的。所以需要用到Core Animation framework。
- 控制透视效果,也就是远处的view看上去比近处的小。这个效果可以通过控制CATransform3D的m34值来实现。m34用来表示X,Y轴的放大系数与view距离camera的类似比值的一个东西。m34默认为0,将它定义成 -1.0/d,d表示view到镜头的距离,取500-1000之间的值都可以。d取值越小,透视的效果越明显。todo 来两张图
- 3D最远端的消失点定义在layer的anchorPoint。
- CALayer的sublayerTransform属性可以为sublayer统一指定他们的perspective和vanishing point。
- 沿Y轴旋转180度,就把layer翻转过来了,呈现镜像。但是如果将CALayer的doubleSided设置成NO,那么反面就不会被draw出来。
- 对于外部的layer外转,内部的layer内转,得分情况来讨论。如果是按照Z轴来旋转,那么内部的layer旋转被抵消,正面显示。但是如果按照Y轴旋转,虽然内部的旋转也被抵消,但是显示在屏幕上的其实是内部layer在外部layer上的投影;此时外部layer已经倾斜了,虽然内部layer很正,但是显示投影上去还是倾斜的。todo 来张图
- CATransform3D 先位移再旋转和先旋转再位移是不一样的。要始终找到旋转以及平移的中心点,以该中心点建立三维坐标系。当旋转或平移时,该坐标系也会跟着旋转和平移,那么下一步的选装或平移要以当前最新的坐标系为标准。
- UIView的touch event接收顺序与zPosition无关,还是与添加UIView的顺序相关。
Specialized Layers
CAShapeLayer
CAShapeLayer使用矢量画图,而不是位图(bitmap image)。
相比与普通的CALayer,它更快:硬件加速;内存更高效:不需要创建backing image;不会被边界限制(backing image的context);放大缩小时不是像素化的:矢量图。
创建CAShapeLayer的CGPath时只能创建一次,不能分开多次创建。也就是说CGPath的路径,颜色,线段粗细等属性只能在创建时一次性全部设置。如果想画多个不同类型(比如:不同颜色,不同粗细等)的shape,那么就得创建多个layer。
CAShapeLayer的path属性是一个CGPath,但是使用UIBezierPath,可以简化我们的使用,不用手动去释放CGPath。
创建一个只有3个角的bezierPath:
1
2
3
4CGRect rect = CGRectMake(50, 50, 100, 100); CGSize radii = CGSizeMake(20, 20); UIRectCorner corners = UIRectCornerTopRight |
UIRectCornerBottomRight | UIRectCornerBottomLeft;
//create path
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii];可以把这个CGPath构成的CAShapeLayer当做某个CALayer的mask。
CATextLayer
CATextLayer提供了UILabel的绝大部分功能,以及一些自己的功能,并且比UILabel渲染的更快。
CATextLayer要注意提供contentsScale属性,以保证在各种屏幕上显示正确。
它的font属性是一个CFTypeRef,所以可以给他CTFontRef或者是CGFontRef;而且这两个和UIFont一样不会压缩point size,也就是字体大小;所以还需要自己指定fontSize属性。
CATextLayer在iOS3.2的时候就已经支持attributed string了,比UILabel等UIKit元素更早。
但是它的行间距和字间距比UILabel的表现要差。
CALayer不太支持autolayout和autoresizing…
如何让一个UIView的root layer不是普通的CALayer呢?UIView通过
+ (Class)layerClass;
方法来返回root layer的类型;所以只需要在我们自己的View类里面重写该方法就可以。1
2
3
4@implementation LayerLabel // 自定义的UIView
+ (Class)layerClass { // override layerClass of UIView
return [CATextLayer class];
}那么这时就不会调用
-drawRect:
方法来绘图,而是使用CATextLayer自己来渲染文字。将CATextLayer作为UILabel的root layer还有个好处,就是不需要设置contentsScale。因为这个被UIView自己设置了。这也佐证了使用带layer的View比单独使用layer要方便。
CATransformLayer
它的存在解决了之前说的外部layer外转内部layer内转,但是内部layer显示奇怪的问题。这个问题的根本原因就是所有sublayer都被containerLayer给扁平化得显示在它自己的空间内,有点剥夺3d效果的感觉。
CATransformLayer不能用于显示任何内容,它必须包含一些sublayer。它有可以应用于sublayer的transform,可以使sublayer立体化起来。
这个例子很赞:
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (CALayer *)faceWithTransform:(CATransform3D)transform {
//create cube face layer
CALayer *face = [CALayer layer];
face.frame = CGRectMake(-50, -50, 100, 100);
//apply a random color
CGFloat red = (rand() / (double)INT_MAX);
CGFloat green = (rand() / (double)INT_MAX);
CGFloat blue = (rand() / (double)INT_MAX);
face.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
//apply the transform and return
face.transform = transform;
return face;
}
- (CALayer *)cubeWithTransform:(CATransform3D)transform {
//create cube layer
CATransformLayer *cube = [CATransformLayer layer];
//add cube face 1
CATransform3D ct = CATransform3DMakeTranslation(0, 0, 50);
[cube addSublayer:[self faceWithTransform:ct]];
//add cube face 2
ct = CATransform3DMakeTranslation(50, 0, 0);
ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);
[cube addSublayer:[self faceWithTransform:ct]];
//add cube face 3
ct = CATransform3DMakeTranslation(0, -50, 0);
ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);
[cube addSublayer:[self faceWithTransform:ct]];
//add cube face 4
ct = CATransform3DMakeTranslation(0, 50, 0);
ct = CATransform3DRotate(ct, -M_PI_2, 1, 0, 0);
[cube addSublayer:[self faceWithTransform:ct]];
//add cube face 5
ct = CATransform3DMakeTranslation(-50, 0, 0);
ct = CATransform3DRotate(ct, -M_PI_2, 0, 1, 0);
[cube addSublayer:[self faceWithTransform:ct]];
//add cube face 6
ct = CATransform3DMakeTranslation(0, 0, -50);
ct = CATransform3DRotate(ct, M_PI, 0, 1, 0);
[cube addSublayer:[self faceWithTransform:ct]];
//center the cube layer within the container
CGSize containerSize = self.containerView.bounds.size;
cube.position = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
//apply the transform and return
cube.transform = transform;
return cube;
}
- (void)viewDidLoad {
[super viewDidLoad];
//set up the perspective transform
CATransform3D pt = CATransform3DIdentity;
pt.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = pt;
//set up the transform for cube 1 and add it
CATransform3D c1t = CATransform3DIdentity;
c1t = CATransform3DTranslate(c1t, -100, 0, 0);
CALayer *cube1 = [self cubeWithTransform:c1t];
[self.containerView.layer addSublayer:cube1];
//set up the transform for cube 2 and add it
CATransform3D c2t = CATransform3DIdentity;
c2t = CATransform3DTranslate(c2t, 100, 0, 0);
c2t = CATransform3DRotate(c2t, -M_PI_4, 1, 0, 0);
c2t = CATransform3DRotate(c2t, -M_PI_4, 0, 1, 0);
CALayer *cube2 = [self cubeWithTransform:c2t];
[self.containerView.layer addSublayer:cube2];
}
@endtodo add picture
CAGradientLayer
- 测试了重写View的layerClass方法之后发现,对于CATextLayer,不会调用
-drawRect;
方法,但是对于其他种类的layer,还是会调用-drawRect;
方法。 - 硬件加速。
- colors属性是一个数组。里面装的是CGColorRef,不是UIColor。所以还是需要使用
(__bridge id)
。startPoint和endPoint指定渐变方向,这两个属性使用的是unit coordinates。 - colors属性可以添加任意个颜色,默认的他们是均匀分布的;但是我们可以通过locations属性改变他们的分布。locations属性是一个装着NSNumber(float value)的数组,从0-1,表示各种color的终区段。
CAReplicatorLayer
它是用来高效的生成一系列相似的layer。
instanceCount属性指定copy的个数,instanceTransform属性指定变化,各个变化是叠加的。
说不清楚,直接上代码…
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@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//create a replicator layer and add it to our view
CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
replicator.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:replicator];
//configure the replicator
replicator.instanceCount = 10;
//apply a transform for each instance
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DTranslate(transform, 0, 200, 0);
transform = CATransform3DRotate(transform, M_PI / 5.0, 0, 0, 1);
transform = CATransform3DTranslate(transform, 0, -200, 0);
replicator.instanceTransform = transform;
//apply a color shift for each instance
replicator.instanceBlueOffset = -0.1;
replicator.instanceGreenOffset = -0.1;
//create a sublayer and place it inside the replicator
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(100.0f, 100.0f, 100.0f, 100.0f);
layer.backgroundColor = [UIColor whiteColor].CGColor;
[replicator addSublayer:layer];
}
@endtodo add picture
CAScrollLayer
- 一个可以scroll的layer,有一个方法
-scrollToPoint:
。因为CALayer无法处理用户手势操作,所以这个方法也是这个layer所能做的所有事了。。。可以从UIView的层面获取手势所处的point,再将该point传递给CAScrollLayer。 - 该layer滑动的时候,改变的是layer的bounds。
OtherLayer####
- Core Animation使用CPU来处理图片,而不是更快的GPU。