iOS動畫-按鈕動畫

前言

在上一篇認識CoreAnimation中筆者介紹了系統(tǒng)的動畫庫CoreAnimation,使用動畫庫有很多好處,這里就不再進行重復(fù)敘述。那么本篇將承接上一篇的內(nèi)容,使用提到的基礎(chǔ)的動畫相關(guān)類來實現(xiàn)動畫效果,效果圖放上:


大體上可以看到demo主要是漸變以及形變兩種動畫,在更早之前的文章,我們就使用UIView的動畫接口完成過相同的動畫,而這次將換成CoreAnimation來完成這些工作

關(guān)于圖層

在iOS中,每一個UIView都擁有一個與之綁定的CALayer圖層對象,其負責視圖內(nèi)容的繪制與顯示。跟前者一樣,CALayer也擁有樹狀的子圖層結(jié)構(gòu),以及相似的接口方法。CALayer是圖層的基類,主要提供了視圖顯示范圍、圖層結(jié)構(gòu)接口等屬性,我們通過使用它的子類。下面是一段在控制器的界面中心添加一個圓形的紫色圖層:

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的數(shù)組屬性,我們也可以遍歷這個數(shù)組來完成移除子視圖之類的操作:

for sublayer in self.view.layer.sublayers! {
    print("\(sublayer)")
    sublayer.removeFromSuperlayer()
}

由于核心動畫框架的動畫都是基于CALayer的圖層進行添加實現(xiàn)的,所以圖層的添加移除方法是最常用的方法。當然,還有一個addAnimation(anim:forKey:)接口用來給圖層添加動畫

基礎(chǔ)動畫

基礎(chǔ)動畫CABasicAnimation是最常用來實現(xiàn)動畫效果的動畫類,其繼承自CAAnimation動畫基類,為圖層動畫效果實現(xiàn)了一個keyPath屬性,我們通過設(shè)置這個屬性來為對應(yīng)的keyPath屬性值執(zhí)行動畫效果。動畫類提供了fromValuetoValue兩個屬性用來設(shè)置動畫的起始和結(jié)束的值,比如下面一段代碼讓添加到視圖上的紫色圖層變得透明:

@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)
}


上面的代碼用動畫表現(xiàn)了在1秒內(nèi)讓圖層的opacity屬性從10的過程。但上面不難看出在動畫結(jié)束之后,紫色的圖層沒有保持opacity等于0的狀態(tài),而是回到了動畫最開始的狀態(tài)。這是為什么呢?

在上一篇中筆者提到過在每一個CALayer中存在著模型呈現(xiàn)、渲染三種圖層樹,正是這些圖層樹共同作用來完成隱式動畫。那么使用核心動畫的時候,實際上CABasicAnimation會根據(jù)動畫時長計算出每一幀的動畫屬性的值,然后實時提交給呈現(xiàn)樹來展示對應(yīng)時間點的視圖效果,在動畫結(jié)束時CAAnimation對象會自動從圖層上移除。而由于在整個動畫過程模型樹的值沒有改變,所以在動畫結(jié)束的時候呈現(xiàn)樹會再次從模型樹獲取圖層的屬性重新繪制。對此,存在這幾種解決方案:

  • 在實現(xiàn)動畫的時候同時修改opacity,保證模型樹的數(shù)據(jù)同步
    @IBAction func actionToAnimatedLayer(sender: AnyObject) {
    let animation = CABasicAnimation(keyPath: "opacity")
    animation.fromValue = NSNumber(double: 1)
    animation.toValue = NSNumber(double: 0)
    animation.fillMode = kCAFillModeForwards
    animation.removedOnCompletion = false
    animation.duration = 1

        layer.addAnimation(animation, forKey: nil)
    }
    
  • 取消CAAnimation的自動移除,并且設(shè)置在動畫結(jié)束后保持動畫的結(jié)束狀態(tài)
    @IBAction func actionToAnimatedLayer(sender: AnyObject) {
    let animation = CABasicAnimation(keyPath: "opacity")
    animation.fromValue = NSNumber(double: 1)
    animation.toValue = NSNumber(double: 0)
    animation.fillMode = kCAFillModeForwards
    animation.removedOnCompletion = false
    animation.duration = 1
    layer.addAnimation(animation, forKey: nil)
    }

  • 實現(xiàn)動畫代理方法。綜合上面兩種方法的操作
    @IBAction func actionToAnimatedLayer(sender: AnyObject) {
    let animation = CABasicAnimation(keyPath: "opacity")
    animation.fromValue = NSNumber(double: 1)
    animation.toValue = NSNumber(double: 0)
    animation.fillMode = kCAFillModeForwards
    animation.removedOnCompletion = false
    animation.duration = 1

        animation.setValue(layer, forKey: "animatedLayer")
        animation.delegate = self
        layer.addAnimation(animation, forKey: nil)
    }
    
    override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
        if anim is CABasicAnimation {
            let animation = anim as! CABasicAnimation
            if let layer = animation.valueForKey("animatedLayer") as? CALayer {
                layer.setValue(animation.toValue, forKey: animation.keyPath!)
                layer.removeAllAnimations()
            }
        }
    }
    

相比較前兩種方法,實現(xiàn)代理然后設(shè)置屬性的做法有些繁雜且無用的感覺。但在某些應(yīng)用場景下,我們需要在動畫結(jié)束時移除圖層或其他操作,通過實現(xiàn)代理是最好的做法。其他常用的keyPath動畫值可以在這里查看

動畫組

接著上面的動畫效果,我想要在漸變的基礎(chǔ)上增加一個形變動畫,那么我需要創(chuàng)建兩個CABasicAnimation對象來完成這一工作:

@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類來將多個動畫對象整合成一個對象添加到圖層上。從使用實現(xiàn)的角度而言,并不會跟上面的代碼有任何出入,卻可以讓代碼的邏輯更加清晰:

@IBAction func actionToAnimatedLayer(sender: AnyObject) {
    // create animations

    let group = CAAnimationGroup()
    group.animations = [opacity, scale]
    group.duration = 1
    layer.addAnimation(group, forKey: "group")
}

按鈕動畫

首先是動畫中的形變和透明漸變分別對應(yīng)transform以及opacity兩個keyPath,其次,動畫圖層不是按鈕本身的圖層,因此還需要添加額外的一個圖層。另外,動畫存在外擴和內(nèi)擴的動畫效果,因此我們還需要定義一個枚舉來區(qū)分:

enum LXDAnimationType {
    case Inner
    case Outer
}

在swift的extension中不支持添加儲值屬性,因此我們需要使用到runtime的動態(tài)綁定來完成對按鈕包括動畫類型、動畫顏色兩個屬性的擴充:

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)
        }
    }
}

接下來是如何保證我們在點擊按鈕的時候可以執(zhí)行我們的動畫。這里我們通過重寫按鈕的sendAction(action:to:forEvent:)方法來執(zhí)行動畫,這個方法在每次按鈕發(fā)送一個事件時會被調(diào)用。同理,當用戶點擊按鈕時也會調(diào)用這個方法:

//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
}

擴展之后的按鈕只要設(shè)置animationType這個屬性之后就會實現(xiàn)在點擊時的動畫效果

animateButton.animationType = .Outer

尾話

相比起國外的應(yīng)用,國內(nèi)的動畫效果要顯得內(nèi)斂得多,甚至很多的app是沒考慮過動畫制作的。但是在移動端開發(fā)已然是一片血海的今天,漂亮的動畫效果仍然會為你的應(yīng)用帶來留存,前提是你的應(yīng)用要靠譜——單純的動效留不住人。因此,掌握動畫是至關(guān)重要的一項基本技能。本文demo

上一篇:認識CoreAnimation
下一篇:定時器動畫

轉(zhuǎn)載請注明本文作者和轉(zhuǎn)載地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在iOS中隨處都可以看到絢麗的動畫效果,實現(xiàn)這些動畫的過程并不復(fù)雜,今天將帶大家一窺ios動畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,691評論 6 30
  • 在iOS中隨處都可以看到絢麗的動畫效果,實現(xiàn)這些動畫的過程并不復(fù)雜,今天將帶大家一窺iOS動畫全貌。在這里你可以看...
    F麥子閱讀 5,270評論 5 13
  • Core Animation Core Animation,中文翻譯為核心動畫,它是一組非常強大的動畫處理API,...
    45b645c5912e閱讀 3,157評論 0 21
  • 在iOS實際開發(fā)中常用的動畫無非是以下四種:UIView動畫,核心動畫,幀動畫,自定義轉(zhuǎn)場動畫。 1.UIView...
    請叫我周小帥閱讀 3,324評論 1 23
  • 本文轉(zhuǎn)載自:http://www.cocoachina.com/ios/20150105/10812.html 為...
    idiot_lin閱讀 1,379評論 0 1

友情鏈接更多精彩內(nèi)容