原文 http://ronnqvi.st/controlling-animation-timing
CABasicAnimation 和 CAKeyframeAnimation 的基类 CAAnimation 实现了一个叫 CAMediaTiming 的协议。它就是所有与timing相关的属性——duration
, beginTime
andrepeatCount
——的来源。联合使用该协议所定义的八个属性可以刚好控制 timing。介绍每个属性的文档只有几句话,所以你可以很快地看完。实际上,这种方式也比看这篇文章要快得多,但是我觉得 timing 还是需要更形象化地阐释。
形象化 CAMediaTiming
我用一个从橙色到蓝色的颜色动画来表现不同 timing 相关属性,既有用到单个属性也有同时使用多个属性。方块显示了动画从开始到结束(从橙色到蓝色)的过程,标记以一秒为间隔。你可以在时间轴的任何一点看到动画进行若干秒后的颜色。例如,下面是可视化的 duration。
这里的 duration 被设置为1.5秒,所以动画完全变为蓝色用了1.5秒。
默认的CAAnimation
在结束后会被从 layer 上移除。正如上图所示。一旦动画到达 final value 就被从layer上移除。如果 layer 的颜色本来是橙色,那么就将回到橙色。图中的 layer 是白色的,所以你可以看到在动画被加到 layer 上之后的2秒它又变回白色。
如果我们也将动画的开始时间可视化,那就会更有意义。
duration 被设为1.5秒,开始时间被设为当前时间(CACurrentMediaTime())加1秒,所以动画在2.5秒后结束。动画被加到layer上之后,动画花费1秒时间启动。耗时1+1.5=2.5(原文:“The rest is just 1+1.5=2.5.”)。
为了在动画开始前显示fromValue
,你可以将动画设为向后填充。方法是将 fillMode 设为 kCAFillModeBackwards[1]。
属性 autoreverses 可以使动画完成后,执行相反的动画回到初始值。这就是说,一共花费两倍的 duration。
相比之下 repeatCount 可以将动画重复两次(如下)或者更多次(是小数,例如1.5表示执行一次半)。一旦动画到达最终值,会立即跳回初始值并开始新的一次动画。
类似于 repeat count,但是很少用到的,就是 repeat duration。它会简单地根据给出的时间(下图显示的是2秒)循环动画。传递一个比动画时间短的 repeat duration 会导致动画提前结束(在 repeat duration 结束之后)。
这些可以被联合使用以实现在一定的次数或时间内往返循环。
其中最有意思的与 timing 相关的属性是 speed 。通过设置 duration 为3而 speed 为2,动画将只执行1.5秒因为它的速度是原来的两倍[2]。
如果只需要配置一个动画,那么你可能可以自己划分 beginTime 和 duration 以获得相同的结果,但是 speed
的作用来自两个方面:
- 动画的 speed 是分层的;
- CAAnimation 不是唯一实现 CAMediaTiming 的类。
分层的 speed
假设一个动画的 speed 是1.5,它所在的 animation group 的 speed 是2,那么实际速度是原来的3倍。
CAMediaTiming 的其它实现
CAMediaTiming 是 CAAnimation 实现的协议,但是实现了同样的协议还有 CALayer,所有 Core Animation 中 layer 的基类。这意味着你可以设 layer 的 speed 为2.0,然后所有加到它上面的动画执行的速度都会翻倍。这对 timing 的层级也有效,speed 为3的动画在 speed 为0.5的 layer 上的执行速度是原来的1.5倍。
把动画或者 layer 的 speed 置为0也能用来暂停动画。加上timeOffset
可以从一个外部装置,比如下文将要介绍的 slider,控制动画的进度。
首先timeOffset
是个奇怪的属性。顾名思义它可以偏移用来计算动画状态的时间。这个最好用视图说明。下图是时长3秒并偏移1秒的动画。
动画在由橙色到蓝色转变的1秒后开始,并进行了剩下的2秒直到完全变蓝。然后它跳回到完全的橙色并进行颜色转变的第1秒。就仿佛我们把第1秒动画剪下,再把它移到最后。
这个属性本身几乎没有作用,但是它和暂停动画(speed = 0)可以被一起使用来控制“current time”。暂停动画会固定在动画的第一帧。如果你观察偏移动画(上图)的第一个颜色,你会看到颜色转变过程的进行1秒后的颜色。通过设置另一个偏移时间,你可以去到那一时刻的状态。
如果你想要更多关于 timing 的图解,我做了一份小抄。
控制动画的 timing
同时使用speed
和timeOffset
可以控制动画的当前“time”。代码很简洁,但是概念比较难以理解(我希望以下的图解能对这部分有所帮助)。方便起见,我把 duration 设为1秒。这时因为时间偏移量是一个绝对值。这么做意味着0.0的时间偏移量代表动画进度的0%(开始),1.0的时间偏移量代表动画进度的100%(结束)。
Slider
开头很简单,我们为 layer 的背景颜色创建并加上一个基础的动画。我们将 layer 的 speed 设为0以暂停动画。
CABasicAnimation *changeColor = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
changeColor.fromValue = (id)[UIColor orangeColor].CGColor;
changeColor.toValue = (id)[UIColor blueColor].CGColor;
changeColor.duration = 1.0; // 方便起见
[self.myLayer addAnimation:changeColor
forKey:@"Change color"];
self.myLayer.speed = 0.0; // 暂停动画
然后在 slider 被拖动的方法中,把当前 slider 的值(也是被配置在0到1之间)设为 layer 的时间偏移量:
- (IBAction)sliderChanged:(UISlider *)sender {
self.myLayer.timeOffset = sender.value; // 更新 "current time"
}
效果就是当我们拖动 slider 时,动画的值改变并更新 layer 的背景颜色。
下卡刷新
你也可以使用其他的机制像滚动事件来控制动画的 timing。这可以用来创建一个定制化下拉刷新动画,这个动画可以随着用户的拉动进行,直到阈值才开始加载新的数据。在我的例子中,滚动事件控制的动画是描绘轨迹(shape layer 属性strokeEnd
的动画)并当触及阈值时它将开始执行另一个动画以示正在加载新数据。
我们用滚动视图被拖动的量代替 slider 控制 timing。这个值将会以点为单位,所以要作为时间偏移量来使用就需要被标准化,这是合理的因为我们需要一个拖动的阈值来知道何时加载更多数据。处理下拉滚动视图的代码如下
- (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];
结果是,当你下拉时你有一个直观的显示动画进度的装置(即拉地越远,单词“Load”就写的越完整)。如果你又向上拉动,动画就就倒退。
一旦超过阈值,就开始执行实际的加载动画和数据加载。我的代码就这么做的。我设置了避免加载中在 scrollViewDidScroll
对 timeOffset 的修改,开始加载动画以及调整 content inset 来避免向上滚动时覆盖了加载的进度图。
self.isLoading = YES;
// 开始记载动画
[self.loadingShape addAnimation:[self loadingAnimation]
forKey:@"Write that word"];
CGFloat contentInset = self.collectionView.contentInset.top;
CGFloat indicatorHeight = CGRectGetHeight(self.loadingIndicator.frame);
// 在顶部插入一定空间以保持动画在屏幕的位置
self.collectionView.contentInset = UIEdgeInsetsMake(contentInset+indicatorHeight, 0, 0, 0);
self.collectionView.scrollEnabled = NO; // 禁止继续滚动
[self loadMoreDataWithAnimation:^{
// 在加载动画期间 (插入新cell时)
self.collectionView.contentInset = UIEdgeInsetsMake(contentInset, 0, 0, 0);
self.loadingIndicator.alpha = 0.0;
} completion:^{
// 重置
[self.loadingShape removeAllAnimations];
self.loadingIndicator.alpha = 1.0;
self.collectionView.scrollEnabled = YES;
self.pullToRefreshShape.timeOffset = 0.0; // 回到最初
self.isLoading = NO;
}];
滚动超过阈值的最终效果如下
像那样控制动画可以给你的应用略微增色,同时你可以如此添加高级的 group animations 而不用写一大堆代码。我没在这里展示,但是你可以用 gesture recognizer 或者其它直接控制机制实现同样的控件。
以上展示的下拉刷新的工程可以在GitHub找到。
-
你可以使用 fillMode 属性来向前填充,并使动画在结束后继续显示
toValue
的效果,但是动画结束后将被移除,仅仅设置 fillMode 是不够的。你可以通过设置removedOnCompletion = NO
来保留动画。只要记住动画只影响视图(展示的图层),所以完成这两个操作,你就用到了 model 和 view 的区别。同样的数据(被赋予动画的属性)存在于两处(属性值和屏幕上展示的效果)但他们并非同步。 ↩ -
有趣的现象:负的 speed(如-1)会导致动画在给定时间内逆向执行。 ↩