Animation Tips

前不久看完 Ray 的 《iOS Animations by Tutorials》,加上最近写动画的小心得,总结了几个 Tips。并不包含很难的动画效果,都是一些很基础的,但是有时候又不容易想到的实现方式(或者只是我没想到而已…)。这个 Tips 会持续更新。

因为这里的动画都是我从项目中抽出来的,所以对大家来说,这可能不是最佳实践。具体需求还是要具体分析,这里仅仅提供一种思路。

完整代码下载:

https://github.com/saitjr/STAnimationTips.git

环境信息:

Mac OS X 10.11.3

Xcode 7.2.1

iOS 9.2

一、多个视图的动画有序且无限次执行

动画循环周期如下:

(黑色放大 —— 橘色旋转一周 —— 黑色缩小)

对我的需求来说,最简单的做法是:黑色做动画,橘色等待;橘色动画完成后,黑色动画 reverse。

假设黑色动画时长是 scaleAnimationDuration,橘色动画的时长是 rotateAnimationDuration

  1. 橘色动画第一次执行,要先等待黑色动画执行完。可以用?beginTime?来达到效果;
  2. 黑色动画先放大,后缩小,可以直接使用?autoreverses = true?;
  3. 我当时遇到的最大的问题,是如何设置等待,因为这个动画中,出现最多的就是等待。

后来发现并不难,等待可以通过在当前的?scaleAnimation?和?rotateAnimation?外面包一个?groupAnimation?来达到效果。

如图(以?scaleAnimation?为例):

如图,在剩下的?groupDuration - scaleDuration?中,将没有动画效果,从而达到等待的目的。接下来便是计算时间。

依然以?scaleAnimation?为例,有几个属性需要注意(rotate 动画同理):

  1. scale 的?duration
  2. scale group 的?duration
  3. 谁来设置?autoreverses
  4. 谁来设置?repeatCount

先说?autoreverses,如果是 scale 设置,那么执行完放大动画后,并不会等待,而是直接反向,所以,这个属性不能由 scale 设置,而应该由 scale group 来设置。

然后?repeatCount?和?autoreverses?原因大致类似,group 不重复,scale 重复再多也没用。

scale 的?duration?是可以随心所欲的。但是 scale group 的?duration?就需要注意了。

如下图,我大致整理了 scale 和 rotate 之间的关系。

红色为自定义的已知量,黑色为 scale 的相关数据,黄色为 rotate 的相关数据。

来说一下最后 rotate 的时间超出周期范围(虚线框)的原因:beginTime?只在第一次执行动画的时候有效,所以 rotate 的等待时间应该要把【缩小+放大】的时间让出来。即 scale duration 的两倍。

这一步要仔细思考下,并不难。可以得到以下公式:

let scaleAnimationDuration: NSTimeInterval = 1.0
let rotateAnimationDuration: NSTimeInterval = 3.0

let scaleGroupAnimationDuration = rotateAnimationDuration / 2.0 + scaleAnimationDuration
let rotateGroupAnimationDuration = rotateAnimationDuration + scaleAnimationDuration * 2.0

好了,最繁琐的地方已经过了,下面就是写代码了。

// 执行旋转的橘色视图
private func orangeViewAnimation() {
    let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
    rotateAnimation.fromValue = -M_PI * 2
    rotateAnimation.duration = rotateAnimationDuration

    let groupAnimation = CAAnimationGroup()
    groupAnimation.beginTime = CACurrentMediaTime() + scaleAnimationDuration
    groupAnimation.duration = rotateAnimationDuration + scaleAnimationDuration * 2
    groupAnimation.repeatCount = Float.infinity
    groupAnimation.animations = [rotateAnimation]
    orangeView.layer.addAnimation(groupAnimation, forKey: "rotateAnimation")
}

// 执行缩放的黑色视图
private func blackViewAnimation() {
    let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
    scaleAnimation.fromValue = 1
    scaleAnimation.toValue = 0.5
    // 因为在放大以后,要停留在放大的状态,等待橘色视图旋转,所以使用 fillMode 和 removedOnCompletion 两个属性来将动画停留在最终的状态
    scaleAnimation.fillMode = kCAFillModeForwards
    scaleAnimation.removedOnCompletion = false
    scaleAnimation.duration = scaleAnimationDuration

    let groupAnimation = CAAnimationGroup()
    groupAnimation.duration = scaleAnimationDuration + rotateAnimationDuration / 2.0
    groupAnimation.repeatCount = Float.infinity
    groupAnimation.autoreverses = true
    groupAnimation.animations = [scaleAnimation]
    blackView.layer.addAnimation(groupAnimation, forKey: "scaleAnimation")
}

第一个 Tips 就到这里。完整代码可以查看:

https://github.com/saitjr/STAnimationTips/blob/master/STAnimationTips/ViewControllers/GroupAnimationVC.swift

 

二、程序挂起动画被移除,程序唤醒时重新添加的处理

首先来看下动画移除导致换形时没有动画的效果:

这问题是以前在写验证码按钮的时候遇到的,直接使用了 UIView 的 animation,挂起后唤醒,发现原本一分钟的验证码动画,居然结束了…

原因就是在程序挂起的时候,所有的动画都被移除了,唤醒的时候当然就没有了动画,从而又回到了最初的状态。

原因找到就很简单了,解决方案是在挂起时,保存当前的动画,唤醒以后,重新 add animation。

首先,需要接收两个通知 UIApplicationWillEnterForegroundNotification ?和?UIApplicationDidEnterBackgroundNotification

func applicationWillEnterForegroundNotification() {
    if let rotateAnimation = rotateAnimation {
        label.layer.addAnimation(rotateAnimation, forKey: RotateAnimationKey)
    }
    rotateAnimation = nil
}

func applicationDidEnterBackgroundNotification() {
    rotateAnimation = label.layer.animationForKey(RotateAnimationKey)
}

以上代码很简单,在已经挂起时,通过 animation key 获得旋转动画,赋值给成员变量 rotateAnimtion。这样就保留了当前动画。在即将唤醒时,又将 rotateAnimation 重新加到 label.layer 上。

除此之外,还可以记录一下当前的动画状态,即在挂起时暂停 layer 状态,唤醒后继续。对于下面的代码,也可以封装为 extension。

private func pauseLayer() {
    let pausedTime = label.layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
    label.layer.speed = 0.0
    label.layer.timeOffset = pausedTime
}

private func resumeLayer() {
    let pausedTime = label.layer.timeOffset
    label.layer.speed = 1.0;
    label.layer.timeOffset = 0.0;
    label.layer.beginTime = 0.0;
    let timeSincePause = label.layer.convertTime(CACurrentMediaTime(), fromLayer: nil) - pausedTime
    label.layer.beginTime = timeSincePause;
}

以上的 暂停 和 继续 两个方法,分别在 挂起 和 唤醒 的时候调用。

完成以后的效果如下:

详细代码见:

https://github.com/saitjr/STAnimationTips/blob/master/STAnimationTips/ViewControllers/EnterBackgroundVC.swift

 

 

《Animation Tips》有5个想法

发表评论

电子邮件地址不会被公开。