Allen Chiang

iOS程序猿的blog

© 2014. All rights reserved.

iOS.Core.Animation笔记一.Layer

image

Nick Lockwood的《iOS Core Animation》全书分为3大部分

  1. The Layer Beneath
  2. Setting Things in Motion
  3. The Performance of a Lifetime

本文章篇幅限定对第一部分Layer的摘记,后续部分可能会继续整理。

Core Animation并不只是包含动画,其实动画只是他的一部分,更合适的名称应该是Layer Kit。


1.The Layer Tree

凡是开发过iOS或者Mac OS应用的同学都应该知道view的概念。view是一个矩形的用于显示内容(比如图像,文字,视频等)的对象。 在iOS中,所有的view都继承自UIView。UIView可以控制用户事件并且支持Core Graphic绘图,仿射变换(例如旋转和拉伸),以及简单的动画比如滑动和渐隐。 同时你可能已经发现了UIView并不是他自己处理了这一切,其实绘图,布局和动画都是由CALayer来控制的。

CALayer

CALayer和UIView非常类似,最主要去区别就是CALayer不处理用户交互行为,也就是说CALayer并不能感知responder chain的存在所以他不能响应用户交互的事件,但他也用于对交互行为触发位置的落点判断。

我们为什么要了解CALayer的存在,因为他有一些UIView不能做的事情:

  1. 阴影、圆角、多彩的border

  2. 3D变化和定位

  3. 非矩形的边界

  4. alpha遮罩

  5. 多步非线性动画


2. The Backing Image

contents

CALayer的contents属性的类型是id,主要是为了同时支持Mac OS和iOS,在Mac OS下contents可以被设置为NSImage,在iOS下则需要设置为CGImageRef。当然如果你设置其他类型的值,则CALayer会显示为空白。

contentsGravity

当图片大小不是适合当前view的时候,你可能会使用view.contentMode来设置缩放和位置,其实他是通过控制CALayer的contentGravity属性来实现的

kCAGravityCenter 			kCAGravityTop
kCAGravityBottom 			kCAGravityLeft
kCAGravityRight 			kCAGravityTopLeft
kCAGravityTopRight 			kCAGravityBottomLeft
kCAGravityBottomRight 		kCAGravityResize 
kCAGravityResizeAspect 		kCAGravityResizeAspectFill

例如我们可以使用类似于UIViewContentModeScaleAspectFit的kCAGravityResizeAspect来设置我们的图片拉伸以适应我们layer的bounds self.layerView.layer.contentsGravity = kCAGravityResizeAspect;

contentScale

contentScale用来定义像素尺寸到layer尺寸的拉伸比率,默认值是1。如果你设置了上面的contentsGravity属性,则contentScale很可能是没有效果的。 其实contentScale多用于设置高分辨率屏幕下的图片尺寸,如果=1则表示每个点代表一个像素,如果=2则表示每个点代表两个像素。因为layer的contents支持的CGImage并不知道当前分辨率的情况(UIImage支持),所以需要配合contentScale属性来支持合适的图片尺寸。以下两种方式设置contentScale都可以:

//1.set the contentsScale to match image
self.layerView.layer.contentsScale = image.scale;

//2.set the screen scale 
self.layerView.layer.contentsScale = [UIScreen mainScreen].scale; ### contentsRect 允许我们定义content image的一部分矩形区域用于当前layer的显示,默认值是{0,0,1,1}表示显示全部。 不同于bounds、frame通过point来度量大小,contentsRect使用的是Unit坐标。在iOS中常见3各种坐标度量:

举例如{0,0,0.5,0.5}表示只显示contents的左上角四分之一,{0.5,0,0.5,0.5}表示只显示contents的右上角的四分之一,其他同理可得。

contentsCenter

contentsCenter定义了layer内部一个可以被拉伸的区域,外部则是一个固定的边界。同时他使用的也是跟上面contentsRect一样的Unit坐标表示法。

image

如上图所示就比较清晰了,定义了contentsCenter={0.25,0.25,0.5,0.5},则表示中间绿色区域都是可以被拉伸的,蓝色区域可以被横向拉伸纵向则固定宽度,红色区域反之是纵向可以被拉伸横向宽度固定。

自定义绘图方法-drawRect: & -displayLayer:

你可能已经知道在UIView的子类中可以通过实现-drawRect:方法来自定义绘图。根据apple的建议如果你不需要自定义绘图请不要实现一个空的该方法,以浪费不必要的系统开销。因为UIView一旦发现有-drawRect:方法实现,系统就会自动为他创建一个像素背景尺寸乘以contentScale大小的图像。

CALayer的自定义绘图则通过layer的delegate对象(CALayerDelegate 协议)的以下两个方法来实现

// 方法1
- (void)displayLayer:(CALayerCALayer *)layer;

// 方法2. 如果方法1没有实现,则调方法2
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;

和UIView的-drawRect: 不同的两点需要注意


3. Layer Geometry

UIView有3个最主要的布局属性:frame、bounds和center,CALayer也有相对应的属性:frame、bounds和position。 frame其实是layer在父layer中的坐标,bounds是自己内部的坐标(左上角为{0,0}原点),frame反映了layer在父layer中轴向坐标下占用的矩形区域。下图显示了一个经过旋转以后的矩形view或者layer的frame、bounds以及center和position。

image

center和position都是anchorPoint(锚点)相对于父layer的坐标。默认情况下,anchorPoint总是位于矩形区域的中间点,在view中不出现anchorPoint,意味着不能移动anchorPoint,所以view中名称就是center。而layer中anchorPoint可以移动,比如你可以把anchorPoint移动到当前layer的左上角{0,0}(anchorPoint用的也是unit坐标,默认中间点{0.5,0.5}),这时由于position保持不变,layer会相对父layer往右下角移动

image

坐标系统

CALayer提供若干工具方法用于转换相对于不同layer的坐标

- (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer;
- (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer *)layer; 
- (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer *)layer;
- (CGRect)convertRect:(CGRect)rect toLayer:(CALayer *)layer;

Flipped Geometry

在iOS中采用的是左上角为原点的坐标,而Mac OS中采用的坐下家为原点的坐标。Core Animation通过geometryFlipped属性来支持上述两种坐标体系的转换。如果geometryFlipped属性为YES,则表示当前layer相对于父layer坐标进行几何垂直翻转。

The Z Axis

不同于UIView的二维坐标,CALayer通过zPosition和anchorPointZ两个float属性提供三维坐标空间。zPosition值越大越显示在上面。

Hit Testing

虽然CALayer不感知responder chain,但是他通过提供-containPoint:和-hitTest:两个方法来帮助你定位事件所在layer。 另外CALayer对自动布局缺乏支持,建议使用UIView。


4. Visual EFfects

Rounded Corners 圆角

CALayer有一个cornerRadius属性用来制作layer的圆角,值表示圆弧的半径,单位是上面提到过3种单位中的point,他可以被设置为任何的float数字(default=0)。默认情况下该属性只作用于backgroundColor,而对于sublayer和背景图都没有效果,但是如果配合masksToBounds=YES的设置,可以对layer内所有元素生效。

Layer Border 边框

CALayer通过borderWidth和borderColor两个属性的组合来定义border的宽度和颜色。 borderWidth用float的point度量定义了boder笔画的粗细。

borderColor的类型是CGColorRef,而不是UIColor;他同时会retain传给他的这个CGColorRef。

border位于bounds范围内,而且位于所有的子layer之上。不管内容是什么形状或者是否超出当前layer的bounds,border总是沿着bounds。

Drop Shadows 阴影

通过设置shadowOpacity属性为一个大于0的值,可以给任意layer添加一个背景阴影。取值范围是0.0到1.0,表示全透明到非透明。同时还可以通过shadowColor、shadowOffset和shadowRadius三个属性来修改阴影的外观。

shadowColor同borderColor和backgroundColor类似用于修改阴影的颜色,默认是黑色。

shadowOffset是一个CGSize类型,控制了阴影的距离和方向。默认值是{0,-3},是的默认值下阴影位于layer的上部,因为这个属性最早用于Mac OS,而Mac OS的坐标系和iOS是相反的。

shadowRadius控制阴影的模糊度,值越大越模糊。通常我们使用较大的值来制造一种比较大的视觉深度。

Shadow Clipping 阴影裁切

不同于border围绕在layer的bounds,shadow是围绕在真正的内容外。正是由于他围绕在真正内容的外面,如果他超过bounds时,会被masksToBounds给裁切掉,通常这不是我们想要的效果。

如果你既想要裁切多余的内容,同时又显示阴影,这时候你就需要两个重叠的layer,一个masksToBounds=YES,另外一个则=NO同时设置想要的阴影外观

shadowPath 阴影路径

由于需要结合所有子layer画出边界外的阴影,所以shadow方法其实会有性能上的隐患;shadowPath正是为了解决性能问题而推出的,可以给开发者手动设置阴影的路径。

shadowPath可以接受CGPathRef类型的值,你可以通过CGPath开头的很多CoreGraphic方法绘制自己想要的阴影图形。

Layer Masking 遮罩

mask属性接受另外一个CALayer值,可以被用作将当前layer裁切成给定layer的轮廓。

Scaling Filters 缩放方式

minificationFilter 用于指定缩小的方式

magnificationFilter 用于指定放大的方式

可用的值均是

kCAFilterLinear
kCAFilterNearest
kCAFilterTrilinear

Group Opacity

我们通常使用UIView的alpha属性来设置view的透明度,如果在CALayer中我们同样会使用opacity来设置,这2个属性同样会作用于他得所有subView和subLayer。

但是如果是一个有sublayer的control可能会出现下图这样的情况

image

这主要是由于helloword所在层也被设置了opacity(假设opacity=0.5),和后面的control的opacity叠加,所以你看到的中间部分其实是0.5*0.5+0.5=0.75。

通常这不是我们想要的效果,我们通常有2种方法来处理:

  1. 在app的Info.plist中添加UIViewGroupOpacity=YES。这会作用于当前app下得所有所有范围,可能还有点小的性能浪费。
  2. 通过CALayer的 shouldRasterize=YES,将layer及其子layer全部打散成一个平面的图形,再使Opacity生效就可以达到我们想要的效果了。由于rasterizationScale默认等于1.0,会使得高清屏幕下图片像素化,所以我们还要同时设置一下

    layer.rasterizationScale = [[UIScreen mainScreen] scale];

5. Transforms

Affine Transforms 仿射变换

UIView的transform属性类型是CGAffineTransform,他是用于对view进行二维空间下的旋转、拉伸或变形。CGAffineTransform其实是一个2列3行的矩阵,被用于乘以一个二维的行向量(当前情况下是CGPoint),来实现变换。但是实际上你会经常看到二维变换是一个3x3的矩阵。

image

仿射变换的限制是无论如何变换,变换之前平行的两条线,变换后仍然平行,比如下图前3个变换都是仿射变换,而最后一个就不是仿射变化,明显竖线上述条件。

image

Core Graphics提供创建仿射变化的快捷方法

// 这里的angle是弧度,通常使用的如:M_PI_4/M_PI
CGAffineTransformMakeRotation(CGFloat angle)

CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)

CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)

// 角度和弧度相互转换的宏
#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)

#define DEGREES_TO_RADIANS(x) ((x)/180.0*M_PI)

UIView可以通过transform属性来实现变换,其实他就是对CALayer的封装。

CALayer同时也有一个transform属性,不过他的类型不是CAAffineTransform,而是CATransform3D。CALayer实现仿射变化的属性是affineTransform。

Combining Transforms

Core Graphics同时提供了在一个transform上合并另外一个的方法。

CGAffineTransformRotate(CGAffineTransform t, CGFloat angle) 

CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy) 

CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)

首先你需要创建一个恒等变换:

CGAffineTransformIdentity

使用下面方法组合两个不同的变换

CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);

另外以顺序组合变换,前一个变换会影响到下一个变换,比如如下代码:

//create a new transform
CGAffineTransform transform = CGAffineTransformIdentity; //scale by 50%
transform = CGAffineTransformScale(transform, 0.5, 0.5); //rotate by 30 degrees

//translate by 200 points
transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0); 

transform = CGAffineTransformTranslate(transform, 200, 0);

//apply transform to layer	
self.layerView.layer.affineTransform = transform;

Shear Transform

虽然官方没有提供这种变换的方法,但是我们仍然可以通过修改transform的a,b,c,d几个参数来实现

3D Transforms

跟仿射变换的3x3矩阵不同,CATransform3D是一个4x4的矩阵,从而支持了对于垂直屏幕的z轴的变换效果。

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

和上面2D的方法有点类似,只是多了一个z的参数,另外旋转方法多了3个x,y,z的参数来表示旋转针对哪个轴。(大于0即可,如果同时有多个值大于0会怎么旋转,回头试试看 lol)

image

如上图所示,环绕z轴的旋转其实就是我们上面提到过的2D旋转,而环绕x,y轴的旋转则使得layer离开了屏幕所在的2维屏幕。 如果只是y轴的旋转,可能会让你看起来似乎只是对layer做了横向的压缩。其实他看起来不对的原因是没有perspective(透视)。

Perspective Projection

透视投影的意思是在真实世界中,我们看到的东西越远则越小。所以我们期望在靠近我们的边界应该比远离我们的边界更大一些。 虽然框架没有提供我们透视变换的方法,但是我们可以使用CATransform3D的m34属性来实现类似效果。m34属性通常被用来计算x和y值的缩放比例,默认值是0。我们一般设置属性m34的值等于 -1.0/d(d的含义是我们设想的镜头和屏幕的距离)。d的取值范围一般在500-1000之间会看起来比较自然,值越小透视效果越明显甚至可能会失真,越大透视效果越小甚至可能没有。

Layer Flatterning

在2维环境下对一个父Layer和子Layer分别做变换没有任何问题,但是在3维环境下就不是了。这里指的3维其实是指对z轴进行变化,而不是指用3d的变换方法。如果用3d的变换方法进行2d的变换,比如只对z做旋转也是没有问题的,但是对x和y轴做旋转可能就不是我们想要的那种效果了。 虽然Core Animation的layer是在3D空间中的,但其实他是一个伪3D。每个layer的3D视觉效果是由他得子layer创建出来的(视觉差的效果),比如你倾斜屏幕并不能真正绕过屏幕上面对你的那个layer。所以说每个layer的3D场景其实是扁平化的。 其实你很难通过Core Animation创建一个非常复杂真实的3D场景,但是有另外CALayer的子类提供了类似的解决方案,后面会提到他CATransformLayer。


6. Specialized Layers

CAShapeLayer

CAShapeLayer使用矢量图形代替了位图图像进行绘制,优点有:

  1. 速度快 - 硬件加速
  2. 高效使用内存 - 不需要像普通CALayer一样先创建一个背景图像,所以更节约内存
  3. 绘图不限于当前边界 - 默认可以绘制到bounds以外
  4. 无像素结构 - 拉伸或者做3D透视变换不会导致layer的像素化

终极版的圆角

使用UIBezierPath下面两个创建圆角的方法,再配合CAShapeLayer可以制作出符合各种需要的圆角layer。再配合layer的mask属性对layer做我们想要的裁切。

+ (UIBezierPath *)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii

+ (UIBezierPath *)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius

CATextLayer

CATextLayer保留了UILabel的大部分显示文字用到的方法,并且还额外添加了一些更好的,同时渲染速度也优于UILabel。一个鲜为人知的事实是在iOS6及更早的版本,UILabel使用WebKit来渲染,这有显著的性能开销。而CATextLayer使用的是Core Text所以明显更快。

CATextLayer *textLayer = [CATextLayer layer];
textLayer.font = CGFontCreateWithFontName([[UIFont systemFontOfSize:15] fontName]);
textLayer.contentScale = [UIScreen mainScreen].scale;
textLayer.string = @"something to show";

string属性的类型是id,因为他同时接受NSString和NSAttributedString类型的值。

通过+(Class)layerClass方法来指定当前view生成的layer类型。

CATransformLayer

CATransformLayer不同于其他的layer,他不显示他自己的任何内容,而是用于承载子layer的变换内容,同时他不会对子layer进行扁平化,从而使得他可以对不同的子Layer运用两个完全独立的3D变换。

CAGradientLayer

两种颜色的渐变通过接受数组的colors属性,以及startPoint和endPoint定义方向。

//create gradient layer and add it to our container view
CAGradientLayer *gradientLayer = [CAGradientLayer layer]; 
gradientLayer.frame = self.containerView.bounds; 	[self.containerView.layer addSublayer:gradientLayer];

//set gradient colors
gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, 							(__bridge id)[UIColor blueColor].CGColor];

//set gradient start and end points
gradientLayer.startPoint = CGPointMake(0, 0); 	gradientLayer.endPoint = CGPointMake(1, 1);

多种颜色的变换,增加一个locations属性定义每个颜色的位置。 locations、startPoint、endPoint属性都采用Unit Point为单位。

CAReplicatorLayer

通过instanceCount设置复制的数量,传递CATransform3D给instanceTransform设置每个拷贝的变换(相对上一个拷贝的变换,而不是原始拷贝)。

CAReplicatorLayer *replicator = [CAReplicatorLayer layer];

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;

可以使用CAReplicatorLayer很方便的实现镜像的效果,例如:

//move reflection instance below original and flip vertically
CATransform3D transform = CATransform3DIdentity;
CGFloat verticalOffset = self.bounds.size.height + 2;
transform = CATransform3DTranslate(transform, 0, verticalOffset, 0); 
transform = CATransform3DScale(transform, 1, -1, 0); 
layer.instanceTransform = transform;

//reduce alpha of reflection layer
layer.instanceAlphaOffset = -0.6;

CASCrollLayer

用于实现最基本的滚动视图效果,类似UIScrollView和UITableView。因为他比较简单所以没有类似的滚动条,不传递touch事件,以及其他许多特别的效果。

CATiledLayer

CATiledLayer用于解决加载太大的图片时的性能问题。

CAEmitterLayer

CAEmitterLayer诞生自iOS 5,他是一个展示高性能的例子效果引擎,例如烟雾、火、雨等等。 CAEmitterLayer是一个CAEmitterCell的容器,CAEmitterCell跟CALayer类似

还有其他很多属性有待研究

CAEAGLLayer

使用Open GL的方法来实现绘图功能

AVPlayerLayer

AVPlayerLayer其实不是Core Animation框架的,而是来自AVFoundation,用于在iOS中播放视频。他基于例如MPMoviePlayer的高级API实现,并提供了更底层对视频播放控制的方法。

comments powered by Disqus