前言
在上一篇《認識CoreAnimation》中筆者介紹了系統的動畫庫CoreAnimation
,使用動畫庫有很多好處,這裡就不再進行重複敘述。那麼本篇將承接上一篇的內容,使用提到的基礎的動畫相關類來實現動畫效果,效果圖放上:
大體上可以看到demo主要是漸變以及形變兩種動畫,在更早之前的文章,我們就使用UIView
的動畫介面完成過相同的動畫,而這次將換成CoreAnimation
來完成這些工作
關於圖層
在iOS中,每一個UIView
都擁有一個與之繫結的CALayer
圖層物件,其負責檢視內容的繪製與顯示。跟前者一樣,CALayer
也擁有樹狀的子圖層結構,以及相似的介面方法。CALayer
是圖層的基類,主要提供了檢視顯示範圍、圖層結構介面等屬性,我們通過使用它的子類。下面是一段在控制器的介面中心新增一個圓形的紫色圖層:
1 2 3 4 5 6 7 8 |
override func viewDidLoad() { super.viewDidLoad() let layer = CAShapeLayer() layer.fillColor = UIColor.purpleColor().CGColor layer.path = UIBezierPath(arcCenter: CGPoint(x: UIScreen.mainScreen().bounds.width / 2, y: UIScreen.mainScreen().bounds.height / 2), radius: 100, startAngle: 0, endAngle: 2.0*CGFloat(M_PI), clockwise: false).CGPath self.view.layer.addSublayer(layer) } |
同樣的,每一個CALayer
存在一個sublayers
的陣列屬性,我們也可以遍歷這個陣列來完成移除子檢視之類的操作:
1 2 3 4 |
for sublayer in self.view.layer.sublayers! { print("\(sublayer)") sublayer.removeFromSuperlayer() } |
由於核心動畫框架的動畫都是基於CALayer
的圖層進行新增實現的,所以圖層的新增移除方法是最常用的方法。當然,還有一個addAnimation(anim:forKey:)
介面用來給圖層新增動畫
基礎動畫
基礎動畫CABasicAnimation
是最常用來實現動畫效果的動畫類,其繼承自CAAnimation
動畫基類,為圖層動畫效果實現了一個keyPath
屬性,我們通過設定這個屬性來為對應的keyPath
屬性值執行動畫效果。動畫類提供了fromValue
和toValue
兩個屬性用來設定動畫的起始和結束的值,比如下面一段程式碼讓新增到檢視上的紫色圖層變得透明:
1 2 3 4 5 6 7 |
@IBAction func actionToAnimatedLayer(sender: AnyObject) { let animation = CABasicAnimation(keyPath: "opacity") animation.fromValue = NSNumber(double: 1) animation.toValue = NSNumber(double: 0) animation.duration = 1 layer.addAnimation(animation, forKey: nil) } |
上面的程式碼用動畫表現了在1秒內讓圖層的opacity
屬性從1
到0
的過程。但上面不難看出在動畫結束之後,紫色的圖層沒有保持opacity
等於0
的狀態,而是回到了動畫最開始的狀態。這是為什麼呢?
在上一篇中筆者提到過在每一個CALayer
中存在著模型
、呈現
、渲染
三種圖層樹,正是這些圖層樹共同作用來完成隱式動畫。那麼使用核心動畫的時候,實際上CABasicAnimation
會根據動畫時長計算出每一幀的動畫屬性的值,然後實時提交給呈現樹
來展示對應時間點的檢視效果,在動畫結束時CAAnimation
物件會自動從圖層上移除。而由於在整個動畫過程模型樹
的值沒有改變,所以在動畫結束的時候呈現樹
會再次從模型樹
獲取圖層的屬性重新繪製。對此,存在這幾種解決方案:
- 在實現動畫的時候同時修改
opacity
,保證模型樹的資料同步
12345678910@IBAction func actionToAnimatedLayer(sender: AnyObject) {let animation = CABasicAnimation(keyPath: "opacity")animation.fromValue = NSNumber(double: 1)animation.toValue = NSNumber(double: 0)animation.fillMode = kCAFillModeForwardsanimation.removedOnCompletion = falseanimation.duration = 1layer.addAnimation(animation, forKey: nil)} - 取消
CAAnimation
的自動移除,並且設定在動畫結束後保持動畫的結束狀態
123456789@IBAction func actionToAnimatedLayer(sender: AnyObject) {let animation = CABasicAnimation(keyPath: "opacity")animation.fromValue = NSNumber(double: 1)animation.toValue = NSNumber(double: 0)animation.fillMode = kCAFillModeForwardsanimation.removedOnCompletion = falseanimation.duration = 1layer.addAnimation(animation, forKey: nil)} - 實現動畫代理方法。綜合上面兩種方法的操作
12345678910111213141516171819202122@IBAction func actionToAnimatedLayer(sender: AnyObject) {let animation = CABasicAnimation(keyPath: "opacity")animation.fromValue = NSNumber(double: 1)animation.toValue = NSNumber(double: 0)animation.fillMode = kCAFillModeForwardsanimation.removedOnCompletion = falseanimation.duration = 1animation.setValue(layer, forKey: "animatedLayer")animation.delegate = selflayer.addAnimation(animation, forKey: nil)}override func animationDidStop(anim: CAAnimation, finished flag: Bool) {if anim is CABasicAnimation {let animation = anim as! CABasicAnimationif let layer = animation.valueForKey("animatedLayer") as? CALayer {layer.setValue(animation.toValue, forKey: animation.keyPath!)layer.removeAllAnimations()}}}
相比較前兩種方法,實現代理然後設定屬性的做法有些繁雜且無用的感覺。但在某些應用場景下,我們需要在動畫結束時移除圖層或其他操作,通過實現代理是最好的做法。其他常用的keyPath
動畫值可以在這裡檢視
動畫組
接著上面的動畫效果,我想要在漸變的基礎上增加一個形變動畫,那麼我需要建立兩個CABasicAnimation
物件來完成這一工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@IBAction func actionToAnimatedLayer(sender: AnyObject) { let opacity = CABasicAnimation(keyPath: "opacity") opacity.fromValue = NSNumber(double: 1) opacity.toValue = NSNumber(double: 0) opacity.duration = 1 layer.addAnimation(opacity, forKey: "opacity") let scale = CABasicAnimation(keyPath: "transform") scale.fromValue = NSValue(CATransform3D: CATransform3DIdentity) scale.toValue = NSValue(CATransform3D: CATransform3DMakeScale(2, 2, 2)) scale.duration = 1 layer.addAnimation(scale, forKey: "scale") } |
除了上面這段程式碼之外,在CoreAnimation
框架中提供了一個CAAnimationGroup
類來將多個動畫物件整合成一個物件新增到圖層上。從使用實現的角度而言,並不會跟上面的程式碼有任何出入,卻可以讓程式碼的邏輯更加清晰:
1 2 3 4 5 6 7 8 |
@IBAction func actionToAnimatedLayer(sender: AnyObject) { // create animations let group = CAAnimationGroup() group.animations = [opacity, scale] group.duration = 1 layer.addAnimation(group, forKey: "group") } |
按鈕動畫
首先是動畫中的形變和透明漸變分別對應transform
以及opacity
兩個keyPath
,其次,動畫圖層不是按鈕本身的圖層,因此還需要新增額外的一個圖層。另外,動畫存在外擴和內擴的動畫效果,因此我們還需要定義一個列舉來區分:
1 2 3 4 |
enum LXDAnimationType { case Inner case Outer } |
在swift的extension
中不支援新增儲值屬性,因此我們需要使用到runtime
的動態繫結來完成對按鈕包括動畫型別、動畫顏色兩個屬性的擴充:
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 28 29 30 31 32 33 34 35 36 |
private var kAnimationTypeKey: UInt = 0 private var kAnimationColorKey: UInt = 1 extension UIButton { enum LXDAnimationType { case Inner case Outer } //MARK: - Expand property var animationType: LXDAnimationType? { get { if let type = (objc_getAssociatedObject(self, &kAnimationTypeKey) as? String) { return LXDAnimationType(rawValue: type) } return nil } set { guard newValue != nil else { return } self.clipsToBounds = (newValue == .Inner) objc_setAssociatedObject(self, &kAnimationTypeKey, newValue!.rawValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } var animationColor: UIColor { get { if let color = objc_getAssociatedObject(self, &kAnimationColorKey) { return color as! UIColor } return UIColor.whiteColor() } set { objc_setAssociatedObject(self, &kAnimationColorKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } } |
接下來是如何保證我們在點選按鈕的時候可以執行我們的動畫。這裡我們通過重寫按鈕的sendAction(action:to:forEvent:)
方法來執行動畫,這個方法在每次按鈕傳送一個事件時會被呼叫。同理,當使用者點選按鈕時也會呼叫這個方法:
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
//MARK: - Override public override func sendAction(action: Selector, to target: AnyObject?, forEvent event: UIEvent?) { super.sendAction(action, to: target, forEvent: event) if let type = animationType { var rect: CGRect? var radius = self.layer.cornerRadius var pos = touchPoint(event) let smallerSize = min(self.frame.width, self.frame.height) let longgerSize = max(self.frame.width, self.frame.height) var scale = longgerSize / smallerSize + 0.5 switch type { case .Inner: radius = smallerSize / 2 rect = CGRect(x: 0, y: 0, width: radius*2, height: radius*2) break case .Outer: scale = 2.5 pos = CGPoint(x: self.bounds.width/2, y: self.bounds.height/2) rect = CGRect(x: pos.x - self.bounds.width, y: pos.y - self.bounds.height, width: self.bounds.width, height: self.bounds.height) break } let layer = animateLayer(rect!, radius: radius, position: pos) let group = animateGroup(scale) self.layer.addSublayer(layer) group.setValue(layer, forKey: "animatedLayer") layer.addAnimation(group, forKey: "buttonAnimation") } } public override func animationDidStop(anim: CAAnimation, finished flag: Bool) { if let layer = anim.valueForKey("animatedLayer") as? CALayer { layer .removeFromSuperlayer() } } //MARK: - Private private func touchPoint(event: UIEvent?) -> CGPoint { if let touch = event?.allTouches()?.first { return touch.locationInView(self) } else { return CGPoint(x: self.frame.width/2, y: self.frame.height/2) } } private func animateLayer(rect: CGRect, radius: CGFloat, position: CGPoint) -> CALayer { let layer = CAShapeLayer() layer.lineWidth = 1 layer.position = position layer.path = UIBezierPath(roundedRect: rect, cornerRadius: radius).CGPath switch animationType! { case .Inner: layer.fillColor = animationColor.CGColor layer.bounds = CGRect(x: 0, y: 0, width: radius*2, height: radius*2) break case .Outer: layer.strokeColor = animationColor.CGColor layer.fillColor = UIColor.clearColor().CGColor break } return layer } private func animateGroup(scale: CGFloat) -> CAAnimationGroup { let opacityAnim = CABasicAnimation(keyPath: "opacity") opacityAnim.fromValue = NSNumber(double: 1) opacityAnim.toValue = NSNumber(double: 0) let scaleAnim = CABasicAnimation(keyPath: "transform") scaleAnim.fromValue = NSValue(CATransform3D: CATransform3DIdentity) scaleAnim.toValue = NSValue(CATransform3D: CATransform3DMakeScale(scale, scale, scale)) let group = CAAnimationGroup() group.animations = [opacityAnim, scaleAnim] group.duration = 0.5 group.delegate = self group.fillMode = kCAFillModeBoth group.removedOnCompletion = false return group } |
擴充套件之後的按鈕只要設定animationType
這個屬性之後就會實現在點選時的動畫效果
1 |
animateButton.animationType = .Outer |
尾話
相比起國外的應用,國內的動畫效果要顯得內斂得多,甚至很多的app是沒考慮過動畫製作的。但是在移動端開發已然是一片血海的今天,漂亮的動畫效果仍然會為你的應用帶來留存,前提是你的應用要靠譜——單純的動效留不住人。因此,掌握動畫是至關重要的一項基本技能。本文demo