iOS動畫-定時器動畫

fi867989 8年前發布 | 6K 次閱讀 iOS開發 移動開發

前言

任何動畫離不開一個重要的概念——時間, CoreAnimation 動畫創建后在動畫后續的不同時間點渲染了不同的圖像幀,使值改變前后生成一個過渡的流暢動畫

定時器的作用類似于 CoreAnimation 的操作,在定時器啟動后對應的時間點插入回調任務。如果每個回調任務之間的間隔足夠短,并在每個任務之間繪制圖案,就能達成自制動畫的效果。本文分別使用 NSTimer 和 CADisplayLink 兩個定時器來實現不同的動畫

關于定時器

iOS開發中有三種常見的定時器: NSTimer 、 CADisplayLink 以及 GCD Timer ,前兩個定時器在使用時要加入到某個運行的 RunLoop 當中,在每個回調時間點會喚醒線程,執行任務。 GCD Timer 依賴于派發線程,從準確度上而言要強于前兩者,但是本文并不涉及這種定時器的使用。

  • NSTimer

    NSTimer 是最常使用的定時器,啟動后會添加到 RunLoop 的定時器源中,然后在后續設置好的時間點喚醒 RunLoop 執行回調。如果在回調時間點遇到了CPU正在執行大量指令時,普遍認為該時間點的任務會被跳過,但實際效果可能與認識有偏差。在iOS10中, NSTimer 還存在著不能正常釋放引用對象的bug。詳細請參考下面的文章鏈接

  • CADisplayLink

    CADisplayLink 比較特殊,它的回調頻率保持 16.67ms 一次,與屏幕的刷新頻率一樣。與 NSTimer 相似的地方在于兩者都會在回調時喚醒所在的 RunLoop ,但 CADisplayLink 會不斷處理來自內核的信號,可能導致大量的不必要的資源損耗,因此使用 CADisplayLink 的時間應當保證盡可能的短暫,具體參考下面的文章鏈接

兩個定時器都能協助我們很好的實現動畫效果,更詳細的介紹參考iOS10定時消息的改動。下面放上本篇博客的動畫效果

聲波動畫

聲波動畫參照自支付鴇的 咻一咻 功能,現在的版本貌似取消了(ps:吐槽一句支付鴇更新之后看個余額都費勁)。從gif圖中不難看到動畫是由多個圖層縮放消失疊加在一起實現的,其中單個縮放消失的動畫在我上一篇按鈕動畫中有提到,基于上篇文章的動畫,筆者在點擊按鈕的時候添加了一個 NSTimer 用來保證每隔一段時間新增一個動畫圖層。理論上來說可以將這些動畫的 CAShapLayer 保存起來重復使用,但demo中偷懶,每次回調創建新的圖層進行動畫

let twinkleInteval = 0.6
@IBAction func signIn(_ sender: UIButton) {
    self.timer = Timer(timeInterval: twinkleInteval, repeats: true, block: { [unowned self] (timer) in
        let frame = self.signInButton.frame
        let layer = self.roundLayer(with: frame)
        self.view.layer.insertSublayer(layer, below: self.signInButton.layer)
            self.twinkle(layer: layer)
    })
}

func twinkle(layer: CAShapeLayer) { let scale = CABasicAnimation(keyPath: "transform") scale.toValue = NSValue(caTransform3D: CATransform3DMakeScale(4, 4, 1))

let opacity = CABasicAnimation(keyPath: "opacity")
opacity.fromValue = NSNumber(floatLiteral: 0.75)
opacity.toValue = NSNumber(floatLiteral: 0)

let animation = CAAnimationGroup()
animation.animations = [scale, opacity]
animation.duration = twinkleInteval * 3
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.setValue(layer, forKey: layerKey)
animation.delegate = self
layer.opacity = 0
layer.add(animation, forKey: nil)

}</code></pre>

通過修改 animation.duration 來確定同一時間停留在屏幕上的圖層數量。另外,由于demo中每次回調創建一個圖層,為了避免長時間動畫后,視圖上保留的 CAShapeLayer 過多時,在每次動畫結束后移除對應的圖層。

open func setValue(_ value: Any?, forKey key: String)

方法可以將圖層通過鍵值對的方式保存在動畫對象 animation 中,并在動畫結束時取出圖層。iOS10之前所有 NSObject 的子類都自動遵守了動畫協議,但在iOS10中我們需要手動遵守 CAAnimationDelegate

let layerKey = "layerKey"
extension XiuXiuViewController, CAAnimationDelegate {
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        if let layer: CALayer = anim.value(forKey: layerKey) as? CALayer {
            layer.removeFromSuperlayer()
        }
    }
}

另外,圖層的位置是通過 bounds + position 來確認的,前者確認圖層大小尺寸,后者確認中心點

func roundLayer(with frame: CGRect) -> CAShapeLayer {
    let layer = CAShapeLayer()
    layer.path = UIBezierPath(roundedRect: frame, cornerRadius: frame.height / 2).cgPath
    layer.bounds = frame
    layer.position = signInButton.center
    layer.fillColor = UIColor(colorLiteralRed: 34/255.0, green: 192/255.0, blue: 100/255.0, alpha: 1).cgColor
    return layer
}

彈性動畫

在認識CoreAnimation一文中展示過類似的彈性動畫,這里對 CoreAnimation 動畫的流程進行介紹

  • 判斷 keyPath 對應屬性是否為可動畫屬性,如果否,不執行下一步
  • 根據 toValue 和 fromValue 計算出動畫差值,根據 duration 屬性計算出動畫幀數,然后兩者計算出每一幀的圖層屬性
  • 根據 fillMode 參數判斷是否將圖層的 presentation 設置為動畫第一幀的圖層屬性并提交渲染
  • 逐幀設置 presentation 并渲染
  • 根據 autoreverses 判斷是否逆向執行一次動畫
  • 動畫結束調用代理對象的 animationDidStop 方法,根據 isRemovedOnCompletion 屬性判斷是否移除動畫
  • 如果上一步未移除動畫,根據 fillMode 屬性判斷是否將圖層設置為最后一幀的屬性。或者將 presentation 同步為模型樹屬性

上面是筆者使用 CoreAnimation 對流程的大致總結,具體可能還有改動,但基本如此。根據這些步驟,筆者使用 CADisplayLink 在屏幕刷新時重新繪制圖層實現波浪效果,在制作這個動畫之前,我們先將波浪動畫的gif單獨放出來:

中間的彈出速度要快于兩邊,并且在達到最高點之后來回彈動。用彈簧動畫是可以很簡單的實現這種彈動效果,但是卻沒辦法幫我們繪制這種效果,即便有人告訴你彈簧的計算公式,然后讓你實現效果

對于筆者這樣的學渣來說無疑是坑爹。所以為了能準確計算出中間的彈動效果,我們需要一些 assistant 來幫忙

@IBOutlet private weak var referView: UIView!
@IBOutlet private weak var springView: UIView!

為了不影響動畫視覺,這兩個 view 應該設置為hidden或者透明色。每次屏幕刷新時,獲取兩個視圖的 presentation 的位置,然后繪制出路徑,設置到圖層上顯示

func animateWave() {
    let path = CGMutablePath()
    path.move(to: .zero)
    path.addLine(to: CGPoint(x: view.frame.width, y: 0))

let controlY = springView.layer.presentation()?.position.y
let referY = referView.layer.presentation()?.position.y

path.addLine(to: CGPoint(x: view.frame.width, y: referY!))
path.addQuadCurve(to: CGPoint(x: 0, y: referY!), control: CGPoint(x: view.frame.width / 2, y: controlY!))
path.addLine(to: .zero)
layer.path = path

}</code></pre>

在用戶點擊按鈕的時候,創建定時器對象,并且給兩個 assistant 添加對應的彈出動畫。這里筆者兩個彈出都使用了 CASpringAnimation 彈簧動畫,經過多次試驗,如果 referView 只是使用簡單的移動動畫,整體的彈出效果會有些不自然。只要保證左右兩側的彈動力遠低于中間,就能看到很好的效果了

@IBAction func animate(_ sender: Any) {
    let target = CGPoint(x: 0, y: view.center.y / 2)
    referView.layer.position = target
    springView.layer.position = target

displayLink?.invalidate()
displayLink = CADisplayLink(target: self, selector: #selector(animateWave))
displayLink?.add(to: RunLoop.current, forMode: .commonModes)

let move = CASpringAnimation(keyPath: "position")
move.fromValue = NSValue(cgPoint: .zero)
move.toValue = NSValue(cgPoint: target)
move.duration = 2

let spring = CASpringAnimation(keyPath: "position")
spring.fromValue = NSValue(cgPoint: .zero)
spring.toValue = NSValue(cgPoint: target)
spring.duration = 2
spring.damping = 7

referView.layer.add(move, forKey: nil)
springView.layer.add(spring, forKey: nil)
referView.layer.position = target
springView.layer.position = target

}</code></pre>

其他

使用 assistant 是一種動畫常見的方式,尤其在彈性動畫方面更是家常便飯。在開發中 CoreAnimation 已經能夠很好的應付 95% 的動畫效果,合理的結合定時器可以讓動效變得更加棒。最后吐槽一下蘋果的 spring 動畫,如果你嘗試在模擬器上 slow animation ,很容易就看到蘋果的彈性動畫回彈時是

 

來自:http://www.jianshu.com/p/af5337ddfc00

 

 本文由用戶 fi867989 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!