翻译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)会导致动画在给定时间内逆向执行。

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

推荐阅读更多精彩内容

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