前言
任何動畫離不開一個重要的概念——時間,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中偷懶,每次回撥建立新的圖層進行動畫
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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) } |
通過修改animation.duration
來確定同一時間停留在螢幕上的圖層數量。另外,由於demo中每次回撥建立一個圖層,為了避免長時間動畫後,檢視上保留的CAShapeLayer
過多時,在每次動畫結束後移除對應的圖層。
1 |
open func setValue(_ value: Any?, forKey key: String) |
方法可以將圖層通過鍵值對的方式儲存在動畫物件animation
中,並在動畫結束時取出圖層。iOS10之前所有NSObject
的子類都自動遵守了動畫協議,但在iOS10中我們需要手動遵守CAAnimationDelegate
1 2 3 4 5 6 7 8 |
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
來確認的,前者確認圖層大小尺寸,後者確認中心點
1 2 3 4 5 6 7 8 |
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單獨放出來:
中間的彈出速度要快於兩邊,並且在達到最高點之後來回彈動。用彈簧動畫是可以很簡單的實現這種彈動效果,但是卻沒辦法幫我們繪製這種效果,即便有人告訴你彈簧的計算公式,然後讓你實現效果
1 2 |
@IBOutlet private weak var referView: UIView! @IBOutlet private weak var springView: UIView! |
為了不影響動畫視覺,這兩個view
應該設定為hidden或者透明色。每次螢幕重新整理時,獲取兩個檢視的presentation
的位置,然後繪製出路徑,設定到圖層上顯示
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 } |
在使用者點選按鈕的時候,建立定時器物件,並且給兩個assistant
新增對應的彈出動畫。這裡筆者兩個彈出都使用了CASpringAnimation
彈簧動畫,經過多次試驗,如果referView
只是使用簡單的移動動畫,整體的彈出效果會有些不自然。只要保證左右兩側的彈動力遠低於中間,就能看到很好的效果了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@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 } |
其他
使用assistant
是一種動畫常見的方式,尤其在彈性動畫方面更是家常便飯。在開發中CoreAnimation
已經能夠很好的應付95%
的動畫效果,合理的結合定時器可以讓動效變得更加棒。最後吐槽一下蘋果的spring
動畫,如果你嘗試在模擬器上slow animation
,很容易就看到蘋果的彈性動畫回彈時是對稱的(⊙﹏⊙)b ,本文demo
上一篇:按鈕動畫