原文:Controlling Animation Timing
感谢翻译小组成员@answer-huang(博客)热心翻译。本篇文章是我们每周推荐优秀国外的技术类文章的其中一篇。如果您有不错的原创或译文,欢迎提交给我们,更欢迎其他朋友加入我们的翻译小组(联系qq:2408167315)。
有一种通过CAAnimation实现的协议叫做CAMediaTiming,也就是CABasicAnimation和CAKeyframeAnimation的基类(指CAAnimation)。像duration,beginTime和repeatCount这些时间相关的属性都在这个类中。大体而言,协议中定义了8个属性,这些属性通过一些方式结合在一起,准确的控制着时间。文档中每个属性只有几句话,所以很有可能在看这篇文章之前你都已经读过了,但是我觉得使用可视化的图形能更好的解释时间。
可视化的CAMediaTiming
为了显示相关属性的不同时间,无论是他们自己还是混合状态,我都会动态的将橙色变为蓝色。下面的块状显示了从开始到结束的动画过程,时间线上每一个标志代表一秒钟。你可以看到时间线上的任意一点,当前颜色即表示动画中的当前时间。比如,duration像下面一样可视。
duration设置为1.5秒,所以动画全程花费了1.5秒变为蓝色。
Figure 1.设置duration为1.5秒
一旦动画完成后,CAAnimation默认从layer上移除。这从上面的图形中也表现出来了。一旦动画达到最终值,它会从layer上移除。如果layer的颜色是橙色(开始的颜色),那么颜色又会便会橙色。在这个可视化界面中,layer的颜色是白色,所以你也可以看到动画加入layer后的两秒钟又会变白,因为那是动画已经结束了。
我们也形象的描述一下动画的beginTime,这样让人更容易理解。
Figure 2.设置duration为1.5秒,开始时间为1.0秒
durations设置为1.5秒了,开始时间设为当前时间。(CACurrentMediaTime())加一秒,所以动画在2.5s后结束。动画加入到layer上之后,花费一秒钟时间来启动并呈现出来。结果是1+1.5=2.5
为了让动画从fromValue开始显示,你可以将动画设置为fill backwards。我们可以通过设置fillMode为kCAFillModeBackwards。
Figure 3.Fill mode可以让动画从fromValue开始显示
autoreverses属性可以产生从初始值到最终值,并反过来回到初始值的动画。这意味着动画发生了两次。
Figure 4.Autoreverses使得动画结束后又回到起始状态
和repeatCount比起来,repeatCount可以将动画重复两次(如下所示)或者任意次(你甚至可以使用像1.5这样的分数来完成一个半动画)。一旦动画达到它的最终值,他就会立马跳回到初始值并重新开始
Figure 5.Repeat count可以让动画运行超过一次
和repeat count类似,但很少用到的就是repeat duration了。它将会根据给定的一个duration简单的重复动画(如下2秒所示)。经过一个repeat duration时,如果它小于动画的duration,那么动画就会提前结束(repeat duration之后结束)
Figure 6.Repeat duration会让动画根据一个给定duration重复
这些都可以组合起来将一个反转动画重复多次或在一个给定的duration间重复。
Figure 7.组合
一个跟时间相关有趣的属性是speed。通过设置duration为3秒,但是speed为2,动画快速的执行了1.5秒,因为它的速度是之前的两倍。
Figure 8.速度为2时,动画执行速度是之前的两倍,所以3秒的动画只需要执行1.5秒
如果只是配置了一个简单的动画,那么你也可以分开使用beginTime和duration以达到相同的效果。但是使用speed属性的优点在于这两个事实:
1.动画的speed是分等级的。(hierarchical)
2.CAAnimation不是唯一一个实现CAMediaTiming的类。
Hierarchical speed
速度为2的动画组有一部分动画的速度为1.5,那么这个动画就是3倍于正常速度。
CAMediaTiming的其他实现
CAMediaTiming是CAAnimation实现的一个协议,但是CALayer(所有Core Animationlayers的基类)也实现了相同的协议,这就意味着你可以设置layer的speed为2.0,这样,所有加入到layer的动画运行都要快两倍。同样的,如果一个速度为3的动画加到一个速度为0.5的layer上,这个动画最终将会以1.5倍的常速运行。
为了控制动画或layer的速度,通常还可以设置speed为0,从而暂停动画。和timeOffset结合在一起时,可以通过像slider类似的控件控制动画,我们在下文中也会讲到。
刚开始timeOffset属性是非常奇怪的。正如名字所示那样,它对时间进行偏移(offset),从而计算出动画的状态。如下图所示。duration为3秒,offset为1秒的动画。
Figure 9.你可以offset整个动画,但是动画所有部分任然会执行
动画开始运行时跳过第一秒进入从橙色到蓝色的过度,直接运行剩下的两秒,直到完全变蓝。然后动画直接跳回完全橙色的时候,并完成第一秒的颜色转换。这看起来有点像我们把动画的第一秒切下来,然后放到最后。
这个属性很少用,但是它可以和一个暂停的动画(speed=0)结合在一起控制’current time’。一个暂停的动画停留在第一帧。如果你观察offset动画每次的第一个颜色,你可以看到它的颜色值一秒就进入颜色转换。将time offset设置为其他值,你可以让那段时间进入转换。
控制动画时间
同时使用speed和timeOffset可以控制动画的当前时间。这几乎不会涉及到什么代码,但是概念却比较难以理解(我希望插图能有所帮助)。为了方便,我将动画的duration设为1.0。因为time offset是绝对值。这样做就意味着当time offset为0.0时,此时就是动画的0%处(动画开始),time offset为1.0时,就是动画的100%处(动画结束)。
Slider
以一个简单的例子开始,我们为一个layer的背景色创建一个基本的动画并增加到layer上。将layer的速度设为0以暂停动画。
- CABasicAnimation *changeColor =
- [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
- changeColor.fromValue = (id)[UIColor orangeColor].CGColor;
- changeColor.toValue = (id)[UIColor blueColor].CGColor;
- changeColor.duration = 1.0; // For convenience
- [self.myLayer addAnimation:changeColor
- forKey:@"Change color"];
- self.myLayer.speed = 0.0; // Pause the animation
当拖动slider时,在action方法中将slider的值(将slider的值设为0-1)设为layer的time offset
- - (IBAction)sliderChanged:(UISlider *)sender {
- self.myLayer.timeOffset = sender.value; // Update "current time"
- }
下面给出了效果图:当拖动slider时动画的值便会改变,并且更新layer的背景色
Figure 10.layer的颜色随着slider的值改变而改变
拉动刷新
你还可以使用另一种机制来控制动画时间:像scroll事件。这样可以创建一个自定义下拉刷新动画,当达到加载新数据的临界值之前,用户的拖拉操作都会产生一个动画。在我的这个例子中,scroll事件控制着一个路径画笔的动画。当达到临界值时,将会启动另一种动画暗示新数据正在加载中。
这次我们使用scroll view向下拖动的总量来控制时间,为了标准化,这个值将会以points的形式,这样是非常好的,因为我们需要设置一个拖动的临界值来判断何时开始加载更多的数据。像下面那样处理代码。
- - (void)scrollViewDidScroll:(UIScrollView *)scrollView
- {
- CGFloat offset =
- scrollView.contentOffset.y+scrollView.contentInset.top;
- if (offset <= 0.0 && !self.isLoading) {
- CGFloat startLoadingThreshold = 60.0;
- CGFloat fractionDragged = -offset/startLoadingThreshold;
- self.pullToRefreshShape.timeOffset = MAX(0.0, fractionDragged);
- if (fractionDragged >= 1.0) {
- [self startLoading];
- }
- }
- }
像这样控制动画:
- CABasicAnimation *writeText =
- [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
- writeText.fromValue = @0;
- writeText.toValue = @1;
- CABasicAnimation *move =
- [CABasicAnimation animationWithKeyPath:@"position.y"];
- move.byValue = @(-22);
- move.toValue = @0;
- CAAnimationGroup *group = [CAAnimationGroup animation];
- group.duration = 1.0;
- group.animations = @[writeText, move];
结果是:下拉视图时,可以直接控制动画的进度。如果你抬起手动画将会退回去。
Figure 11.使用scroll事件直接控制拖动刷新
一旦超过临界值便真正开始加载动画以及加载更多的数据。我在scrollViewDidScroll:中通过设置isLoading防止timeOffset,开始加载动画并调整content inset以防止scrollview向上滚动时传递loading的指示。
- self.isLoading = YES;
- // start the loading animation
- [self.loadingShape addAnimation:[self loadingAnimation]
- forKey:@"Write that word"];
- CGFloat contentInset = self.collectionView.contentInset.top;
- CGFloat indicatorHeight = CGRectGetHeight(self.loadingIndicator.frame);
- // inset the top to keep the loading indicator on screen
- self.collectionView.contentInset =
- UIEdgeInsetsMake(contentInset+indicatorHeight, 0, 0, 0);
- self.collectionView.scrollEnabled = NO; // no further scrolling
- [self loadMoreDataWithAnimation:^{
- // during the reload animation (where new cells are inserted)
- self.collectionView.contentInset =
- UIEdgeInsetsMake(contentInset, 0, 0, 0);
- self.loadingIndicator.alpha = 0.0;
- } completion:^{
- // reset everything
- [self.loadingShape removeAllAnimations];
- self.loadingIndicator.alpha = 1.0;
- self.collectionView.scrollEnabled = YES;
- self.pullToRefreshShape.timeOffset = 0.0; // back to the start
- self.isLoading = NO;
- }];
最终,当你向下拖动超过临界值时便会像这样:
Figure 12.拖动刷新和加载动画
为你的应用增加像这样的动画可以很好的丰富应用。并且你可以像这样无需大量代码而做出高级的动画。这里我并没有展示,但是你可以通过类似的,或手势识别或任何其他直接控制的机制。
示例代码可在GitHub上下载:
https://github.com/d-ronnqvist/blogpost-codesample-PullToRefresh