Core Animation
参考文档:https://pan.baidu.com/s/1HaQRu8c8bNfKTSbxF5__Sw
相同内容网页版:https://www.kancloud.cn/manual/ios/97798
Core Animation不单是用来做动画的,实际上它是从Layer Kit这么一个不怎么和动画有关的名字演变而来,所以做动画只是Core Animation特性的冰山一角。
Core Animation是一个复合引擎,它的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的图层,存储在一个叫做视图树的体系之中。于是这个树形成了UIKit以及在iOS应用程序当中你所能在屏幕上看见的一切的基础。
CALayer
CALayer类在概念上和UIVIew类似,同样是一些被层级关系树管理的矩形块,同样也可以包含一些内容(例如图片、文本或者背景色),管理子图层的位置。它们有一些方法和属性用来做动画和变换。和UIView最大的区别是CALayer不能处理用户交互。
平行的层级关系
每一个UIView都有一个CALayer示例的图层属性,也就是所谓的backing layer,视图的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,它们关联的图层也同样对应在层级关系树中有相同的操作。
实际上这些背后关联的图层才是真正用来在屏幕上显示和做动画,UIView仅仅是对它的一个封装,提供了一些iOS类似于处理触摸的具体功能,以及Core Animation底层方法的高级接口。
之所以iOS提供两个平行的层级关系,在于要做职责分离,这样也能避免很多重复的代码。实际上这里不是两个层级关系,而是四个,视图层级、图层树、呈现树和渲染树。
图层的能力
UIView没有暴露出来的CALayer的功能:
- 阴影、圆角、带颜色的边框
- 3D变换
- 非矩形范围
- 透明遮罩
- 多级非线性动画
CALayer的contents属性
这个属性被定义为id,意味着它可以是任何类型的对象,在这种情况下,你可以给contents属性赋任何值,你的app仍然能够编译通过。但是,在实践中,如果你给contents赋的不是CGImage,那么你得到的图层将是空白。
实际上,真正赋值给contents的类型应该是CGImageRef,它是一个指向CGImage结构的指针。UIImage有一个CGImage属性,它返回一个“CGImage”,如果你想把这个值直接赋值给CALayer的contents,那你将会得到一个编译错误。因为CGImageRef并不是一个真正的Cocoa对象,而是一个Core Foundation类型。所以在将UIImage的CGImage属性值赋值给contents属性的时候要通过bridged关键字转换
layer.contents = (_bridge id)image.CGImage;
CALayer的contentsGravity属性
UIView大多数视觉相关的属性比如contentMode,对这些属性的操作其实是对对应图层的操作。CALayer与contentMode对应的属性叫做contentsGravity,它是一个NSString类型。contentsGravity可选的常量值有以下一些:
- contentsGravity
- kCAGravityTop
- kCAGravityBottom
- kCAGravityLeft
- kCAGravityRight
- kCAGravityTopLeft
- kCAGravityTopRight
- kCAGravityBottomLeft
- kCAGravityBottomRight
- kCAGravityResize
- kCAGravityResizeAspect
- kCAGravityResizeAspectFill
CALayer的contentsScale属性
contentsScale属性定义了寄宿图的像素尺寸和视图大小的比例,默认情况下它是一个值为1.0的浮点数。该值越大,显示的寄宿图的视觉效果越小。contentsScale的目的并不那么明显,它并不是总会对屏幕上的寄宿图有影响。例如,如果layer.contentsGravity = kCAGravityResizeAspect;
则contentsScale属性不起效果。如果layer.contentsGravity = kCAGravityCenter;
则contentsScale对寄宿图的显示有影响。
contentsScale属性其实属于支持高分辨率(又称Hi-DPI或者Retina)屏幕机制的一部分。它用来判断在绘制图层的时候应该为寄宿图创建的空间大小,和需要显示的图片的拉伸度(假设并没有设置contentsGravity属性)。UIView有一个类似的功能,但非常少用到的contentScaleFactor属性。
如果contentScale设置为1.0,将会以每个点1个像素绘制图片,如果设置为2.0,则会以每个点2个像素绘制图片,这就是我们熟知的Retina屏。
layer.contentsScale = image.scale;
如果如此设置,则会根据图片是1倍图、2倍图或者3倍图进行相应绘制。
当用代码的方式来处理寄宿图的时候,需要进行如下设置,否则图片在Retina设备上就显示的不正确
layer.contentsScale = [UIScreen mainScreen].scale
CALayer的maskToBounds属性
图层的maskToBounds相当于UIView的clipsToBounds
CALayer的contentsRect属性
contentsRect属性允许我们在图层边框里显示寄宿图的一子域。和bounds、frame不同,contentRect不是按点来计算的,它使用单位坐标,单位坐标指定在0~1之间,是一个相对寄宿图尺寸的值
例如,如果设置layer.contentsRect = CGRectMake(0, 0, 0.5, 0.5);
,则效果如下:
CALayer的contentsCenter属性
contentsCenter属性只有在图片被拉伸后才会起作用,contentsCenter可以用来定义全面拉伸的范围。
如果contentsCenter属性是上图中间的蓝色方框,那么当这个图片被拉伸后,contentsCenter属性定义的区域会被全面拉伸(也就是从四个方向进行放大或者缩小),而被这个方框分割后的其他方格会按照下图所表示的进行横向或者纵向的拉伸,或者某些方框根本不拉伸!这就是contentsCenter属性的意义。
Snip20180619_2.png
其相当于xib中如下位置的配置
Snip20180619_3.png
CALayerDelegate
如果设置了代理,则当需要被重绘的时候,CALayer会请求它的代理给它一个寄宿图来显示。它通过调用下面的方来来做到:
- (void)displayLayer:(CALayer *)layer;
如果过代理想要直接设置contents属性的话,就可以在上面的代理方法中实现。如果代理不实现-displayLayer:
方法,CALayer会转而尝试调用下面的方法:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
Demo
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(150, 150, 100, 100);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;
blueLayer.delegate = self;
//ensure that layer backing image use correct scale
blueLayer.contentsScale = [UIScreen mainScreen].scale;
[self.view.layer addSublayer:blueLayer];
//force layer to redraw。如果不实现下面代码,不会调用代理方法
[blueLayer display];
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
//draw a thick red circle
CGContextSetLineWidth(ctx, 10.0f);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextStrokeEllipseInRect(ctx, layer.bounds);
}
上面Demo的效果图
- 当UIView创建它的宿主图层的时候,它就会自动把图层的delegate设置为它自己,并提供一个-displayLayer:的实现。
- 不同于UIView,当图层显示在屏幕上的时候,CALayer不会自动重绘它的内容,它把重绘的决定权交给来开发者
- 尽管上面的demo没有使用masksToBounds属性,绘制的那个圆仍然沿边界被裁剪了。因为通过CALayerDelegate绘制寄宿图的时候,并没有对超出边界外的内容提供绘制支持
UIView和CALayer布局
UIView有三个比较重要的布局属性:fame,bounds和center,CALayer对应地叫做frame,bounds和position。center和position都代表了相对于父图层anchorPoint所在的位置。
Snip20180620_2.png
当操纵视图的frame,实际上是该表了视图下方的CALayer的frame,不能够独立于图层之外改变视图的frame。
对于视图或者图层来说,frame其实是一个虚拟的属性,是更具bounds,position和transform计算而来。所以当其中任何一个值发生改变,frame都会变化。相反,改变frame的值同样会影响到它们当中的值。
当对图层做变换的时候,比如旋转或者缩放,frame实际上代表覆盖在图层旋转之后的整个轴对齐的矩形区域,也就是说frame的宽高可能和bounds的宽高不再一致了。
Snip20180620_3.png
锚点anchorPoint
图层的anchorPoint通过position来控制它的frame的位置,可以认为anchorPoint是用来移动图层的把柄。anchorPoint用单位坐标来描述,也就是图层的相对坐标,图层左上角是{0,0},右下角是{1,1},默认坐标是{0.5,0.5}。anchorPoint可以通过指定x和y值小于0或者大于1,使得它被放置在图层范围之外。
当anchorPoint改变的时候,position属性保持不变,但是frame却改变了,实际上就是将anchorPoint点移动到原图层的中心点。示例如下:
Snip20180620_4.png
Z坐标轴
和UIView严格的二维坐标系不同,CALayer存在于一个三维空间当中。除了position和anchorPoint属性行之外,CALayer还有另外两个属性zPosition和anchorPointZ,二者都是Z轴上描述图层位置的浮点类型。通过增加图层的zPosition,可以把图层前置,于是它就在所有其他图层的前面了(或者至少是小于它的zPosition值的图层的前面)。zPosition并不需要增加太多,一般增加一个像素就可以把对应的图层前置了。
Hit Testing
CALayer并不关心任何响应链事件,所以不能直接处理触摸事件或者手势,但通过-containspoint:和-hitTest:方法可以实现类似效果。
-containspoint:接受一个本图层坐标系下的CGPoint,如果这个点在图层的frame范围内就返回YES。所以使用的时候需要把触摸坐标转换成对应图层坐标系下的坐标。
-hitTest:方法同样接受一个CGPoint类型参数,它返回图层本身,或者包含这个坐标点的叶子节点图层。
注意:当调用图层的-hitTest:方法的时候,测算顺序严格依赖于图层树当中的图层顺序,上面提到的zPosition属性可以改变屏幕上图层的顺序,当不能改变事件传递的顺序。
自动布局
如果想要随意控制CALayer的布局,就需要手动操作。最简单的方法就是使用CALayerDelegate的入校函数:
- (void)layoutSublayerOfLayer:(CALayer *)layer;
当图层的bounds发生改变,或者图层的-setneedsLayout方法被调用的时候,这个函数将会被执行。这使得你可以手动地重新摆放或者调整子图层的大小,但是不能像UIView的autoresizingMask和constraints属性做到自适应屏幕旋转。这也是为什么最好使用视图而不是单独的图层来构建应用程序的另一个重要原因。
视觉效果
圆角
通过CALayer的cornerRadius和masksToBounds属性就可以实现圆角功能
图层边框
通过CALayer的borderWidth和borderColor属性就可以实现边框功能
阴影
给shadowOpacity属性一个大于默认值(也就是0)的值,阴影就可以显示在任意图层之下。shadowOpacity是一个必须在0.0(不可见)和1.0(完全不透明)之间的浮点数。如果设置为1.0,将会显示一个有轻微模糊的黑色阴影稍微在图层之上。
通过shadowColor、shadowOffset和shadowRadius三个属性可以控制阴影的表现。shadowColor控制阴影的颜色;shadowOffset控制阴影的方向和距离;shadowRadius控制阴影的模糊度,当它的值是0的时候,阴影就喝视图一样有一个非常明确的边界线,当值越来越大的时候,边界线看上去就会越来越模糊和自然。
阴影裁剪问题
如果设置masksToBounds为YES,阴影无法显示出来,解决方法是另外用一个layer实现阴影。
shadowPath属性
通过shadowPath属性可以自定义阴影形状,demo如下:
self.layerView1.layer.shadowOpacity = 0.5f;
self.layerView2.layer.shadowOpacity = 0.5f;
self.layerView1.backgroundColor = [UIColor clearColor];
self.layerView2.backgroundColor = [UIColor clearColor];
//create a square shadow
CGMutablePathRef squarePath = CGPathCreateMutable();
CGPathAddRect(squarePath, NULL, self.layerView1.bounds);
self.layerView1.layer.shadowPath = squarePath;
CGPathRelease(squarePath);
//create a circular shadow
CGMutablePathRef circlePath = CGPathCreateMutable();
CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds);
self.layerView2.layer.shadowPath = squarePath;
CGPathRelease(squarePath);
Snip20180620_6.png
如果是矩形或者圆之类的,用CGPath会相当简单明了,但是如果是更加复杂的一些图形,UIBezierPath类会更适合。
图层蒙板
CALayer有一个属性叫做mask,这个属性本身就是一个CALayer类型。它类似于一个字图层,相对于父图层(即拥有该属性的图层)布局。不同于那些绘制在父图层中的字图层,mask图层定义了父图层的部分可见区域。
mask图层的color属性是无关紧要的,真正重要的是图层的轮廓,mask图层实心的部分会被保留下来,其他的则会被抛弃。
Snip20180620_7.png
Demo
@interface SecondViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imgView;
@end
@implementation SecondViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = self.imgView.bounds;
UIImage *maskImage = [UIImage imageNamed:@"u6594"];
maskLayer.contents = (__bridge id)maskImage.CGImage;
self.imgView.image = [UIImage imageNamed:@"u6414"];
self.imgView.layer.mask = maskLayer;
}
@end
Snip20180620_10.png
CALayer蒙板图层真正厉害的地方在于蒙板图不局限于静态图。任何有图层构成的都可以作为mask属性,这意味着蒙板可以通过代码甚至是动画实时生成。
拉伸过滤
CALayer提供三种拉伸过滤算法:
- kCAFilterLinear
- kCAFilterNearest
- kCAFilterTrilinear
minificationFilter和magnificationFilter默认的过滤器都是kCAFilterLinear,这个过滤器采用双线性滤波算法。kCAFilterNearest优点在于保留像素的差异,使用于少斜线或是曲线轮廓的图片。kCAFilterTrilinear和kCAFilterLinear非常相似,适用于多斜线或曲线轮廓的图片。
组透明
CALayer对应于UIView的alpha属性的是opacity属性,这两个属性都是影响子层级的。也就是说,如果你给一个图层设置了opacity属性,那它的字图层都会受此影响。
可以通过设置shouldRasterize主 sing来实现组透明的效果,如果它被设置为yes,在应用透明度之前,图层及其字图层都会被 整合成一个整体的图片,这样就没有透明度混合的问题了。
为了启用shouldRasterize属性,需要设置resterizationScale属性。默认情况下,所有图层拉伸都是1.0,所以如果你使用了shouldRasterize属性,需要确保resterizationScale属性匹配屏幕,以防止出现Retina屏幕像素化的问题。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
UIButton *button2 = [self customButton];
button2.center = CGPointMake(250, 150);
button2.alpha = 0.5;
[self.view addSubview:button2];
button2.layer.shouldRasterize = YES;
button2.layer.rasterizationScale = [UIScreen mainScreen].scale;
}
- (UIButton *)customButton
{
CGRect frame = CGRectMake(0, 0, 150, 50);
UIButton *button = [[UIButton alloc] initWithFrame:frame];
button.backgroundColor = [UIColor whiteColor];
button.layer.cornerRadius = 10;
frame = CGRectMake(20, 10, 110, 30);
UILabel *label = [[UILabel alloc] initWithFrame:frame];
label.text = @"Hello world";
label.textAlignment = NSTextAlignmentCenter;
[button addSubview:label];
return button;
}
变换,CGAffineTransform、CATransform3D
UIView的transform类型是CGAffineTransform,CALayer的transform类型是CATransform3D,CALayer的affineTransform属性对应的是CGAffineTransform
CGAffineTransform的实质
struct CGAffineTransform {
CGFloat a, b, c, d;
CGFloat tx, ty;
};
Snip20180621_1.png
混合变换
CGAffineTransformConcat(CGAffineTransform t1,
CGAffineTransform t2)
3D变换
Snip20180625_1.pngSnip20180625_2.png
透视投影
在真实世界中,当物体远离我们的时候,有事视觉的原因看是来会变小,理论上远离我们的视图的边要比靠近视角的边更短,但实际上3D变换中并没有发生这种情况,因为3D变换中依然保持平行。在等距离投影中,远处的物体和近处的物体保持同样的缩放比例。
CATransform3D的透视效果通过一个矩阵中的一个很简单的元素控制:m34 。m34的默认值是0,我们可以通过设置m34为-1.0/d来应用透视效果,d代表了想象中视觉相机和屏幕之间的距离,以像素为单位。
灭点
当在透视角度绘图的时候,远离相机视觉的物体将会变小变远,当远离到一个极限距离,它们可能就缩成一个点,于是所有的物体最后都汇聚消失在同一个点。
在现实中,这个点通常是视图的中心,于是为了在应用中创建拟真效果的透视,这个点应该聚在屏幕中心,或者至少是包含所有3D对象的视图中心。
Snip20180626_1.png
Core Animation定义了这个点位于变换图层的anchorPoint(通常位于图层的中心,但也有例外)。这就是说,当图层发生变换时,这个点永远位于图层变换之前anchorPoint的位置。
当改变一个图层的position,你也改变了它的灭点,做3D变换的时候要时刻记住这一点,当你视图通过调整m34来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置(而不是直接改变它的position),这样所有的3D图层都共享一个灭点。
sublayerTransform
如果有多个视图或者图层,每个都做3D变换,那就需要分别设置相同的m34值,并且确保在变换之前都在屏幕中央共享同一个position。
CALayer有一个属性叫做sublayerTransform,它也是CATransform3D类型,但和对一个图层的变换不同,它影响到所有子图层。这意味着可以一次性对包含这些图层的容器做变换,于是所有的字图层都自动继承了这个变换方法。
相较而言,通过在一个地方设置透视变换会很方便,同时它会带着另一个显著的优势:灭点被设置在容器图层的中心,从而不需要再对字图层分别设置了,这意味着可以随意使用position和frame来放置字图层,而不需要把它们放置在屏幕中点,然后为了保证统一的灭点用变换来做平移。
背面
图层是双面绘制的,反面显示的是正面的一个镜像图片。图层的双面绘制会造成资源浪费,在看不见图层背面的情况下,为什么还要浪费GPU来绘制它们呢。
CALayer有一个叫做doubleSided的属性来控制图层的背面是否要绘制,这是一个Bool类型,默认为YES,如果设置为NO,那么当图层正面从相机视觉消失的时候,它将不会被绘制。
扁平化
尽管Core Animation图层存在于3D控件之内,但它们并不都是在同一个3D空间,每个图层的3D场景其实都是扁平化的,当你从正面观察一个图层,看到的实际上由子图层创建的想象出来的3D场景,但当你倾斜这个图层,你会发现实际上这个3D场景仅仅是被绘制在图层的表面。所以单纯对一个视图的layer层做3D变换并不会影响到子视图
专用图层
CAShapeLayer
CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类。你指定诸如颜色和线宽灯属性,用CGPath来定义想要绘制的图形,最后CAShapeLayer就自动渲染出来了。当然,也可以用Core Graphics直接向原始的CALayer的内容中绘制一个路径,相比之下,使用CAShapeLayer有以下优点:
- 渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比Core Graphics快很多
- 高效使用内容。一个CAShapeLayer不需要向普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。
- 不会被图层边界裁剪掉。一个CAShapeLayer可以在边界之外绘制。
- 不会出现像素化。当你给CAShapeLayer做3D变换时,它不像一个有寄宿图的普通图层一样变得像素化。
Demo
UIBezierPath *path = [[UIBezierPath alloc] init];
[path moveToPoint:CGPointMake(175, 100)];
[path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:2 * M_PI clockwise:YES];
[path moveToPoint:CGPointMake(150, 125)];
[path addLineToPoint:CGPointMake(150, 175)];
[path addLineToPoint:CGPointMake(125, 225)];
[path moveToPoint:CGPointMake(150, 175)];
[path addLineToPoint:CGPointMake(175, 225)];
[path moveToPoint:CGPointMake(100, 150)];
[path addLineToPoint:CGPointMake(200, 150)];
CAShapeLayer *shapLayer = [CAShapeLayer layer];
shapLayer.strokeColor = [UIColor whiteColor].CGColor;
shapLayer.fillColor = [UIColor clearColor].CGColor;
shapLayer.lineWidth = 5;
shapLayer.lineJoin = kCALineJoinRound;
shapLayer.lineCap = kCALineCapRound;
shapLayer.path = path.CGPath;
[self.outerView.layer addSublayer:shapLayer];
Snip20180627_1.png
圆角
创建圆角矩形,其实就是人工绘制单独的直线和弧度,但是事实上UIBezierPath有自动绘制圆角矩形的构造方法。Demo如下,绘制一个有三个圆角一个直角的矩形:
CGRect rect = CGRectMake(50, 50, 100, 100);
CGSize radii = CGSizeMake(20, 20);
UIRectCorner corners = UIRectCornerTopRight | UIRectCornerTopLeft | UIRectCornerBottomLeft;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii];
CAShapeLayer *shapLayer = [CAShapeLayer layer];
shapLayer.strokeColor = [UIColor whiteColor].CGColor;
shapLayer.fillColor = [UIColor clearColor].CGColor;
shapLayer.lineWidth = 5;
shapLayer.lineJoin = kCALineJoinRound;
shapLayer.lineCap = kCALineCapRound;
shapLayer.path = path.CGPath;
[self.outerView.layer addSublayer:shapLayer];
Snip20180627_2.png
如果想依照图形来裁剪视图内容,可以把CAShapeLayer作为视图的宿主图层,而不是添加一个字视图。
CATextLayer
CATextLayer以图层的形式包含来UILabel几乎所有的绘制特性,并且额外提供了一些心特性。
CATextLayer的string属性并不是NSString类型,而是id类型,所以既可以用NSString也可以用NSAttributeString来指定文本
Demo:
//create a text layer
CATextLayer *textLayer = [CATextLayer layer];
textLayer.contentsScale = [UIScreen mainScreen].scale;
textLayer.frame = self.outerView.bounds;
[self.outerView.layer addSublayer:textLayer];
//set text attributes
textLayer.foregroundColor = [UIColor blackColor].CGColor;
textLayer.alignmentMode = kCAAlignmentJustified;
textLayer.wrapped = YES;
//choose a font
UIFont *font = [UIFont systemFontOfSize:15];
//set layer font
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFontRef fontRef = CGFontCreateWithFontName(fontName);
textLayer.font = fontRef;
textLayer.fontSize = font.pointSize;
CGFontRelease(fontRef);
//choose some text
NSString *text = @"辣椒开始的法律监督减肥了空间阿里的开发价值,。春夏女快放假了哦 i 将凯迪拉克你的,v来肯德基法拉第健身房";
//set layer text
textLayer.string = text;
Snip20180627_3.png
行距和字距
由于绘制的实现机制不同(Core Text和WebKit),用CATextLayer渲染和用UILabel渲染出的文本行距和字距也不尽相同。
UILabel的替代品
CATextLayer比UILabel有着更好的性能表现,但是如果是给UILabel添加一个CATextLayer子图层,由于UILabel仍然会使用-drawRect:方法创建空寄宿图层,而且由于CALayer不支持自动缩放和自动布局,字图层并不是主动跟踪视图边界的大小,所以每次视图大小被更改,就不得不手动更新子图层的边界。
每一个UIView都是寄宿在一个CALayer的示例上,这个图层由视图自动创建和管理。继承UIView,并重写+layerClass方法可以返回一个不同的图层子类。UIView会在初始化的时候调用+layerClass方法,然后用它返回类型来创建宿主图层。
CATextLayer作为宿主图层,视图会自动设置contentsScale属性。
自定义Label Demo CATextLayer的frame会随着label的frame改变
#import "LayerLabel.h"
@implementation LayerLabel
+ (Class)layerClass
{
return [CATextLayer class];
}
- (CATextLayer *)textLayer
{
return (CATextLayer *)self.layer;
}
- (void)setup
{
self.text = self.text;
self.textColor = self.textColor;
self.font = self.font;
[self textLayer].alignmentMode = kCAAlignmentJustified;
[self textLayer].wrapped = YES;
[self.layer display];
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self setup];
}
return self;
}
- (void)awakeFromNib
{
[super awakeFromNib];
[self setup];
}
- (void)setText:(NSString *)text
{
super.text = text;
[self textLayer].string = text;
}
- (void)setTextColor:(UIColor *)textColor
{
super.textColor = textColor;
[self textLayer].foregroundColor = textColor.CGColor;
}
- (void)setFont:(UIFont *)font
{
super.font = font;
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFontRef fontRef = CGFontCreateWithFontName(fontName);
[self textLayer].font = fontRef;
[self textLayer].fontSize = font.pointSize;
CGFontRelease(fontRef);
}
CATransformLayer
CATransformLayer不同于普通的CALayer,它不能显示自己的内容,只有存在了一个作用于子图层的变换,它才真正存在。CATransformLayer并不平面化它的字图层,所以它能够用于构造一个层级的3D结构,比如我的手臂示例。
Demo
- (void)viewDidLoad {
[super viewDidLoad];
[self create];
}
- (void)create
{
CATransform3D pt = CATransform3DIdentity;
pt.m34 = -1.0 / 500.0;
self.testImageView.layer.sublayerTransform = pt;
CATransform3D c1t = CATransform3DIdentity;
c1t = CATransform3DTranslate(c1t, -100, 0, 0);
CALayer *cube1 = [self cubeWithTransform:c1t];
[self.testImageView.layer addSublayer:cube1];
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.testImageView.layer addSublayer:cube2];
}
- (CALayer *)faceWithTransform:(CATransform3D)transform {
CALayer *face = [CALayer layer];
face.frame = CGRectMake(-50, -50, 100, 100);
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].CGColor;
face.transform = transform;
return face;
}
- (CALayer *)cubeWithTransform:(CATransform3D)transform {
CATransformLayer *cube = [CATransformLayer layer];
CATransform3D ct = CATransform3DMakeTranslation(0, 0, 50);
[cube addSublayer:[self faceWithTransform:ct]];
ct = CATransform3DMakeTranslation(50, 0, 0);
ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);
[cube addSublayer:[self faceWithTransform:ct]];
ct = CATransform3DMakeTranslation(0, -50, 0);
ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);
[cube addSublayer:[self faceWithTransform:ct]];
ct = CATransform3DMakeTranslation(0, 50, 0);
ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);
[cube addSublayer:[self faceWithTransform:ct]];
ct = CATransform3DMakeTranslation(-50, 0, 0);
ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);
[cube addSublayer:[self faceWithTransform:ct]];
ct = CATransform3DMakeTranslation(0, 0, -50);
ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);
[cube addSublayer:[self faceWithTransform:ct]];
CGSize containerSize = self.testImageView.bounds.size;
cube.position = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
cube.transform = transform;
return cube;
}
同一视觉下的两不同变换的立方体
CAGradientLayer
CAGradientLayer是用来生成两种或多种颜色平滑渐变的。用Core Graphics复制一个CAGrandientLayer并将内容绘制到一个普通图层的寄宿图也是有可能的,但是CAGradientLayer的真正好处是绘制使用了硬件加速。
Demo
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = _testImageView.bounds;
[self.testImageView.layer addSublayer:gradientLayer];
gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor yellowColor].CGColor,(__bridge id)[UIColor blueColor].CGColor];
//如果不设置locations属性,则各种颜色平均分布
gradientLayer.locations = @[@0.0,@0.25,@0.5];
//startPoint 和 endPoint是单位坐标
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(1, 1);
Snip20180628_4.png
CAReplicatorLayer
CAReplicatorLayer的目的是为了高效生成许多相似的图层,它会绘制一个多个图层的字图层,没在每个复制体上应用不同的变换。
变换时会逐步增加的,每一个实例都是相对于前一个实例布局。instanceCount属性指定了图层需要重复多少次,instanceTransform指定一个CATransform3D 3D变换
Demo旋转时绕CAReplicatorLayer中心点旋转
CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
replicator.frame = self.outerView.bounds;
[self.outerView.layer addSublayer:replicator];
replicator.instanceCount = 10;
//apply a transform for each instance
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DRotate(transform, M_PI/5.0, 0, 0, 1);
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(107.5, 0, 50.0, 50.0);
layer.backgroundColor = [UIColor greenColor].CGColor;
CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = CGRectMake(0, 0, 50, 50);
textLayer.string = @"xy";
[layer addSublayer:textLayer];
[replicator addSublayer:layer];
反射
使用CAReplicatorLayer并应用一个负比例变换于一个复制图层,你就可以创建指定视图内容的景象图片。
自定义反射view Demo
#import "ReflectionView.h"
@implementation ReflectionView
+ (Class)layerClass
{
return [CAReplicatorLayer class];
}
- (void)setup
{
CAReplicatorLayer *repLayer = (CAReplicatorLayer *)self.layer;
repLayer.instanceCount = 2;
CATransform3D transform = CATransform3DIdentity;
CGFloat verticalOffset = self.bounds.size.height + 2;
transform = CATransform3DTranslate(transform, 0, verticalOffset, 0);
transform = CATransform3DScale(transform, 1, -1, 0);
repLayer.instanceTransform = transform;
repLayer.instanceAlphaOffset = -0.6;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self setup];
}
return self;
}
- (void)awakeFromNib
{
[super awakeFromNib];
[self setup];
}
@end
CAScrollLayer
CAScrollLayer实现图层的滚动,不过需要自定义手势。
CATileLayer
CATileLayer可以解决载入大图的性能问题。CATileLayer将大图分解成小片,然后将它们大度按需载入。
为了能够从CATiledLayer中受益,我们需要预先把图片切成许多小一些的图片(可以用代码完成这件事)。如果是在运行时读入这个那个图片并裁切,那CATileLayer的所有性能优点就损失殆尽了。CATileLayer的默认小图大小是256*256.
CATileLayer可以很好的和UIScrollView结合使用。除了设置图层和滑动视图以适配整个图片大小,我们真正要做的就是实现-drawLayer:incontext:方法,当需要载入新的小图时,CATileLayer就会调用这个方法。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
CATiledLayer *tileLayer = [CATiledLayer layer];
tileLayer.delegate = self;
[self.scrollView.layer addSublayer:tileLayer];
self.scrollView.contentSize = tileLayer.frame.size;
[tileLayer setNeedsDisplay];
}
- (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx
{
//determine tile coordinate
CGRect bounds = CGContextGetClipBoundingBox(ctx);
NSInteger x = floor(bounds.origin.x / layer.tileSize.width);
NSInteger y = floor(bounds.origin.y / layer.tileSize.height);
//load tile image
NSString *imageName = [NSString stringWithFormat:@"picture_%ld_%ld",x,y];
UIImage *tileImage = [UIImage imageNamed:imageName];
UIGraphicsPushContext(ctx);
[tileImage drawInRect:bounds];
UIGraphicsPopContext();
}
小图并不是以Retina的分辨率显示的,为了以屏幕的原生分辨率来渲染CATileLayer,我们需要设置图层的contentScale来匹配UIScreen的scale属性:
tileLayer.contentsScale = [UIScreen mainScreen].scale;
tileSize是以像素为单位,而不是点,所以增大了contentsScale就自动有了默认的小图尺寸(现在是128128的点而不是256256),所以不需要手工更新小图的尺寸或者在Retina分辨率下指定一个不同的小图,我们需要做的是适应小图渲染代码以对应安排scale的变化
//determine tile coordinate
CGRect bounds = CGContextGetClipBoundingBox(ctx);
CGFloat scale = [UIScreen mainScreen].scale;
NSInteger x = floor(bounds.origin.x / layer.tileSize.width * scale);
NSInteger y = floor(bounds.origin.y / layer.tileSize.height * scale);
通过这个方法纠正scale也意味着图片将以原先一半的大小渲染在Retina设备上。
CAEmitterLayer
CAEmitterLayer是一个高性能的粒子引擎,被用来创建实时例子动画如:烟雾、火、雨等类似效果
CAEmitterLayer看上去像是许多CAEmitterCell的容器,这些CAEmitterCell定义了一个例子效果。我们需要为不同的例子效果定义一个或多个CAEmitterCell作为模版,同时CAEmitterLayer负责基于这些模版实例化一个粒子流。一个CAEmitterCell类似于一个CALayer:它有一个contents属性可以定义为一个CGImage,另外还有一些可设置属性控制着变现和行为。
CAEmitterLayer *emitter = [CAEmitterLayer layer];
emitter.frame = self.outerView.bounds;
[self.outerView.layer addSublayer:emitter];
//configure emitter
emitter.renderMode = kCAEmitterLayerAdditive;
emitter.emitterPosition = CGPointMake(emitter.frame.size.width/2.0, CGRectGetHeight(emitter.frame)/2.0);
//create a particle template
CAEmitterCell *cell = [CAEmitterCell new];
cell.contents = (__bridge id)([UIImage imageNamed:@"u3264"].CGImage);
cell.birthRate = 150;
cell.lifetime = 5.0;
cell.color = [UIColor redColor].CGColor;
cell.alphaSpeed = -0.4;
cell.velocity = 50;
cell.velocityRange = 50;
cell.emissionRange = M_PI * 2.0;
//add particle template to emitter
emitter.emitterCells = @[cell];
实际效果是动画过程,小正方形不断向外扩散
CAEmitterCell的属性基本上可以分为三种:
- 这种粒子的某一属性的初始值。比如,color属性置顶了一个可以混合图片内容颜色的混合颜色
- 粒子某一属性的变化范围。比如emissionRange属性值是M_PI * 2.0,这意味着粒子可以从360度任意位置发射出去
- 指定值在时间线上的变化。比如,上面例子中,我们将alphaSpeed设置为-0.4,就是说例子的透明度每秒就是较少0.4,这就是发射出去后逐渐消失的效果。
CAEmitterLayer的属性它自己控制着整个例子系统的位置和形状,一些属性比如birthRate、lifetime和celocity,这些属性在CAEmitterCell中也有。这些属性会以相乘的方式作用在一起,这样就可以用一个值来加速或者扩大整个例子系统。其他需要注意的属性有如下这些:
- preservesDepth,是否将3D例子系统平面化到一个图层(默认值)或者可以在3D空间中混合其他的图层
- renderMode,控制着在视觉上粒子图片是如何混合的。上面例子中kCAEmitterLayerAdditive实现的效果为:合并例子重叠部分的亮度使得看上去更亮。
CAEAGLLayer
OpenGL提供了Core Animation的基础,它是底层的C接口。iOS5中,苹果引入了一个新的框架叫做GLKit,它去掉了一些设置OpenGL的复杂性,提供了一个叫做CLKView的UIView子类,帮你处理大部分的设置和绘制工作。前提是各种各样的OpenGL绘图缓冲的底层可配置项仍需要你用CAEAGLLayer完成,它是CALayer的一个子类,用来显示任意的OpenGL图形。
@interface SecondViewController ()<CALayerDelegate>
@property (nonatomic, weak) IBOutlet UIView *glView;
@property (nonatomic, strong) EAGLContext *glContext;
@property (nonatomic, strong) CAEAGLLayer *glLayer;
@property (nonatomic, assign) GLuint framebuffer;
@property (nonatomic, assign) GLuint colorRenderbuffer;
@property (nonatomic, assign) GLint framebufferWidth;
@property (nonatomic, assign) GLint framebufferHeight;
@property (nonatomic, strong) GLKBaseEffect *effect;
@end
@implementation SecondViewController
- (void)setUpBuffers
{
//set up frame buffer
glGenFramebuffers(1, &_framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
//set up color render buffer
glGenRenderbuffers(1, &_colorRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer);
[self.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.glLayer];
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_framebufferWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_framebufferHeight);
//check success
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
NSLog(@"Failed to make complete framebuffer object: %i", glCheckFramebufferStatus(GL_FRAMEBUFFER));
}
}
- (void)tearDownBuffers
{
if (_framebuffer) {
//delete framebuffer
glDeleteFramebuffers(1, &_framebuffer);
_framebuffer = 0;
}
if (_colorRenderbuffer) {
//delete color render buffer
glDeleteRenderbuffers(1, &_colorRenderbuffer);
_colorRenderbuffer = 0;
}
}
- (void)drawFrame {
//bind framebuffer & set viewport
glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
glViewport(0, 0, _framebufferWidth, _framebufferHeight);
//bind shader program
[self.effect prepareToDraw];
//clear the screen
glClear(GL_COLOR_BUFFER_BIT); glClearColor(0.0, 0.0, 0.0, 1.0);
//set up vertices
GLfloat vertices[] = {
-0.5f, -0.5f, -1.0f, 0.0f, 0.5f, -1.0f, 0.5f, -0.5f, -1.0f,
};
//set up colors
GLfloat colors[] = {
0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
};
//draw triangle
glEnableVertexAttribArray(GLKVertexAttribPosition);
glEnableVertexAttribArray(GLKVertexAttribColor);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, vertices);
glVertexAttribPointer(GLKVertexAttribColor,4, GL_FLOAT, GL_FALSE, 0, colors);
glDrawArrays(GL_TRIANGLES, 0, 3);
//present render buffer
glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
[self.glContext presentRenderbuffer:GL_RENDERBUFFER];
}
- (void)viewDidLoad
{
[super viewDidLoad];
//set up context
self.glContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:self.glContext];
//set up layer
self.glLayer = [CAEAGLLayer layer];
self.glLayer.frame = self.glView.bounds;
[self.glView.layer addSublayer:self.glLayer];
self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@NO, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};
//set up base effect
self.effect = [[GLKBaseEffect alloc] init];
//set up buffers
[self setUpBuffers];
//draw frame
[self drawFrame];
}
- (void)viewDidUnload
{
[self tearDownBuffers];
[super viewDidUnload];
}
- (void)dealloc
{
[self tearDownBuffers];
[EAGLContext setCurrentContext:nil];
}
@end
Snip20180629_2.png
AVPlayerLayer
AVPlayerLayer不是Core Animation框架的一部分,其是AVFoundation的一部分。AVPlayerLayer与Core Animation紧密地结合在一起,提供一个CALayer子类来显示自定义的内容类型。
AVPlayerLayer是用来在iOS上播放视频的,它是高级接口如MPMoviePlayer的底层实现,提供了显示视频的底层控制。
NSString *urlPath = [[NSBundle mainBundle] pathForResource:@"aaa" ofType:@"mp4"];
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:[NSURL fileURLWithPath:urlPath]];
self.player = [AVPlayer playerWithPlayerItem:item];
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
playerLayer.frame = CGRectMake(0, 0, CGRectGetWidth([UIScreen mainScreen].bounds), 200);
[_bgView.layer addSublayer:playerLayer];
[self.player play];
隐式动画
事务
当改变CALayer的一个可做动画的属性,它并不能立刻在屏幕上体现出来。相反,它是从先前的值平滑过渡到新的值,这一切都是默认的行为。这就是所谓的隐式动画。
当改变一个属性Core Animation是如何判断动画类型和持续时间的呢?实际上动画执行的时间取决于当前事务的设置,动画类型取决于图层行为。
事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。
事务是通过CATransaction类来做管理,这个类设计有些奇怪,不像你从它命名预期的那样去管理一个简单的事务,而是管理类一叠你不能访问的事务。CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建。但可以用+begin和+commit分别来入栈或者出栈。
任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(默认0.25秒)。
Demo
- (void)viewDidLoad
{
[super viewDidLoad];
//set up context
self.colorLayer = [CALayer layer];
self.colorLayer.frame = self.colorView.bounds;
[self.colorView.layer addSublayer:self.colorLayer];
}
- (IBAction)buttonOnClickAction:(id)sender {
//begin a new transaction
[CATransaction begin];
//set the animation duration to 1 second
[CATransaction setAnimationDuration:1.0];
CGFloat red = arc4random() /(CGFloat)INT_MAX;
CGFloat green = arc4random() /(CGFloat)INT_MAX;
CGFloat blue = arc4random() /(CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1].CGColor;
//commit the transaction
[CATransaction commit];
}
实际上,UIView的+beginAnimations: context:和+commitAnimations之间所有视图或者图层属性的改变而做的动画都是由于设置了CATransaction的原因。UIView的+animateWithDuration: animations:方法内部也会自动调用CATransaction的+begin和+commit方法
给事务设置CompletionBlock
[CATransaction begin];
//set the animation duration to 1 second
[CATransaction setAnimationDuration:1.0];
//add the spin animation on completion
[CATransaction setCompletionBlock:^{
}];
CGFloat red = arc4random() /(CGFloat)INT_MAX;
CGFloat green = arc4random() /(CGFloat)INT_MAX;
CGFloat blue = arc4random() /(CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1].CGColor;
//commit the transaction
[CATransaction commit];
图层行为
Core Animation通常对CALayer的所有属性(可动画的属性)做动画,但是UIView把和它关联的图层的这个特性关闭了。
当CALayer的属性被修改的时候,它会调用-actionForKey:方法,传递属性的名称。剩下的操作都是CALayer的头文件中有详细的说明,实质上是如下几步:
- 图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forkey:方法。如果有,直接调用并返回结果。
- 如果没有委托,或者委托没有实现-actionForLayer:forkey:方法,图层接着检查包含属性名称对应映射的actions字典。
- 如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
- 最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。
所以一轮完整的搜索结束之后,-actionForKey:要么返回空(这种情况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去和先前和当前的值做动画。
UIView对它关联的图层禁用隐式动画的实现方法就是:当不在一个动画块的实现中,-actionForKey:方法返回空,但是在动画block范围之内,它就返回一个非空值。
//test layer action when outside of animation block,返回空值
NSLog(@"Outside: %@",[self.colorView actionForLayer:self.colorView.layer forKey:@"xxx"]);
//bing animation block
[UIView beginAnimations:nil context:nil];
//test layer action when inside of animation block,返回非空值
NSLog(@"Inside: %@",[self.colorView actionForLayer:self.colorView.layer forKey:@"xxx"]);
//end animation block
[UIView commitAnimations];
呈现与模型
每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层当中,它可以通过-presentationLayer方法来访问,这个呈现图层实际上是模型图层的复制,但是它的属性值代表了任何指定时刻当前外观效果。换句话说,你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值。
前面提到除来图层树之外还有呈现树。呈现树就是通过图层树中所有图层的呈现图层形成的。
显示动画
像所有NSObject子类一样,CAAnimation实现KVC协议,于是你可以用-setValue:forKey:和-valueForKey:方法来存取属性。但是CAAnimation有一个不同的性能,它更像一个NSDictionary,可以让你随意设置键值对,即使和你使用的动画类所声明的属性并不匹配。这意味着可以对动画用任意类型打标签,从而在代理方法中识别动画。
关键帧动画
CAKeyframeAnimation是另一种UIKit没有暴露出来但功能强大的累,和CABasicAnimation类似,CAKeyframeAnimation同样是CAProperyAnimation的一个子类,它依然作用于单一的一个属性,但是和CABasicAnimation不一样的是,它不限制于设置一个起始和结束的值,而是可以根据一连串随意的值来做动画。
动画组CAAnimationGroup
CABasicAnimation和CAKeyframeAnimation仅仅作用于单独的属性,而CAAnimationGroup可以把这些动画组合在一起。CAAnimationGroup是另一个继承于CAAnimation的子类,它添加了一个animations数组的属性,用来组合别的动画。关键代码如下:
CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
groupAnimation.animations = @[animation1,animation2];
groupAnimation.duration = 4.0;
[self.colorView.layer addAnimation:groupAnimation forKey:nil];
过渡
有时候对于iOS应用程序来说,希望通过属性动画来对比较难做的动画的布局进行一些改变,比如交换一段文本和图片,或者用一段网格视图来替换,等等。属性动画只对图层的可动画属性起作用,所以如果要改变一个不能动画的属性(比如图片),或者从层级关系汇总添加或者移除图层,属性动画将不起作用。
于是就有了过渡的概念,过渡并不像属性动画那样平滑地在两个值之间动画,而是影响到整个图层的变化。过渡动画首先展示之前的图层外观,然后通过一个交换过渡到了新的外观。
为了创建一个过渡动画,我们将使用CATransition,同样是另一个CAAnimation的子类,和别的子类不同,CATransition有一个type和subtype来表示变换效果。type属性是一个NSString类型,可以被设置成如下类型:
kCATransitionFade(默认) 淡入淡出
kCATransitionMoveIn 从顶部滑动进入
kCATransitionPush 从边缘的一侧滑动进来
kCATransitionReveal 把原始的图层滑动出去来显示新的外观
后面三种过渡类型都有一个默认的动画方向,它们都从左侧滑入,但可以通过subtype来控制它们的方向,提供如下四种类型:
kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom
自定义动画
UIView的+transitionFromView: toView: duration: options: completion:和+transitionWithView: duration: options: animations: completion:方法提供了Core Animation的过渡特性。UIView过渡方法中options参数可以由如下常量指定:
UIViewAnimationOptionTransitionNone 默认
UIViewAnimationOptionTransitionFlipFromLeft ,
UIViewAnimationOptionTransitionFlipFromRight ,
UIViewAnimationOptionTransitionCurlUp ,
UIViewAnimationOptionTransitionCurlDown ,
UIViewAnimationOptionTransitionCrossDissolve ,
UIViewAnimationOptionTransitionFlipFromTop ,
UIViewAnimationOptionTransitionFlipFromBottom ,
除了UIViewAnimationOptionTransitionCrossDissolve之外,剩下的值和CATransition类型完全没关系
通过使用CALayer的-renderInContext:方法把它绘制到Core Graphics的上下文中捕获当前内容的图片,然后在另外的视图中显示出来。如果把这个截图置于原始视图之上,就可以遮住真实视图的所有变化,于是自定义一个过渡效果。
在动画过程中取消动画
终止指定的动画:
- (void)removeAnimationForKey:(NSString *)key;
移除所有动画:
- (void)removeAllAnimations;
动画一旦被移除,图层的外观就立刻更新到当前的模型层的值。一般来说,动画在结束之后被自动移除,除非设置removedOnCompletion为NO。如果设置动画结束之后不被自动移除,那么当它不需要的时候要手动移除它,否则它会一直存在于内存中,直到图层被销毁。
图层时间
CAMediaTiming协议
在“显式动画”中提到过的duration就是CAMediaTiming的属性之一,duration为将要进行的动画的一次迭代指定了时间。CAMediaTiming另外一个属性叫做repeatCount,代表动画重复的迭代次数。完整的动画时间是duration * repeatCount。
创建重复动画的另一种方式是使用repeatDuration属性,它让动画重复一个指定的时间,而不是指定次数。甚至可以设置autoreverses属性,使得每次间隔交替循环过程中自动回放。
将repeatDuration或者repeatCount设置为INFINITY可以实现无限循环
相对时间
关于Core Animation的时间是相对的,每个动画都有它自己描述的时间,可以独立地加速、延时或者偏移。
beginTime指定了动画开始之前的延迟时间,这里的延迟从动画添加到可见图层的那一刻开始测量,默认是0
speed是一个时间的倍数,默认1.0,减少它会减慢图层/动画的时间,增加它会加快速度。如果speed为2.0,那么对于一个duration为1的动画,实际上在0.5秒的时候就已经完成了。
timeOffset是让动画快进到某一时间点。例如,对于一个持续1秒的动画来说,设置timeOffset为0.5意味着动画将从一半的地方开始。
和beginTime不同的是timeOffset并不受speed的影响,所以如果你把speed设置为2.0,吧timeOffset设置为0.5,那么对于duration为1的动画,动画将从动画最后结束的地方开始。
层级关系时间
每个动画和图层在时间上都有它的层级概念,相对于它的父亲来测量。对图层调整时间将会影响到它本身和字图层的动画,但不会影响到父图层。
对CALayer或者CAGroupAnimation调整duration和repeatCount/repeatDuration属性并不会影响到子动画。但是beginTime,timeOffset和speed属性将会影响到子动画。
全局时间和本地时间
Core Animation有一个全局时间的概念,也就是所谓的马赫时间。马赫时间在设备上所有进程都是全局的,但是在不同设备上并不是全局的。可以使用如下方法访问马赫时间:
CFTimeInterval time = CACurrentMediaTime();
这个函数返回的恶值其实无关紧要(它返回了设备自从上次启动后的秒数),它真实的作用在于对动画时间测量提供一个相对值。
注意:当设备休眠的时候马赫时间会暂停,也就是所有CAAnimations(基于马赫时间)同样会暂停。
每个CALayer和CAAnimation实例都有自己本地时间的概念,是根据父图层/动画层级关系中的beginTime,timeOffset和speed属性计算。CALayer提供了如下方法转换不同图层之间的本地时间:
- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(nullable CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(nullable CALayer *)l;
暂停、倒回和快进
给图层添加一个CAAnimation实际上是给动画对象做一个不可改变的拷贝,所以对原始动画对象属性的修改对真实的动画并没有作用。相反,使用-animationForKey:来检索图层正在进行的动画可以返回正确的动画对象,但是修改它会抛出异常。
一个简单的方法是利用CAMediaTiming来暂停图层本身。如果把图层的speed设置为0,它会暂停任何添加到图层上的动画,类似的,设置speed大于1.0将会快进,设置成一个负值将会倒回动画。
手动动画
timeOffset一个很有用的功能在于它可以让你手动控制动画进程,通过设置speed为0,可以禁用动画的自动播放,让后使用timeOffset来来回回显示动画序列。
缓冲
CAMediaTimingFunction
使用缓冲方程式的方法是设置CAAnimation的timingFunction属性,是CAMediaTimingFunction类的一个对象。如果行抗改变隐式动画的基石函数,同样也可以使用CATransaction的+setAnimationTimingfunction:方法。
最简单的创建CAMediaTimingFunction的方式是调用+ (instancetype)functionWithName:(NSString *)name;方法,这里传入如下几个常量之一:
kCAMediaTimingFunctionLinear(默认)匀速
kCAMediaTimingFunctionEaseIn 慢到快
kCAMediaTimingFunctionEaseOut 快到慢
kCAMediaTimingFunctionEaseInEaseOut 慢快慢
kCAMediaTimingFunctionDefault 和kCAMediaTimingFunctionEaseInEaseOut类似,只是加速和减速的过程都稍微有些慢。
CAMediaTimingFunction使用了一个叫做三次贝塞尔曲线的函数,它只可以产出指定缓冲函数的子集。一个三次贝塞尔曲线通过四个点来定义,第一个和最后一个点代表了曲线的起点和终点,剩下中间两个点叫做控制点,因为它们控制了曲线的形状,贝塞尔曲线控制点其实是位于曲线之外的点,也就是说曲线并不一定要穿过它们。可以把它们想象成吸引经过它们的磁铁。
CAKeyframeAnimation有个timingFunctions属性是CAMediaTimingFunction对象数组。可以用于实现复杂动画