iOS核心动画高级技巧--(九)图层时间

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

CAMediaTiming 协议

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

持续和重复

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

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

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

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

- (void)viewDidLoad {
    [super viewDidLoad];
    
    CALayer *doorLayer = [CALayer layer];
    doorLayer.frame = CGRectMake(0, 0, 128, 256);
    doorLayer.position = CGPointMake(150 - 64, 150);
    doorLayer.anchorPoint = CGPointMake(0, 0.5);
    doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Spaceship"].CGImage;
    [self.containerView.layer addSublayer:doorLayer];
    
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -1.0 / 500.0;
    self.containerView.layer.sublayerTransform = perspective;
    
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"transform.rotation.y";
    animation.toValue = @(-M_PI_2);
    animation.duration = 2.0;
    animation.repeatCount = INFINITY;
    animation.autoreverses = true;
    [doorLayer addAnimation:animation forKey:nil];
}
实现门的摇摆效果.gif
相对时间

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

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

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

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

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

设置speedtimeOffset滑块到随意的 值,然后点击播放来观察效果

#import "ViewController.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIView *containerView;
@property (weak, nonatomic) IBOutlet UILabel *speedLabel;

@property (weak, nonatomic) IBOutlet UILabel *timeOffsetLabel;

@property (weak, nonatomic) IBOutlet UISlider *speedSlider;

@property (weak, nonatomic) IBOutlet UISlider *timeOffsetSlider;

@property (nonatomic, strong) UIBezierPath *bezierPath;
@property (nonatomic, strong) CALayer *shipLayer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.bezierPath = [[UIBezierPath alloc]init];
    [self.bezierPath moveToPoint:CGPointMake(0, 150)];
    [self.bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(200, 200) controlPoint2:CGPointMake(150, 100)];
    
    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];
    
    self.shipLayer = [CALayer layer];
    self.shipLayer.frame = CGRectMake(0, 0, 64, 64);
    self.shipLayer.position = CGPointMake(0, 150);
    self.shipLayer.contents = (__bridge id)[UIImage imageNamed:@"ship"].CGImage;
    [self.containerView.layer addSublayer:self.shipLayer];
    
    [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:(id)sender {
    
    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 = false;
    [self.shipLayer addAnimation:animation forKey:@"slide"];
    
}
测试 timeOffset 和 speed 属性.gif
fillMode

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

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

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

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

kCAFillModeForwards
kCAFillModeBackwards
kCAFillModeBoth
kCAFillModeRemoved

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

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

层级关系时间

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

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

全局时间和本地时间

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

 CFTimeInterval time = CACurrentMediaTime();

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

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

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

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

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

暂停,倒回和快进

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

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

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

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

 self.window.layer.speed = 100;

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

手动动画

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

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

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

#import "ViewController.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *containerView;
@property (nonatomic, strong) CALayer *doorLayer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.doorLayer = [CALayer layer];
    self.doorLayer.frame = CGRectMake(0, 0, self.containerView.bounds.size.width, self.containerView.bounds.size.height);;
    self.doorLayer.position = CGPointMake(0, self.containerView.bounds.size.height * 0.5);
    self.doorLayer.anchorPoint = CGPointMake(0, 0.5);
    self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Spaceship"].CGImage;
    [self.containerView.layer addSublayer:self.doorLayer];
    
    
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -0.1 / 500.0;
    self.containerView.layer.sublayerTransform = perspective;
    
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(pan:)];
    
    [self.view addGestureRecognizer:pan];
    
    self.doorLayer.speed = 0.0;
    
    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{
    CGFloat x = [pan translationInView:self.view].x;
    x /= 200.0f;
    CFTimeInterval timeOffset = self.doorLayer.timeOffset;
    timeOffset = MIN(0.999, MAX(0.0, timeOffset - x));
    self.doorLayer.timeOffset = timeOffset;
    [pan setTranslation:CGPointZero inView:self.view];
    
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
@end
通过触摸手势手动控制动画.gif

这其实是个小诡计,也许相对于设置个动画然后每次显示一帧而言,用移动手势来 直接设置门的 transform 会更简单。

总结

在这一章,我们了解了 CAMediaTiming协议,以及Core Animation用来操作时间 控制动画的机制。在下一章,我们将要接触 缓冲 ,另一个用来使动画更加真实的 操作时间的技术。

iOS核心动画高级技巧--目录

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容