翻译Controlling Animation Timing(控制动画时机)

原文 http://ronnqvi.st/controlling-animation-timing

CABasicAnimation 和 CAKeyframeAnimation 的基类 CAAnimation 实现了一个叫 CAMediaTiming 的协议。它就是所有与timing相关的属性——duration, beginTime andrepeatCount——的来源。联合使用该协议所定义的八个属性可以刚好控制 timing。介绍每个属性的文档只有几句话,所以你可以很快地看完。实际上,这种方式也比看这篇文章要快得多,但是我觉得 timing 还是需要更形象化地阐释。

形象化 CAMediaTiming

我用一个从橙色到蓝色的颜色动画来表现不同 timing 相关属性,既有用到单个属性也有同时使用多个属性。方块显示了动画从开始到结束(从橙色到蓝色)的过程,标记以一秒为间隔。你可以在时间轴的任何一点看到动画进行若干秒后的颜色。例如,下面是可视化的 duration。

这里的 duration 被设置为1.5秒,所以动画完全变为蓝色用了1.5秒。

设置 duration 1.5秒

默认的CAAnimation在结束后会被从 layer 上移除。正如上图所示。一旦动画到达 final value 就被从layer上移除。如果 layer 的颜色本来是橙色,那么就将回到橙色。图中的 layer 是白色的,所以你可以看到在动画被加到 layer 上之后的2秒它又变回白色。

如果我们也将动画的开始时间可视化,那就会更有意义。

设置 duration 1.5秒和开始时间1.0秒

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]

填充模式可以用来在动画开始前展示 fromValue

属性 autoreverses 可以使动画完成后,执行相反的动画回到初始值。这就是说,一共花费两倍的 duration。

autoreverses 使动画到达最终值之后回到初始值

相比之下 repeatCount 可以将动画重复两次(如下)或者更多次(是小数,例如1.5表示执行一次半)。一旦动画到达最终值,会立即跳回初始值并开始新的一次动画。

repeat count 使得动画执行一次以上

类似于 repeat count,但是很少用到的,就是 repeat duration。它会简单地根据给出的时间(下图显示的是2秒)循环动画。传递一个比动画时间短的 repeat duration 会导致动画提前结束(在 repeat duration 结束之后)。

动画按照 repeat duration 循环

这些可以被联合使用以实现在一定的次数或时间内往返循环。

这些可以被联合使用

其中最有意思的与 timing 相关的属性是 speed 。通过设置 duration 为3而 speed 为2,动画将只执行1.5秒因为它的速度是原来的两倍[2]

值为2的 speed 是的动画速度翻倍,所以3秒的动画只花费了1.5秒

如果只需要配置一个动画,那么你可能可以自己划分 beginTime 和 duration 以获得相同的结果,但是 speed的作用来自两个方面:

  1. 动画的 speed 是分层的;
  2. 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

同时使用speedtimeOffset可以控制动画的当前“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 的背景颜色。

layer 的颜色随着 slider 的值改变

下卡刷新

你也可以使用其他的机制像滚动事件来控制动画的 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找到。


  1. 你可以使用 fillMode 属性来向前填充,并使动画在结束后继续显示toValue的效果,但是动画结束后将被移除,仅仅设置 fillMode 是不够的。你可以通过设置removedOnCompletion = NO来保留动画。只要记住动画只影响视图(展示的图层),所以完成这两个操作,你就用到了 model 和 view 的区别。同样的数据(被赋予动画的属性)存在于两处(属性值和屏幕上展示的效果)但他们并非同步。

  2. 有趣的现象:负的 speed(如-1)会导致动画在给定时间内逆向执行。

推荐阅读更多精彩内容

  • 这是我第一次翻译国外大神的文章。为了行文通顺,某些地方没有完全遵照原文。末尾附有自己的一些私货。原文链接如下:ht...
    我们是斗士阅读 187评论 0 0
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 3,367评论 4 20
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 2,544评论 2 10
  • 前言 本文要探讨的是CoreAnimation框架是如何来控制时间的。 CAMediaTiming协议 动画所有跟...
    hehc08阅读 131评论 0 1
  • CAAnimation CAAnimation 是一个抽像类。CAAnimation 也派生出了很多子类,我们使用...
    谢谢生活阅读 424评论 0 8