iOS Core Animation:Advanced Techniques(iOS核心动画高级技巧)-上篇
iOS Core Animation:Advanced Techniques(iOS核心动画高级技巧)-中篇
iOS Core Animation:Advanced Techniques(iOS核心动画高级技巧)-下篇
目录
- 专用图层
6.1 CAShapeLayer
6.2 CATextLayer
6.3 CATransformLayer
6.4 CAGradientLayer
6.5 CAReplicatorLayer
6.6 CAScrollLayer
6.7 CATiledLayer
6.8 CAEmitterLayer
6.9 CAEAGLLayer
6.10 AVPlayerLayer - 隐式动画
7.1 事务
7.2 完成块
7.3 图层行为
7.4 呈现与模型 - 显示动画
8.1 属性动画
8.2 动画组
8.3 过渡
8.4 在动画过程中取消动画 - 图层时间
9.1 CAMediaTiming协议
9.2 层级关系时间
9.3 手动动画 - 缓冲
10.1 动画速度
10.2 自定义缓冲函数
第六章:专用图层
前言:
到目前为止,我们已经探讨过CALayer类了,同时我们也了解到了一些非常有用的绘图和动画功能。但是Core Animation图层不仅仅能作用于图片和颜色而已。本章就会学习其他的一些图层类,进一步扩展使用Core Animation绘图的能力。
第1篇:CAShapeLayer
CAShapeLayer是一个通过矢量图形而不是bitmap来绘制的图层子类。你指定诸如颜色和线宽等属性,用CGPath来定义想要绘制的图形,最后CAShapeLayer就自动渲染出来了。当然,你也可以用Core Graphics直接向原始的CALyer的内容中绘制一个路径,相比直下,使用CAShapeLayer有以下一些优点:
- 渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多。
- 高效使用内存。一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。
- 不会被图层边界剪裁掉。一个CAShapeLayer可以在边界之外绘制。你的图层路径不会像在使用Core Graphics的普通CALayer一样被剪裁掉(如我们在第二章所见)。
- 不会出现像素化。当你给CAShapeLayer做3D变换时,它不像一个有寄宿图的普通图层一样变得像素化。
创建一个CGPath
CAShapeLayer可以用来绘制所有能够通过CGPath来表示的形状。这个形状不一定要闭合,图层路径也不一定要不可破,事实上你可以在一个图层上绘制好几个不同的形状。你可以控制一些属性比如lineWith(线宽,用点表示单位),lineCap(线条结尾的样子),和lineJoin(线条之间的结合点的样子);但是在图层层面你只有一次机会设置这些属性。如果你想用不同颜色或风格来绘制多个形状,就不得不为每个形状准备一个图层了。
下面的代码用一个CAShapeLayer渲染一个简单的火柴人。CAShapeLayer属性是CGPathRef类型,但是我们用UIBezierPath帮助类创建了图层路径,这样我们就不用考虑人工释放CGPath了。
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)];
//create shape layer
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 5;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.lineCap = kCALineCapRound;
shapeLayer.path = path.CGPath;
//add it to our view
[self.containerView.layer addSublayer:shapeLayer];
圆角
下面这段代码使用UIBezierPath和CAShapeLayer绘制了一个有三个圆角一个直角的矩形:
//define path parameters
CGRect 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];
我们可以通过这个图层路径绘制一个既有直角又有圆角的视图。如果我们想依照此图形来剪裁视图内容,我们可以把CAShapeLayer作为视图的宿主图层,而不是添加一个子视图(图层蒙板的详细解释见第四章『视觉效果』)。
第2篇:CATextLayer
如果你想在一个图层里面显示文字,完全可以借助图层代理直接将字符串使用Core Graphics写入图层的内容(这就是UILabel的精髓)。
Core Animation提供了一个CALayer的子类CATextLayer,它以图层的形式包含了UILabel几乎所有的绘制特性,并且额外提供了一些新的特性。
同样,CATextLayer也要比UILabel渲染得快得多。很少有人知道在iOS 6及之前的版本,UILabel其实是通过WebKit来实现绘制的,这样就造成了当有很多文字的时候就会有极大的性能压力。而CATextLayer使用了Core text,并且渲染得非常快。
用CATextLayer来实现一个UILabel
//create a text layer
CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = self.labelView.bounds;
[self.labelView.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 = @"Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis";
//set layer text
textLayer.string = text;
如果你实现了代码并且界面显示出来了,你会发现一个奇怪的地方:这些文本有一些像素化了。这是因为并没有以Retina的方式渲染,第二章提到了这个contentScale属性,用来决定图层内容应该以怎样的分辨率来渲染。contentsScale并不关心屏幕的拉伸因素而总是默认为1.0。如果我们想以Retina的质量来显示文字,我们就得手动地设置CATextLayer的contentsScale属性,如下:
textLayer.contentsScale = [UIScreen mainScreen].scale;
注意:CATextLayer的string属性并不是你想象的NSString类型,而是id类型。这样你既可以用NSString也可以用NSAttributedString来指定文本了(注意,NSAttributedString并不是NSString的子类)。属性化字符串是iOS用来渲染字体风格的机制,它以特定的方式来决定指定范围内的字符串的原始信息,比如字体,颜色,字重,斜体等。
富文本
用CATextLayer实现一个富文本标签。
UILabel的替代品
我们想要一个用CATextLayer作为宿主图层的UILabel子类,这样就可以没有冗余的寄宿图啦。
就像我们在第一章『图层树』讨论的一样,每一个UIView都是寄宿在一个CALayer的示例上。这个图层是由视图自动创建和管理的,那我们可以用别的图层类型替代它么?一旦被创建,我们就无法代替这个图层了。但是如果我们继承了UIView,那我们就可以重写+layerClass方法使得在创建的时候能返回一个不同的图层子类。UIView会在初始化的时候调用+layerClass方法,然后用它的返回类型来创建宿主图层。
下面代码演示了一个UILabel子类LayerLabel用CATextLayer绘制它的问题,而不是调用一般的UILabel使用的较慢的-drawRect:方法
#import "LayerLabel.h"
@implementation LayerLabel
+ (Class)layerClass {
//this makes our label create a CATextLayer //instead of a regular CALayer for its backing layer
return [CATextLayer class];
}
- (CATextLayer *)textLayer {
return (CATextLayer *)self.layer;
}
- (void)setUp {
//set defaults from UILabel settings
self.text = self.text;
self.textColor = self.textColor;
self.font = self.font;
//we should really derive these from the UILabel settings too
//but that's complicated, so for now we'll just hard-code them
[self textLayer].alignmentMode = kCAAlignmentJustified;
[self textLayer].wrapped = YES;
[self.layer display];
}
- (id)initWithFrame:(CGRect)frame {
//called when creating label programmatically
if (self = [super initWithFrame:frame]) {
[self setUp];
}
return self;
}
- (void)awakeFromNib {
//called when creating label using Interface Builder
[self setUp];
}
- (void)setText:(NSString *)text {
super.text = text;
//set layer text
[self textLayer].string = text;
}
- (void)setTextColor:(UIColor *)textColor {
super.textColor = textColor;
//set layer text color
[self textLayer].foregroundColor = textColor.CGColor;
}
- (void)setFont:(UIFont *)font {
super.font = font;
//set layer font
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFontRef fontRef = CGFontCreateWithFontName(fontName);
[self textLayer].font = fontRef;
[self textLayer].fontSize = font.pointSize;
CGFontRelease(fontRef);
}
@end
如果你运行代码,你会发现文本并没有像素化,而我们也没有设置contentsScale属性。把CATextLayer作为宿主图层的另一好处就是视图自动设置了contentsScale属性。
总得来说,如果想在app里面充分利用CALayer子类,用+layerClass来创建基于不同图层的视图是一个简单可复用的方法。
第3篇:CATransformLayer
CATransformLayer不同于普通的CALayer,因为它不能显示它自己的内容。只有当存在了一个能作用域子图层的变换它才真正存在。CATransformLayer并不平面化它的子图层,所以它能够用于构造一个层级的3D结构
第4篇:CAGradientLayer
CAGradientLayer是用来生成两种或更多颜色平滑渐变的。用Core Graphics复制一个CAGradientLayer并将内容绘制到一个普通图层的寄宿图也是有可能的,但是CAGradientLayer的真正好处在于绘制使用了硬件加速。
基础渐变:
用CAGradientLayer实现简单的两种颜色的对角线渐变
//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属性是一个浮点数值的数组(以NSNumber包装)。这些浮点数定义了colors属性中每个不同颜色的位置,同样的,也是以单位坐标系进行标定。0.0代表着渐变的开始,1.0代表着结束。
locations数组并不是强制要求的,但是如果你给它赋值了就一定要确保locations的数组大小和colors数组大小一定要相同,否则你将会得到一个空白的渐变。
用locations构造偏移至左上角的三色渐变
//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 yellowColor].CGColor, (__bridge id)[UIColor greenColor].CGColor];
//set locations
gradientLayer.locations = @[@0.0, @0.25, @0.5];
//set gradient start and end points
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(1, 1);
第5篇:CAReplicatorLayer
CAReplicatorLayer的目的是为了高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换。
重复图层(Repeating Layers)
第6篇:CAScrollLayer
第7篇:CATiledLayer
第8篇:CAEmitterLayer
第9篇:CAEAGLLayer
第10篇:AVPlayerLayer
总结:
这一章我们简要概述了一些专用图层以及用他们实现的一些效果,我们只是了解到这些图层的皮毛,像CATiledLayer和CAEMitterLayer这些类可以单独写一章的。但是,重点是记住CALayer是用处很大的,而且它并没有为所有可能的场景进行优化。为了获得Core Animation最好的性能,你需要为你的工作选对正确的工具,希望你能够挖掘这些不同的CALayer子类的功能。 这一章我们通过CAEmitterLayer和AVPlayerLayer类简单地接触到了一些动画,在第二章,我们将继续深入研究动画,就从隐式动画开始。
第七章:隐式动画
前言:
我们在第一部分讨论了Core Animation除了动画之外可以做到的任何事情。但是动画是Core Animation库一个非常显著的特性。这一章我们来看看它是怎么做到的。具体来说,我们先来讨论框架自动完成的隐式动画(除非你明确禁用了这个功能)。
第1篇:事务
Core Animation基于一个假设,说屏幕上的任何东西都可以(或者可能)做动画。动画并不需要你在Core Animation中手动打开,相反需要明确地关闭,否则他会一直存在。
当你改变CALayer的一个可做动画的属性,它并不能立刻在屏幕上体现出来。相反,它是从先前的值平滑过渡到新的值。这一切都是默认的行为,你不需要做额外的操作。
这其实就是所谓的隐式动画。之所以叫隐式是因为我们并没有指定任何动画的类型 。我们仅仅改变了一个属性,然后Core Animation来决定如何并且何时去做动画。Core Animaiton同样支持显式动画,下章详细说明。
但当你改变一个属性,Core Animation是如何判断动画类型和持续时间的呢?实际上动画执行的时间取决于当前事务的设置,动画类型取决于图层行为。
事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。
事务是通过CATransaction类来做管理,CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建它。但是可以用+begin和+commit分别来入栈或者出栈。
任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(默认0.25秒)。
Core Animation在每个run loop周期中自动开始一次新的事务(run loop是iOS负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的东西),即使你不显式的用[CATransaction begin]开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。
使用CATransaction控制动画时间
- (IBAction)changeColor {
//begin a new transaction
[CATransaction begin];
//set the animation duration to 1 second
[CATransaction setAnimationDuration:1.0];
//randomize the layer background color
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.0].CGColor;
//commit the transaction
[CATransaction commit];
}
如果你用过UIView的动画方法做过一些动画效果,那么应该对这个模式不陌生。UIView有两个方法,+beginAnimations:context:和+commitAnimations,和CATransaction的+begin和+commit方法类似。实际上在+beginAnimations:context:和+commitAnimations之间所有视图或者图层属性的改变而做的动画都是由于设置了CATransaction的原因。
在iOS4中,苹果对UIView添加了一种基于block的动画方法:+animateWithDuration:animations:。这样写对做一堆的属性动画在语法上会更加简单,但实质上它们都是在做同样的事情。
CATransaction的+begin和+commit方法在+animateWithDuration:animations:内部自动调用,这样block中所有属性的改变都会被事务所包含。这样也可以避免开发者由于对+begin和+commit匹配的失误造成的风险。
第2篇:完成块
基于UIView的block的动画允许你在动画结束的时候提供一个完成的动作。CATranscation接口提供的+setCompletionBlock:方法也有同样的功能。
在每次颜色变化结束之后切换到另一个旋转90的动画
- (IBAction)changeColor {
//begin a new transaction
[CATransaction begin];
//set the animation duration to 1 second
[CATransaction setAnimationDuration:1.0];
//add the spin animation on completion
[CATransaction setCompletionBlock:^{
//rotate the layer 90 degrees
CGAffineTransform transform = self.colorLayer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_2);
self.colorLayer.affineTransform = transform;
}];
//randomize the layer background color
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.0].CGColor;
//commit the transaction
[CATransaction commit];
}
注意旋转动画要比颜色渐变快得多,这是因为完成块是在颜色渐变的事务提交并出栈之后才被执行
第3篇:图层行为
Core Animation通常对CALayer的所有属性(可动画的属性)做动画,但是UIView把它关联的图层的这个特性关闭了。为了更好说明这一点,我们需要知道隐式动画是如何实现的。
我们把改变属性时CALayer自动应用的动画称作行为,当CALayer的属性被修改时候,它会调用-actionForKey:方法,传递属性的名称。剩下的操作都在CALayer的头文件中有详细的说明,实质上是如下几步:
- 图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法。如果有,直接调用并返回结果。
- 如果没有委托,或者委托没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
- 如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
- 最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。
所以一轮完整的搜索结束之后,-actionForKey:要么返回空(这种情况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画。
于是这就解释了UIKit是如何禁用隐式动画的:每个UIView对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey的实现方法。当不在一个动画块的实现中,UIView对所有图层行为返回nil,但是在动画block范围之内,它就返回了一个非空值,我们可以用一个demo做个简单的实验:
//test layer action when outside of animation block
NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
//begin animation block
[UIView beginAnimations:nil context:nil];
//test layer action when inside of animation block
NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
//end animation block
[UIView commitAnimations];
运行程序,控制台显示结果如下:
$ LayerTest[21215:c07] Outside:
$ LayerTest[21215:c07] Inside:
于是我们可以预言,当属性在动画块之外发生改变,UIView直接通过返回nil来禁用隐式动画。但如果在动画块范围之内,根据动画具体类型返回相应的属性,在这个例子就是CABasicAnimation(第八章“显式动画”将会提到)。
当然返回nil并不是禁用隐式动画唯一的办法,CATransacition有个方法叫做+setDisableActions:,可以用来对所有属性打开或者关闭隐式动画。如果在清单7.2的[CATransaction begin]之后添加下面的代码,同样也会阻止动画的发生:
[CATransaction setDisableActions:YES];
总结一下,我们知道了如下几点
UIView关联的图层禁用了隐式动画,对这种图层做动画的唯一办法就是使用UIView的动画函数(而不是依赖CATransaction),或者继承UIView,并覆盖-actionForLayer:forKey:方法,或者直接创建一个显式动画(具体细节见第八章)。
对于单独存在的图层,我们可以通过实现图层的-actionForLayer:forKey:委托方法,或者提供一个actions字典来控制隐式动画。
第4篇:呈现与模型
CALayer的属性行为其实很不正常,因为改变一个图层的属性并没有立刻生效,而是通过一段时间渐变更新。这是怎么做到的呢?
当你改变一个图层的属性,属性值的确是立刻更新的(如果你读取它的数据,你会发现它的值在你设置它的那一刻就已经生效了),但是屏幕上并没有马上发生改变。这是因为你设置的属性并没有直接调整图层的外观,相反,他只是定义了图层动画结束之后将要变化的外观。
在iOS中,屏幕每秒钟重绘60次。如果动画时长比60分之一秒要长,Core Animation就需要在设置一次新值和新值生效之间,对屏幕上的图层进行重新组织。这意味着CALayer除了“真实”值(就是你设置的值)之外,必须要知道当前显示在屏幕上的属性值的记录。
每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层当中,他可以通过-presentationLayer方法来访问。这个呈现图层实际上是模型图层的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值
你可能注意到有一个叫做–modelLayer的方法。在呈现图层上调用–modelLayer将会返回它正在呈现所依赖的CALayer。通常在一个图层上调用-modelLayer会返回–self(实际上我们已经创建的原始图层就是一种数据模型)。
大多数情况下,你不需要直接访问呈现图层,你可以通过和模型图层的交互,来让Core Animation更新显示。两种情况下呈现图层会变得很有用,一个是同步动画,一个是处理用户交互。
- 如果你在实现一个基于定时器的动画(见第11章“基于定时器的动画”),而不仅仅是基于事务的动画,这个时候准确地知道在某一时刻图层显示在什么位置就会对正确摆放图层很有用了。
- 如果你想让你做动画的图层响应用户输入,你可以使用-hitTest:方法(见第三章“图层几何学”)来判断指定图层是否被触摸,这时候对呈现图层而不是模型图层调用-hitTest:会显得更有意义,因为呈现图层代表了用户当前看到的图层位置,而不是当前动画结束之后的位置。
我们可以用一个简单的案例来证明后者(见下面代码)。在这个例子中,点击屏幕上的任意位置将会让图层平移到那里。点击图层本身可以随机改变它的颜色。我们通过对呈现图层调用-hitTest:来判断是否被点击。
如果修改代码让-hitTest:直接作用于colorLayer而不是呈现图层,你会发现当图层移动的时候它并不能正确显示。这时候你就需要点击图层将要移动到的位置而不是图层本身来响应点击(这就是为什么用呈现图层来响应交互的原因)。
使用presentationLayer图层来判断当前图层位置
- (void)viewDidLoad {
[super viewDidLoad];
//create a red layer
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(0, 0, 100, 100);
self.colorLayer.position = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2);
self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
[self.view.layer addSublayer:self.colorLayer];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//get the touch point
CGPoint point = [[touches anyObject] locationInView:self.view];
//check if we've tapped the moving layer
if ([self.colorLayer.presentationLayer hitTest:point]) {
//randomize the layer background color
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.0].CGColor;
} else {
//otherwise (slowly) move the layer to new position
[CATransaction begin];
[CATransaction setAnimationDuration:4.0];
self.colorLayer.position = point;
[CATransaction commit];
}
}
总结:
这一章讨论了隐式动画,还有Core Animation对指定属性选择合适的动画行为的机制。同时你知道了UIKit是如何充分利用Core Animation的隐式动画机制来强化它的显式系统,以及动画是如何被默认禁用并且当需要的时候启用的。最后,你了解了呈现和模型图层,以及Core Animation是如何通过它们来判断出图层当前位置以及将要到达的位置。
在下一章中,我们将研究Core Animation提供的显式动画类型,既可以直接对图层属性做动画,也可以覆盖默认的图层行为。
第八章:显式动画
前言:
上一章介绍了隐式动画的概念。隐式动画是在iOS平台创建动态用户界面的一种直接方式,也是UIKit动画机制的基础,不过它并不能涵盖所有的动画类型。在这一章中,我们将要研究一下显式动画,它能够对一些属性做指定的自定义动画,或者创建非线性动画,比如沿着任意一条曲线移动。
第1篇:属性动画
当使用-addAnimation:forKey:把动画添加到图层,这里有一个到目前为止我们都设置为nil的key参数。这里的键是-animationForKey:方法找到对应动画的唯一标识符,而当前动画的所有键都可以用animationKeys获取。如果我们对每个动画都关联一个唯一的键,就可以对每个图层循环所有键,然后调用-animationForKey:来比对结果。尽管这不是一个优雅的实现。
幸运的是,还有一种更加简单的方法。像所有的NSObject子类一样,CAAnimation实现了KVC(键-值-编码)协议,于是你可以用-setValue:forKey:和-valueForKey:方法来存取属性。但是CAAnimation有一个不同的性能:它更像一个NSDictionary,可以让你随意设置键值对,即使和你使用的动画类所声明的属性并不匹配。
这意味着你可以对动画用任意类型打标签。在这里,我们给UIView类型的指针添加的动画,所以可以简单地判断动画到底属于哪个视图,然后在委托方法中用这个信息正确地更新钟的指针(见下面的代码)。
使用KVC对动画打标签
CABasicAnimation *animation = [CABasicAnimation animation];
[self updateHandsAnimated:NO];
animation.keyPath = @"transform";
animation.toValue = [NSValue valueWithCATransform3D:transform];
animation.duration = 0.5;
animation.delegate = self;
[animation setValue:handView forKey:@"handView"];
[handView.layer addAnimation:animation forKey:nil];
- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag {
//set final position for hand view
UIView *handView = [anim valueForKey:@"handView"];
handView.layer.transform = [anim.toValue CATransform3DValue];
}
我们成功的识别出每个图层停止动画的时间,然后更新它的变换到一个新值,很好。
不幸的是,即使做了这些,还是有个问题,该代码在模拟器上运行的很好,但当真正跑在iOS设备上时,我们发现在-animationDidStop:finished:委托方法调用之前,指针会迅速返回到原始值,这个清单8.3图层颜色发生的情况一样。
问题在于回调方法在动画完成之前已经被调用了,但不能保证这发生在属性动画返回初始状态之前。这同时也很好地说明了为什么要在真实的设备上测试动画代码,而不仅仅是模拟器。
我们可以用一个fillMode属性来解决这个问题,下一章会详细说明,这里知道在动画之前设置它比在动画结束之后更新属性更加方便。
关键帧动画
CABasicAnimation揭示了大多数隐式动画背后依赖的机制,这的确很有趣,但是显式地给图层添加CABasicAnimation相较于隐式动画而言,只能说费力不讨好。
CAKeyframeAnimation是另一种UIKit没有暴露出来但功能强大的类。和CABasicAnimation类似,CAKeyframeAnimation同样是CAPropertyAnimation的一个子类,它依然作用于单一的一个属性,但是和CABasicAnimation不一样的是,它不限制于设置一个起始和结束的值,而是可以根据一连串随意的值来做动画。
设置一个颜色的数组,然后通过关键帧动画播放出来(见下面的代码)
使用CAKeyframeAnimation应用一系列颜色的变化
- (IBAction)changeColor {
//create a keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"backgroundColor";
animation.duration = 2.0;
animation.values = @[
(__bridge id)[UIColor blueColor].CGColor,
(__bridge id)[UIColor redColor].CGColor,
(__bridge id)[UIColor greenColor].CGColor,
(__bridge id)[UIColor blueColor].CGColor ];
//apply animation to layer
[self.colorLayer addAnimation:animation forKey:nil];
}
运行它,你会发现动画通过颜色不断循环,但效果看起来有些奇怪。原因在于动画以一个恒定的步调在运行。当在每个动画之间过渡的时候并没有减速,这就产生了一个略微奇怪的效果,为了让动画看起来更自然,我们需要调整一下缓冲,第十章将会详细说明。
提供一个数组的值就可以按照颜色变化做动画,但一般来说用数组来描述动画运动并不直观。CAKeyframeAnimation有另一种方式去指定动画,就是使用CGPath。path属性可以用一种直观的方式,使用Core Graphics函数定义运动序列来绘制动画。
虚拟属性
之前提到过属性动画实际上是针对于关键路径而不是一个键,这就意味着可以对子属性甚至是虚拟属性做动画。但是虚拟属性到底是什么呢?
考虑一个旋转的动画:如果想要对一个物体做旋转的动画,那就需要作用于transform属性,因为CALayer没有显式提供角度或者方向之类的属性。
解决方案:为了旋转图层,我们可以对transform.rotation关键路径应用动画,而不是transform本身。
对虚拟的transform.rotation属性做动画
//add the ship
CALayer *shipLayer = [CALayer layer];
shipLayer.frame = CGRectMake(0, 0, 128, 128);
shipLayer.position = CGPointMake(150, 150);
shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:shipLayer];
//animate the ship rotation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation";
animation.duration = 2.0;
animation.byValue = @(M_PI * 2);
[shipLayer addAnimation:animation forKey:nil];”
用transform.rotation而不是transform做动画的好处如下:
- 我们可以不通过关键帧一步旋转多于180度的动画。
- 可以用相对值而不是绝对值旋转(设置byValue而不是toValue)。
- 可以不用创建CATransform3D,而是使用一个简单的数值来指定角度。
- 不会和transform.position或者transform.scale冲突(同样是使用关键路径来做独立的动画属性)。
transform.rotation属性有一个奇怪的问题是它其实并不存在。这是因为CATransform3D并不是一个对象,它实际上是一个结构体,也没有符合KVC相关属性,transform.rotation实际上是一个CALayer用于处理动画变换的虚拟属性。
第2篇:动画组
CABasicAnimation和CAKeyframeAnimation仅仅作用于单独的属性,而CAAnimationGroup可以把这些动画组合在一起。CAAnimationGroup是另一个继承于CAAnimation的子类,它添加了一个animations数组的属性,用来组合别的动画。
第3篇:过渡
属性动画只对图层的可动画属性起作用,所以如果要改变一个不能动画的属性(比如图片),或者从层级关系中添加或者移除图层,属性动画将不起作用。
于是就有了过渡的概念。过渡并不像属性动画那样平滑地在两个值之间做动画,而是影响到整个图层的变化。过渡动画首先展示之前的图层外观,然后通过一个交换过渡到新的外观。
为了创建一个过渡动画,我们将使用CATransition,同样是另一个CAAnimation的子类,和别的子类不同,CATransition有一个type和subtype来标识变换效果。type属性是一个NSString类型,可以被设置成如下类型:
- kCATransitionFade
- kCATransitionMoveIn
- kCATransitionPush
- kCATransitionReveal
到目前为止你只能使用上述四种类型,但你可以通过一些别的方法来自定义过渡效果,后续会详细介绍。
默认的过渡类型是kCATransitionFade,当你在改变图层属性之后,就创建了一个平滑的淡入淡出效果。
我们在第七章的例子中就已经用到过kCATransitionPush,它创建了一个新的图层,从边缘的一侧滑动进来,把旧图层从另一侧推出去的效果。
kCATransitionMoveIn和kCATransitionReveal与kCATransitionPush类似,都实现了一个定向滑动的动画,但是有一些细微的不同,kCATransitionMoveIn从顶部滑动进入,但不像推送动画那样把老土层推走,然而kCATransitionReveal把原始的图层滑动出去来显示新的外观,而不是把新的图层滑动进入。
后面三种过渡类型都有一个默认的动画方向,它们都从左侧滑入,但是你可以通过subtype来控制它们的方向,提供了如下四种类型:
- kCATransitionFromRight
- kCATransitionFromLeft
- kCATransitionFromTop
- kCATransitionFromBottom
一个简单的用CATransition来对非动画属性做动画的例子如下面所示,这里我们对UIImage的image属性做修改,但是隐式动画或者CAPropertyAnimation都不能对它做动画,因为Core Animation不知道如何在插图图片。通过对图层应用一个淡入淡出的过渡,我们可以忽略它的内容来做平滑动画,我们来尝试修改过渡的type常量来观察其它效果。
使用CATransition对图像平滑淡入淡出
interface ViewController ()
@property (nonatomic, weak) IBOutlet UIImageView *imageView;
@property (nonatomic, copy) NSArray *images;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//set up images
self.images = @[[UIImage imageNamed:@"Anchor.png"],
[UIImage imageNamed:@"Cone.png"],
[UIImage imageNamed:@"Igloo.png"],
[UIImage imageNamed:@"Spaceship.png"]];
}
- (IBAction)switchImage {
//set up crossfade transition
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
//apply transition to imageview backing layer
[self.imageView.layer addAnimation:transition forKey:nil];
//cycle to next image
UIImage *currentImage = self.imageView.image;
NSUInteger index = [self.images indexOfObject:currentImage];
index = (index + 1) % [self.images count];
self.imageView.image = self.images[index];
}
@end
你可以从代码中看出,过渡动画和之前的属性动画或者动画组添加到图层上的方式一致,都是通过-addAnimation:forKey:方法。但是和属性动画不同的是,对指定的图层一次只能使用一次CATransition,因此,无论你对动画的键设置什么值,过渡动画都会对它的键设置成“transition”,也就是常量kCATransition。
隐式过渡
CATransision可以对图层任何变化平滑过渡的事实使得它成为那些不好做动画的属性图层行为的理想候选。苹果当然意识到了这点,并且当设置了CALayer的content属性的时候,CATransition的确是默认的行为。但是对于视图关联的图层,或者是其他隐式动画的行为,这个特性依然是被禁用的,但是对于你自己创建的图层,这意味着对图层contents图片做的改动都会自动附上淡入淡出的动画。
对图层树的动画
CATransition并不作用于指定的图层属性,这就是说你可以在即使不能准确得知改变了什么的情况下对图层做动画,例如,在不知道UITableView哪一行被添加或者删除的情况下,直接就可以平滑地刷新它,或者在不知道UIViewController内部的视图层级的情况下对两个不同的实例做过渡动画。
这些例子和我们之前所讨论的情况完全不同,因为它们不仅涉及到图层的属性,而且是整个图层树的改变--我们在这种动画的过程中手动在层级关系中添加或者移除图层。
这里用到了一个小诡计,要确保CATransition添加到的图层在过渡动画发生时不会在树状结构中被移除,否则CATransition将会和图层一起被移除。一般来说,你只需要将动画添加到被影响图层的superlayer。
在下列代码中,我们展示了如何在UITabBarController切换标签的时候添加淡入淡出的动画。这里我们建立了默认的标签应用程序模板,然后用UITabBarControllerDelegate的-tabBarController:didSelectViewController:方法来应用过渡动画。我们把动画添加到UITabBarController的视图图层上,于是在标签被替换的时候动画不会被移除。
对UITabBarController做动画
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController {
//set up crossfade transition
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
//apply transition to tab bar controller's view
[self.tabBarController.view.layer addAnimation:transition forKey:nil];
}
自定义动画
我们证实了过渡是一种对那些不太好做平滑动画属性的强大工具,但是CATransition的提供的动画类型太少了。
更奇怪的是苹果通过UIView +transitionFromView:toView:duration:options:completion:和+transitionWithView:duration:options:animations:方法提供了Core Animation的过渡特性。但是这里的可用的过渡选项和CATransition的type属性提供的常量完全不同。
但这并不意味着在iOS上就不能实现自定义的过渡效果了。这只是意味着你需要做一些额外的工作。就像之前提到的那样,过渡动画做基础的原则就是对原始的图层外观截图,然后添加一段动画,平滑过渡到图层改变之后那个截图的效果。如果我们知道如何对图层截图,我们就可以使用属性动画来代替CATransition或者是UIKit的过渡方法来实现动画。
事实证明,对图层做截图还是很简单的。CALayer有一个-renderInContext:方法,可以通过把它绘制到Core Graphics的上下文中捕获当前内容的图片,然后在另外的视图中显示出来。如果我们把这个截屏视图置于原始视图之上,就可以遮住真实视图的所有变化,于是重新创建了一个简单的过渡效果。
清单8.14演示了一个基本的实现。我们对当前视图状态截图,然后在我们改变原始视图的背景色的时候对截图快速转动并且淡出,图8.5展示了我们自定义的过渡效果。
为了让事情更简单,我们用UIView -animateWithDuration:completion:方法来实现。虽然用CABasicAnimation可以达到同样的效果,但是那样的话我们就需要对图层的变换和不透明属性创建单独的动画,然后当动画结束的是哦户在CAAnimationDelegate中把coverView从屏幕中移除。
用renderInContext:创建自定义过渡效果
@implementation ViewController
- (IBAction)performTransition {
//preserve the current view snapshot
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0);
[self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *coverImage = UIGraphicsGetImageFromCurrentImageContext();
//insert snapshot view in front of this one
UIView *coverView = [[UIImageView alloc] initWithImage:coverImage];
coverView.frame = self.view.bounds;
[self.view addSubview:coverView];
//update the view (we'll simply randomize the layer background color)
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.view.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
//perform animation (anything you like)
[UIView animateWithDuration:1.0 animations:^{
//scale, rotate and fade the view
CGAffineTransform transform = CGAffineTransformMakeScale(0.01, 0.01);
transform = CGAffineTransformRotate(transform, M_PI_2);
coverView.transform = transform;
coverView.alpha = 0.0;
} completion:^(BOOL finished) {
//remove the cover view now we're finished with it
[coverView removeFromSuperview];
}];
}
@end
这里有个警告:-renderInContext:捕获了图层的图片和子图层,但是不能对子图层正确地处理变换效果,而且对视频和OpenGL内容也不起作用。但是用CATransition,或者用私有的截屏方式就没有这个限制了。
第4篇:在动画过程中取消动画
之前提到过,你可以用-addAnimation:forKey:方法中的key参数来在添加动画之后检索一个动画,使用如下方法:
- (CAAnimation *)animationForKey:(NSString *)key;
但并不支持在动画运行过程中修改动画,所以这个方法主要用来检测动画的属性,或者判断它是否被添加到当前图层中。
为了终止一个指定的动画,你可以用如下方法把它从图层移除掉:
- (void)removeAnimationForKey:(NSString *)key;
或者移除所有动画:
- (void)removeAllAnimations;
动画一旦被移除,图层的外观就立刻更新到当前的模型图层的值。一般说来,动画在结束之后被自动移除,除非设置removedOnCompletion为NO,如果你设置动画在结束之后不被自动移除,那么当它不需要的时候你要手动移除它;否则它会一直存在于内存中,直到图层被销毁。
我们来扩展之前旋转飞船的示例,这里添加一个按钮来停止或者启动动画。这一次我们用一个非nil的值作为动画的键,以便之后可以移除它。-animationDidStop:finished:方法中的flag参数表明了动画是自然结束还是被打断,我们可以在控制台打印出来。如果你用停止按钮来终止动画,它会打印NO,如果允许它完成,它会打印YES。
总结:
这一章中,我们涉及了属性动画(你可以对单独的图层属性动画有更加具体的控制),动画组(把多个属性动画组合成一个独立单元)以及过度(影响整个图层,可以用来对图层的任何内容做任何类型的动画,包括子图层的添加和移除)。
在第九章中,我们继续学习CAMediaTiming协议,来看一看Core Animation是怎样处理逝去的时间。
第九章:图层时间
前言:
在上面两章中,我们探讨了可以用CAAnimation和它的子类实现的多种图层动画。动画的发生是需要持续一段时间的,所以计时对整个概念来说至关重要。在这一章中,我们来看看CAMediaTiming,看看Core Animation是如何跟踪时间的。
第1篇: CAMediaTiming协议
CAMediaTiming协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayer和CAAnimation都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。
持续和重复:
duration(CAMediaTiming的属性之一),duration是一个CFTimeInterval的类型(类似于NSTimeInterval的一种双精度浮点类型),对将要进行的动画的一次迭代指定了时间。
这里的一次迭代是什么意思呢?CAMediaTiming另外还有一个属性叫做repeatCount,代表动画重复的迭代次数。
创建重复动画的另一种方式是使用repeatDuration属性,它让动画重复一个指定的时间,而不是指定次数。你甚至设置一个叫做autoreverses的属性(BOOL类型)在每次间隔交替循环过程中自动回放。这对于播放一段连续非循环的动画很有用,例如打开一扇门,然后关上它
把repeatDuration设置为INFINITY,于是动画无限循环播放,设置repeatCount为INFINITY也有同样的效果。注意repeatCount和repeatDuration可能会相互冲突,所以你只要对其中一个指定非零值。对两个属性都设置非0值的行为没有被定义。
相对时间:
每次讨论到Core Animation,时间都是相对的,每个动画都有它自己描述的时间,可以独立地加速,延时或者偏移。
beginTime指定了动画开始之前的的延迟时间。这里的延迟从动画添加到可见图层的那一刻开始测量,默认是0(就是说动画会立刻执行)。
speed是一个时间的倍数,默认1.0,减少它会减慢图层/动画的时间,增加它会加快速度。如果2.0的速度,那么对于一个duration为1的动画,实际上在0.5秒的时候就已经完成了。
timeOffset和beginTime类似,但是和增加beginTime导致的延迟动画不同,增加timeOffset只是让动画快进到某一点,例如,对于一个持续1秒的动画来说,设置timeOffset为0.5意味着动画将从一半的地方开始。
和beginTime不同的是,timeOffset并不受speed的影响。所以如果你把speed设为2.0,把timeOffset设置为0.5,那么你的动画将从动画最后结束的地方开始,因为1秒的动画实际上被缩短到了0.5秒。然而即使使用了timeOffset让动画从结束的地方开始,它仍然播放了一个完整的时长,这个动画仅仅是循环了一圈,然后从头开始播放。
fillMode:
对于beginTime非0的一段动画来说,会出现一个当动画添加到图层上但什么也没发生的状态。类似的,removeOnCompletion被设置为NO的动画将会在动画结束的时候仍然保持之前的状态。这就产生了一个问题,当动画开始之前和动画结束之后,被设置动画的属性将会是什么值呢?
一种可能是属性和动画没被添加之前保持一致,也就是在模型图层定义的值(见第七章“隐式动画”,模型图层和呈现图层的解释)。
另一种可能是保持动画开始之前那一帧,或者动画结束之后的那一帧。这就是所谓的填充,因为动画开始和结束的值用来填充开始之前和结束之后的时间。
这种行为就交给开发者了,它可以被CAMediaTiming的fillMode来控制。fillMode是一个NSString类型,可以接受如下四种常量:
- kCAFillModeForwards
- kCAFillModeBackwards
- kCAFillModeBoth
- kCAFillModeRemoved
默认是kCAFillModeRemoved,当动画不再播放的时候就显示图层模型指定的值剩下的三种类型向前,向后或者即向前又向后去填充动画状态,使得动画在开始前或者结束后仍然保持开始和结束那一刻的值。
这就对避免在动画结束的时候急速返回提供另一种方案(见第八章)。但是记住了,当用它来解决这个问题的时候,需要把removeOnCompletion设置为NO,另外需要给动画添加一个非空的键,于是可以在不需要动画的时候把它从图层上移除。
第2篇: 层级关系时间(书中无)
在第三章“图层几何学”中,你已经了解到每个图层是如何相对在图层树中的父图层定义 它的坐标系的。动画时间和它类似,每个动画和图层在时间上都有它自己的层级概念,相对于它的父亲来测量。对图层调整时间将会影响到它本身和子图层的动画, 但不会影响到父图层。另一个相似点是所有的动画都被按照层级组合(使用CAAnimationGroup实例)。
对CALayer或者 CAGroupAnimation调整duration和repeatCount/repeatDuration属性并不会影响到子动画。但是 beginTime,timeOffset和speed属性将会影响到子动画。然而在层级关系中,beginTime指定了父图层开始动画(或者组合关系 中的父动画)和对象将要开始自己动画之间的偏移。类似的,调整CALayer和CAGroupAnimation的speed属性将会对动画以及子动画速 度应用一个缩放的因子。
全局时间和本地时间
CoreAnimation 有一个全局时间的概念,也就是所谓的马赫时间(“马赫”实际上是iOS和Mac OS系统内核的命名)。马赫时间在设备上所有进程都是全局的--但是在不同设备上并不是全局的--不过这已经足够对动画的参考点提供便利了,你可以使用 CACurrentMediaTime函数来访问马赫时间:
CFTimeInterval time = CACurrentMediaTime();
这个函数返回的值其实无关紧要(它返回了设备自从上次启动后的秒数,并不是你所关心的),它真实的作用在于对动画的时间测量提供了一个相对值。注意当设备休眠的时候马赫时间会暂停,也就是所有的CAAnimations(基于马赫时间)同样也会暂停。
因此马赫时间对长时间测量并不有用。比如用CACurrentMediaTime去更新一个实时闹钟并不明智。(可以用[NSDate date]代替,就像第三章例子所示)。
每 个CALayer和CAAnimation实例都有自己本地时间的概念,是根据父图层/动画层级关系中的beginTime,timeOffset和 speed属性计算。就和转换不同图层之间坐标关系一样,CALayer同样也提供了方法来转换不同图层之间的本地时间。如下:
- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;
当用来同步不同图层之间有不同的speed,timeOffset和beginTime的动画,这些方法会很有用。
暂停,倒回和快进
设 置动画的speed属性为0可以暂停动画,但在动画被添加到图层之后不太可能再修改它了,所以不能对正在进行的动画使用这个属性。给图层添加一个 CAAnimation实际上是给动画对象做了一个不可改变的拷贝,所以对原始动画对象属性的改变对真实的动画并没有作用。相反,直接用 -animationForKey:来检索图层正在进行的动画可以返回正确的动画对象,但是修改它的属性将会抛出异常。
如果移除图层正在进行的动画,图层将会急速返回动画之前的状态。但如果在动画移除之前拷贝呈现图层到模型图层,动画将会看起来暂停在那里。但是不好的地方在于之后就不能再恢复动画了。
一个简单的方法是可以利用CAMediaTiming来暂停图层本身。如果把图层的speed设置成0,它会暂停任何添加到图层上的动画。类似的,设置speed大于1.0将会快进,设置成一个负值将会倒回动画。
通过增加主窗口图层的speed,可以暂停整个应用程序的动画。这对UI自动化提供了好处,我们可以加速所有的视图动画来进行自动化测试(注意对于在主窗口之外的视图并不会被影响,比如UIAlertview)。可以在app delegate设置如下进行验证:
self.window.layer.speed = 100;
你也可以通过这种方式来减速,但其实也可以在模拟器通过切换慢速动画来实现。
第3篇:手动动画
timeOffset一个很有用的功能在于你可以它可以让你手动控制动画进程,通过设置speed为0,可以禁用动画的自动播放,然后来使用timeOffset来来回显示动画序列。这可以使得运用手势来手动控制动画变得很简单。
举个简单的例子:还是之前关门的动画,修改代码来用手势控制动画。我们给视图添加一个UIPanGestureRecognizer,然后用timeOffset左右摇晃。
因为在动画添加到图层之后不能再做修改了,我们来通过调整layer的timeOffset达到同样的效果。
通过触摸手势手动控制动画
@interface ViewController ()
@property (nonatomic, weak) UIView *containerView;
@property (nonatomic, strong) CALayer *doorLayer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//add the door
self.doorLayer = [CALayer layer];
self.doorLayer.frame = CGRectMake(0, 0, 128, 256);
self.doorLayer.position = CGPointMake(150 - 64, 150);
self.doorLayer.anchorPoint = CGPointMake(0, 0.5);
self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Door.png"].CGImage;
[self.containerView.layer addSublayer:self.doorLayer];
//apply perspective transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
//add pan gesture recognizer to handle swipes
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init];
[pan addTarget:self action:@selector(pan:)];
[self.view addGestureRecognizer:pan];
//pause all layer animations
self.doorLayer.speed = 0.0;
//apply swinging animation (which won't play because layer is paused)
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation.y";
animation.toValue = @(-M_PI_2);
animation.duration = 1.0;
[self.doorLayer addAnimation:animation forKey:nil];
}
- (void)pan:(UIPanGestureRecognizer *)pan {
//get horizontal component of pan gesture
CGFloat x = [pan translationInView:self.view].x;
//convert from points to animation duration //using a reasonable scale factor
x /= 200.0f;
//update timeOffset and clamp result
CFTimeInterval timeOffset = self.doorLayer.timeOffset;
timeOffset = MIN(0.999, MAX(0.0, timeOffset - x));
self.doorLayer.timeOffset = timeOffset;
//reset pan gesture
[pan setTranslation:CGPointZero inView:self.view];
}
@end
这其实是个小诡计,也许相对于设置个动画然后每次显示一帧而言,用移动手势来直接设置门的transform会更简单。
在这个例子中的确是这样,但是对于比如说关键这这样更加复杂的情况,或者有多个图层的动画组,相对于实时计算每个图层的属性而言,这就显得方便的多了。
总结:
在这一章,我们了解了CAMediaTiming协议,以及Core Animation用来操作时间控制动画的机制。在下一章,我们将要接触缓冲,另一个用来使动画更加真实的操作时间的技术。
第十章:缓冲
前言:
在第九章“图层时间”中,我们讨论了动画时间和CAMediaTiming协议。现在我们来看一下另一个和时间相关的机制--所谓的缓冲。Core Animation使用缓冲来使动画移动更平滑更自然,而不是看起来的那种机械和人工,在这一章我们将要研究如何对你的动画控制和自定义缓冲曲线。
第1篇:动画速度
动画实际上就是一段时间内的变化,这就暗示了变化一定是随着某个特定的速率进行。速率由以下公式计算而来:
velocity = change / time
上面的等式假设了速度在整个动画过程中都是恒定不变的(就如同第八章“显式动画”的情况),对于这种恒定速度的动画我们称之为“线性步调”,而且从技术的角度而言这也是实现动画最简单的方式,但也是完全不真实的一种效果。
我们如何在动画中实现这种加速度呢?一种方法是使用物理引擎来对运动物体的摩擦和动量来建模,然而这会使得计算过于复杂。我们称这种类型的方程为缓冲函数,幸运的是,Core Animation内嵌了一系列标准函数提供给我们使用。
CAMediaTimingFunction
这里传入如下几个常量之一:
- kCAMediaTimingFunctionLinear
- kCAMediaTimingFunctionEaseIn
- kCAMediaTimingFunctionEaseOut
- kCAMediaTimingFunctionEaseInEaseOut
- kCAMediaTimingFunctionDefault
kCAMediaTimingFunctionLinear选项创建了一个线性的计时函数,同样也是CAAnimation的timingFunction属性为空时候的默认函数。线性步调对于那些立即加速并且保持匀速到达终点的场景会有意义(例如射出枪膛的子弹),但是默认来说它看起来很奇怪,因为对大多数的动画来说确实很少用到。
kCAMediaTimingFunctionEaseIn常量创建了一个慢慢加速然后突然停止的方法。对于之前提到的自由落体的例子来说很适合,或者比如对准一个目标的导弹的发射。
kCAMediaTimingFunctionEaseOut则恰恰相反,它以一个全速开始,然后慢慢减速停止。它有一个削弱的效果,应用的场景比如一扇门慢慢地关上,而不是砰地一声。
kCAMediaTimingFunctionEaseInEaseOut创建了一个慢慢加速然后再慢慢减速的过程。这是现实世界大多数物体移动的方式,也是大多数动画来说最好的选择。如果只可以用一种缓冲函数的话,那就必须是它了。那么你会疑惑为什么这不是默认的选择,实际上当使用UIView的动画方法时,他的确是默认的,但当创建CAAnimation的时候,就需要手动设置它了。
最后还有一个kCAMediaTimingFunctionDefault,它和kCAMediaTimingFunctionEaseInEaseOut很类似,但是加速和减速的过程都稍微有些慢。它和kCAMediaTimingFunctionEaseInEaseOut的区别很难察觉,可能是苹果觉得它对于隐式动画来说更适合(然后对UIKit就改变了想法,而是使用kCAMediaTimingFunctionEaseInEaseOut作为默认效果),虽然它的名字说是默认的,但还是要记住当创建显式的CAAnimation它并不是默认选项(换句话说,默认的图层行为动画用kCAMediaTimingFunctionDefault作为它们的计时方法)。
UIView的动画缓冲
UIKit的动画也同样支持这些缓冲方法的使用,尽管语法和常量有些不同,为了改变UIView动画的缓冲选项,给options参数添加如下常量之一:
- UIViewAnimationOptionCurveEaseInOut
- UIViewAnimationOptionCurveEaseIn
- UIViewAnimationOptionCurveEaseOut
- UIViewAnimationOptionCurveLinear
它们和CAMediaTimingFunction紧密关联,UIViewAnimationOptionCurveEaseInOut是默认值(这里没有kCAMediaTimingFunctionDefault相对应的值了)。
//perform the animation
[UIView animateWithDuration:1.0 delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
//set the position
self.colorView.center = [[touches anyObject] locationInView:self.view];
}
completion:NULL];
缓冲和关键帧动画
或许你会回想起第八章里面颜色切换的关键帧动画由于线性变换的原因(见清单8.5)看起来有些奇怪,使得颜色变换非常不自然。为了纠正这点,我们来用更加合适的缓冲方法,例如kCAMediaTimingFunctionEaseIn,给图层的颜色变化添加一点脉冲效果,让它更像现实中的一个彩色灯泡。
我们不想给整个动画过程应用这个效果,我们希望对每个动画的过程重复这样的缓冲,于是每次颜色的变换都会有脉冲效果。
CAKeyframeAnimation有一个NSArray类型的timingFunctions属性,我们可以用它来对每次动画的步骤指定不同的计时函数。但是指定函数的个数一定要等于keyframes数组的元素个数减一,因为它是描述每一帧之间动画速度的函数。
在这个例子中,我们自始至终想使用同一个缓冲函数,但我们同样需要一个函数的数组来告诉动画不停地重复每个步骤,而不是在整个动画序列只做一次缓冲,我们简单地使用包含多个相同函数拷贝的数组就可以了。
运行更新后的代码,你会发现动画看起来更加自然了。
对CAKeyframeAnimation使用CAMediaTimingFunction
- (IBAction)changeColor {
//create a keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"backgroundColor";
animation.duration = 2.0;
animation.values = @[
(__bridge id)[UIColor blueColor].CGColor,
(__bridge id)[UIColor redColor].CGColor,
(__bridge id)[UIColor greenColor].CGColor,
(__bridge id)[UIColor blueColor].CGColor ];
//add timing function
CAMediaTimingFunction *fn = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn];
animation.timingFunctions = @[fn, fn, fn];
//apply animation to layer
[self.colorLayer addAnimation:animation forKey:nil];
}
第2篇:自定义缓冲函数
在第八章中,我们给时钟项目添加了动画。看起来很赞,但是如果有合适的缓冲函数就更好了。在显示世界中,钟表指针转动的时候,通常起步很慢,然后迅速啪地一声,最后缓冲到终点。但是标准的缓冲函数在这里每一个适合它,那该如何创建一个新的呢?
除了+functionWithName:之外,CAMediaTimingFunction同样有另一个构造函数,一个有四个浮点参数的+functionWithControlPoints::::(注意这里奇怪的语法,并没有包含具体每个参数的名称,这在objective-C中是合法的,但是却违反了苹果对方法命名的指导方针,而且看起来是一个奇怪的设计)。
使用这个方法,我们可以创建一个自定义的缓冲函数。
Core Animation的插值机制。这是一个传入起点和终点,然后在这两个点之间指定时间点产出一个新点的机制。对于简单的浮点起始值,公式如下(假设时间从0到1):
value = (endValue – startValue) × time + startValue;
那么如果要插入一个类似于CGPoint,CGColorRef或者CATransform3D这种更加复杂类型的值,我们可以简单地对每个独立的元素应用这个方法(也就CGPoint中的x和y值,CGColorRef中的红,蓝,绿,透明值,或者是CATransform3D中独立矩阵的坐标)。我们同样需要一些逻辑在插值之前对对象拆解值,然后在插值之后在重新封装成对象,也就是说需要实时地检查类型。
一旦我们可以用代码获取属性动画的起始值之间的任意插值,我们就可以把动画分割成许多独立的关键帧,然后产出一个线性的关键帧动画。清单10.7展示了相关代码。
注意到我们用了60 x 动画时间(秒做单位)作为关键帧的个数,这时因为Core Animation按照每秒60帧去渲染屏幕更新,所以如果我们每秒生成60个关键帧,就可以保证动画足够的平滑(尽管实际上很可能用更少的帧率就可以达到很好的效果)。
总结:
在这一章中,我们了解了有关缓冲和CAMediaTimingFunction类,它可以允许我们创建自定义的缓冲函数来完善我们的动画,同样了解了如何用CAKeyframeAnimation来避开CAMediaTimingFunction的限制,创建完全自定义的缓冲函数。
在下一章中,我们将要研究基于定时器的动画--另一个给我们对动画更多控制的选择,并且实现对动画的实时操纵。