图层时间

时间和空间最大的区别在于,时间不能被复用 -- 弗斯特梅里克

在上面两章中,我们探讨了可以用CAAnimation和它的子类实现的多种图层动画。动画的发生是需要持续一段时间的,所以计时对整个概念来说至关重要。在这一章中,我们来看看CAMediaTiming,看看Core Animation是如何跟踪时间的。

CAMediaTiming协议

CAMediaTiming 协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayerCAAnimation都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。

持续和重复

我们在第八章“显式动画”中简单提到过durationCAMediaTiming的属性之一),duration是一个CFTimeInterval的类型(类似于NSTimeInterval的一种双精度浮点类型),对将要进行的动画的一次迭代指定了时间。

这里的一次迭代是什么意思呢?CAMediaTiming另外还有一个属性叫做 repeatCount ,代表动画重复的迭代次数。如果 duration 是2, repeatCount 设为3.5(三个半迭代),那么完整的动画时长将是7秒。

durationrepeatCount默认都是0。但这不意味着动画时长为0秒,或者0次,这里的0仅仅代表了“默认”,也就是0.25秒和1次,你可以用一个简单的测试来尝试为这两个属性赋多个值,如清单9.1,图9.1展示了程序的结果。

清单9.1 测试durationrepeatCount

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UITextField *durationField;
@property (nonatomic, weak) IBOutlet UITextField *repeatField;
@property (nonatomic, weak) IBOutlet UIButton *startButton;
@property (nonatomic, strong) CALayer *shipLayer; @end @implementation ViewController - (void)viewDidLoad
{
[super viewDidLoad];
//add the ship
self.shipLayer = [CALayer layer];
self.shipLayer.frame = CGRectMake(, , , );
self.shipLayer.position = CGPointMake(, );
self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:self.shipLayer];
} - (void)setControlsEnabled:(BOOL)enabled
{
for (UIControl *control in @[self.durationField, self.repeatField, self.startButton]) {
control.enabled = enabled;
control.alpha = enabled? 1.0f: 0.25f;
}
} - (IBAction)hideKeyboard
{
[self.durationField resignFirstResponder];
[self.repeatField resignFirstResponder];
} - (IBAction)start
{
CFTimeInterval duration = [self.durationField.text doubleValue];
float repeatCount = [self.repeatField.text floatValue];
//animate the ship rotation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation";
animation.duration = duration;
animation.repeatCount = repeatCount;
animation.byValue = @(M_PI * );
animation.delegate = self;
[self.shipLayer addAnimation:animation forKey:@"rotateAnimation"];
//disable controls
[self setControlsEnabled:NO];
} - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
//reenable controls
[self setControlsEnabled:YES];
} @end

图9.1 演示durationrepeatCount的测试程序

创建重复动画的另一种方式是使用 repeatDuration 属性,它让动画重复一个指定的时间,而不是指定次数。你甚至设置一个叫做 autoreverses 的属性(BOOL类型)在每次间隔交替循环过程中自动回放。这对于播放一段连续非循环的动画很有用,例如打开一扇门,然后关上它(图9.2)。

图9.2 摆动门的动画

对门进行摆动的代码见清单9.2。我们用了autoreverses来使门在打开后自动关闭,在这里我们把repeatDuration设置为INFINITY,于是动画无限循环播放,设置repeatCountINFINITY也有同样的效果。注意repeatCountrepeatDuration可能会相互冲突,所以你只要对其中一个指定非零值。对两个属性都设置非0值的行为没有被定义。

清单9.2 使用autoreverses属性实现门的摇摆

@interface ViewController ()

@property (nonatomic, weak) UIView *containerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//add the door
CALayer *doorLayer = [CALayer layer];
doorLayer.frame = CGRectMake(, , , );
doorLayer.position = CGPointMake( - , );
doorLayer.anchorPoint = CGPointMake(, 0.5);
doorLayer.contents = (__bridge id)[UIImage imageNamed: @"Door.png"].CGImage;
[self.containerView.layer addSublayer:doorLayer];
//apply perspective transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
//apply swinging animation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation.y";
animation.toValue = @(-M_PI_2);
animation.duration = 2.0;
animation.repeatDuration = INFINITY;
animation.autoreverses = YES;
[doorLayer addAnimation:animation forKey:nil];
} @end

相对时间

每次讨论到Core Animation,时间都是相对的,每个动画都有它自己描述的时间,可以独立地加速,延时或者偏移。

beginTime指定了动画开始之前的的延迟时间。这里的延迟从动画添加到可见图层的那一刻开始测量,默认是0(就是说动画会立刻执行)。

speed是一个时间的倍数,默认1.0,减少它会减慢图层/动画的时间,增加它会加快速度。如果2.0的速度,那么对于一个duration为1的动画,实际上在0.5秒的时候就已经完成了。

timeOffsetbeginTime类似,但是和增加beginTime导致的延迟动画不同,增加timeOffset只是让动画快进到某一点,例如,对于一个持续1秒的动画来说,设置timeOffset为0.5意味着动画将从一半的地方开始。

beginTime不同的是,timeOffset并不受speed的影响。所以如果你把speed设为2.0,把timeOffset设置为0.5,那么你的动画将从动画最后结束的地方开始,因为1秒的动画实际上被缩短到了0.5秒。然而即使使用了timeOffset让动画从结束的地方开始,它仍然播放了一个完整的时长,这个动画仅仅是循环了一圈,然后从头开始播放。

可以用清单9.3的测试程序验证一下,设置speedtimeOffset滑块到随意的值,然后点击播放来观察效果(见图9.3)

清单9.3 测试timeOffsetspeed属性

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UILabel *speedLabel;
@property (nonatomic, weak) IBOutlet UILabel *timeOffsetLabel;
@property (nonatomic, weak) IBOutlet UISlider *speedSlider;
@property (nonatomic, weak) IBOutlet UISlider *timeOffsetSlider;
@property (nonatomic, strong) UIBezierPath *bezierPath;
@property (nonatomic, strong) CALayer *shipLayer; @end @implementation ViewController - (void)viewDidLoad
{
[super viewDidLoad];
//create a path
self.bezierPath = [[UIBezierPath alloc] init];
[self.bezierPath moveToPoint:CGPointMake(, )];
[self.bezierPath addCurveToPoint:CGPointMake(, ) controlPoint1:CGPointMake(, ) controlPoint2:CGPointMake(, )];
//draw the path using a CAShapeLayer
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.path = self.bezierPath.CGPath;
pathLayer.fillColor = [UIColor clearColor].CGColor;
pathLayer.strokeColor = [UIColor redColor].CGColor;
pathLayer.lineWidth = 3.0f;
[self.containerView.layer addSublayer:pathLayer];
//add the ship
self.shipLayer = [CALayer layer];
self.shipLayer.frame = CGRectMake(, , , );
self.shipLayer.position = CGPointMake(, );
self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:self.shipLayer];
//set initial values
[self updateSliders];
} - (IBAction)updateSliders
{
CFTimeInterval timeOffset = self.timeOffsetSlider.value;
self.timeOffsetLabel.text = [NSString stringWithFormat:@"%0.2f", timeOffset];
float speed = self.speedSlider.value;
self.speedLabel.text = [NSString stringWithFormat:@"%0.2f", speed];
} - (IBAction)play
{
//create the keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.timeOffset = self.timeOffsetSlider.value;
animation.speed = self.speedSlider.value;
animation.duration = 1.0;
animation.path = self.bezierPath.CGPath;
animation.rotationMode = kCAAnimationRotateAuto;
animation.removedOnCompletion = NO;
[self.shipLayer addAnimation:animation forKey:@"slide"];
} @end

图9.3 测试时间偏移和速度的简单的应用程序

fillMode

对于beginTime非0的一段动画来说,会出现一个当动画添加到图层上但什么也没发生的状态。类似的,removeOnCompletion被设置为NO的动画将会在动画结束的时候仍然保持之前的状态。这就产生了一个问题,当动画开始之前和动画结束之后,被设置动画的属性将会是什么值呢?

一种可能是属性和动画没被添加之前保持一致,也就是在模型图层定义的值(见第七章“隐式动画”,模型图层和呈现图层的解释)。

另一种可能是保持动画开始之前那一帧,或者动画结束之后的那一帧。这就是所谓的填充,因为动画开始和结束的值用来填充开始之前和结束之后的时间。

这种行为就交给开发者了,它可以被CAMediaTimingfillMode来控制。fillMode是一个NSString类型,可以接受如下四种常量:

kCAFillModeForwards
kCAFillModeBackwards
kCAFillModeBoth
kCAFillModeRemoved

默认是kCAFillModeRemoved,当动画不再播放的时候就显示图层模型指定的值剩下的三种类型向前,向后或者即向前又向后去填充动画状态,使得动画在开始前或者结束后仍然保持开始和结束那一刻的值。

这就对避免在动画结束的时候急速返回提供另一种方案(见第八章)。但是记住了,当用它来解决这个问题的时候,需要把removeOnCompletion设置为NO,另外需要给动画添加一个非空的键,于是可以在不需要动画的时候把它从图层上移除。

层级关系时间

在第三章“图层几何学”中,你已经了解到每个图层是如何相对在图层树中的父图层定义它的坐标系的。动画时间和它类似,每个动画和图层在时间上都有它自己的层级概念,相对于它的父亲来测量。对图层调整时间将会影响到它本身和子图层的动画,但不会影响到父图层。另一个相似点是所有的动画都被按照层级组合(使用CAAnimationGroup实例)。

CALayer或者CAGroupAnimation调整durationrepeatCount/repeatDuration属性并不会影响到子动画。但是beginTimetimeOffsetspeed属性将会影响到子动画。然而在层级关系中,beginTime指定了父图层开始动画(或者组合关系中的父动画)和对象将要开始自己动画之间的偏移。类似的,调整CALayerCAGroupAnimationspeed属性将会对动画以及子动画速度应用一个缩放的因子。

全局时间和本地时间

CoreAnimation有一个全局时间的概念,也就是所谓的马赫时间(“马赫”实际上是iOS和Mac OS系统内核的命名)。马赫时间在设备上所有进程都是全局的--但是在不同设备上并不是全局的--不过这已经足够对动画的参考点提供便利了,你可以使用CACurrentMediaTime函数来访问马赫时间:

CFTimeInterval time = CACurrentMediaTime();

这个函数返回的值其实无关紧要(它返回了设备自从上次启动后的秒数,并不是你所关心的),它真实的作用在于对动画的时间测量提供了一个相对值。注意当设备休眠的时候马赫时间会暂停,也就是所有的CAAnimations(基于马赫时间)同样也会暂停。

因此马赫时间对长时间测量并不有用。比如用CACurrentMediaTime去更新一个实时闹钟并不明智。(可以用[NSDate date]代替,就像第三章例子所示)。

每个CALayerCAAnimation实例都有自己本地时间的概念,是根据父图层/动画层级关系中的beginTimetimeOffsetspeed属性计算。就和转换不同图层之间坐标关系一样,CALayer同样也提供了方法来转换不同图层之间的本地时间。如下:

- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;

当用来同步不同图层之间有不同的speedtimeOffsetbeginTime的动画,这些方法会很有用。

暂停,倒回和快进

设置动画的speed属性为0可以暂停动画,但在动画被添加到图层之后不太可能再修改它了,所以不能对正在进行的动画使用这个属性。给图层添加一个CAAnimation实际上是给动画对象做了一个不可改变的拷贝,所以对原始动画对象属性的改变对真实的动画并没有作用。相反,直接用-animationForKey:来检索图层正在进行的动画可以返回正确的动画对象,但是修改它的属性将会抛出异常。

如果移除图层正在进行的动画,图层将会急速返回动画之前的状态。但如果在动画移除之前拷贝呈现图层到模型图层,动画将会看起来暂停在那里。但是不好的地方在于之后就不能再恢复动画了。

一个简单的方法是可以利用CAMediaTiming来暂停图层本身。如果把图层的speed设置成0,它会暂停任何添加到图层上的动画。类似的,设置speed大于1.0将会快进,设置成一个负值将会倒回动画。

通过增加主窗口图层的speed,可以暂停整个应用程序的动画。这对UI自动化提供了好处,我们可以加速所有的视图动画来进行自动化测试(注意对于在主窗口之外的视图并不会被影响,比如UIAlertview)。可以在app delegate设置如下进行验证:

self.window.layer.speed = ;

你也可以通过这种方式来减速,但其实也可以在模拟器通过切换慢速动画来实现。

手动动画

timeOffset一个很有用的功能在于你可以它可以让你手动控制动画进程,通过设置speed为0,可以禁用动画的自动播放,然后来使用timeOffset来来回显示动画序列。这可以使得运用手势来手动控制动画变得很简单。

举个简单的例子:还是之前关门的动画,修改代码来用手势控制动画。我们给视图添加一个UIPanGestureRecognizer,然后用timeOffset左右摇晃。

因为在动画添加到图层之后不能再做修改了,我们来通过调整layer的 timeOffset 达到同样的效果(清单9.4)。

清单9.4 通过触摸手势手动控制动画

@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(, , , );
self.doorLayer.position = CGPointMake( - , );
self.doorLayer.anchorPoint = CGPointMake(, 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用来操作时间控制动画的机制。在下一章,我们将要接触缓冲,另一个用来使动画更加真实的操作时间的技术。

[iOS Animation]CALayer-图层时间的更多相关文章

  1. [iOS Animation]-CALayer 图层性能

    图层性能 要更快性能,也要做对正确的事情. ——Stephen R. Covey 在第14章『图像IO』讨论如何高效地载入和显示图像,通过视图来避免可能引起动画帧率下降的性能问题.在最后一章,我们将着 ...

  2. [iOS Animation]-CALayer 图层几何学

    图层几何学 不熟悉几何学的人就不要来这里了 --柏拉图学院入口的签名 在第二章里面,我们介绍了图层背后的图片,和一些控制图层坐标和旋转的属性.在这一章中,我们将要看一看图层内部是如何根据父图层和兄弟图 ...

  3. [iOS Animation]-CALayer 图层树

    图层的树状结构 巨妖有图层,洋葱也有图层,你有吗?我们都有图层 -- 史莱克 Core Animation其实是一个令人误解的命名.你可能认为它只是用来做动画的,但实际上它是从一个叫做Layer Ki ...

  4. [iOS Animation]-CALayer 显示动画

    显式动画 如果想让事情变得顺利,只有靠自己 -- 夏尔·纪尧姆 上一章介绍了隐式动画的概念.隐式动画是在iOS平台创建动态用户界面的一种直接方式,也是UIKit动画机制的基础,不过它并不能涵盖所有的动 ...

  5. [iOS Animation]-CALayer 缓冲

    缓冲 生活和艺术一样,最美的永远是曲线. -- 爱德华布尔沃 - 利顿 在第九章“图层时间”中,我们讨论了动画时间和CAMediaTiming协议.现在我们来看一下另一个和时间相关的机制--所谓的缓冲 ...

  6. iOS开发 - CALayer图层

    CALayer的基本使用 在iOS中.你能看得见摸得着的东西基本上都是UIView.比方一个button.一个文本标签.一个文本输入框.一个图标等等.这些都是UIView 事实上UIView之所以能显 ...

  7. [iOS Animation]-CALayer 专用图层

    专用图层 复杂的组织都是专门化的 Catharine R. Stimpson 到目前为止,我们已经探讨过CALayer类了,同时我们也了解到了一些非常有用的绘图和动画功能.但是Core Animati ...

  8. [iOS Animation]-CALayer 绘图效率

    绘图 不必要的效率考虑往往是性能问题的万恶之源. ——William Allan Wulf 在第12章『速度的曲率』我们学习如何用Instruments来诊断Core Animation性能问题.在构 ...

  9. [iOS Animation]-CALayer 性能优化

    性能优化 代码应该运行的尽量快,而不是更快 - 理查德 在第一和第二部分,我们了解了Core Animation提供的关于绘制和动画的一些特性.Core Animation功能和性能都非常强大,但如果 ...

随机推荐

  1. javaweb作業中的幾個要點

    1.DDoS攻击原理DDoS是指分布式拒绝服务(Distributed Denial of Service):试图通过恶意请求使系统或者网络超载进而无法继续提供服务.对于一个网站来说,这意味着,该网站 ...

  2. shell 空格问题

    1.定义变量时, =号的两边不可以留空格. eg: gender=femal------------right gender =femal-----------wrong gender= femal- ...

  3. 求N以内与N互质的数的和

    题目连接 /* 求所有小于N且与N不互质的数的和. 若:gcd(n,m)=1,那么gcd(n,n-m)=1; sum(n)=phi(n)*n/2; //sum(n)为小于n的所有与n互质的数的和 // ...

  4. img图片inline-block总结

    <div style="font-size:0;"> <img data-src="http://image.zhangxinxu.com/image/ ...

  5. Vsftp配置都没有问题 连接不上 530 Login incorrect 解决方法

    客户端输入正确的用户名和密码之后,却一直显示:530 Login incorrectLogin Failed后来发现在etc下面有个pam.d文件夹进去打开vsftpd这个文件, 发现里面对之前的用户 ...

  6. 区间gcd问题 HDU 5869 离线+树状数组

    题目大意:长度n的序列, m个询问区间[L, R], 问区间内的所有子段的不同GCD值有多少种. 子段就是表示是要连续的a[] 思路:固定右端点,预处理出所有的gcd,每次都和i-1的gcd比较,然后 ...

  7. mysql show processlist详解

    SHOW PROCESSLIST显示哪些线程正在运行.您也可以使用mysqladmin processlist语句得到此信息.如果您有SUPER权限,您可以看到所有线程.否则,您只能看到您自己的线程( ...

  8. 草,又学了个新命令,nc传文件。

    nc -l 5222 > aa nc 192.168.0.48 5222 < a http://www.linuxso.com/command/nc.html

  9. js中将 整数转成字符,,将unicode 编码后的字符还原出来的方法。

    一.将整数转成字符: String.fromCharCode(17496>>8,17496&0xFF,19504>>8,19504&0xFF,12848> ...

  10. HDU 2897 邂逅明下(巴什博奕变形)

    巴什博奕的变形,与以往巴什博奕不同的是,这里给出了上界和下界,原先是(1,m),现在是(p,q),但是原理还是一样的,解释如下: 假设先取者为A,后取者为B,初始状态下有石子n个,除最后一次外其他每次 ...