iOS動(dòng)畫進(jìn)階-手摸手教你寫-Slack-的-Loading-動(dòng)畫

前幾天看了一篇關(guān)于動(dòng)畫的博客叫手摸手教你寫 Slack 的 Loading 動(dòng)畫,看著挺炫,但是是安卓版的,尋思的著仿造著寫一篇iOS版的,下面是我寫這個(gè)動(dòng)畫的分解~

老規(guī)矩先上圖和demo地址

這里寫圖片描述

剛看到這個(gè)動(dòng)畫的時(shí)候,腦海里出現(xiàn)了兩個(gè)方案,一種是通過(guò)drawRect畫出來(lái),然后配合CADisplayLink不停的繪制線的樣式;第二種是通過(guò)CAShapeLayer配合CAAnimation來(lái)實(shí)現(xiàn)動(dòng)畫效果。再三考慮覺(jué)得使用后者,因?yàn)榍罢咝枰?jì)算很多,比較復(fù)雜,而且經(jīng)過(guò)測(cè)試前者相比于后者消耗更多的CPU,下面將我的思路寫下來(lái):

相關(guān)配置和初始化方法

在寫這個(gè)動(dòng)畫之前,我們把先需要的屬性寫好,比如線條的粗細(xì),動(dòng)畫的時(shí)間等等,下面是相關(guān)的配置和初識(shí)化方法:

    //線的寬度
    var lineWidth:CGFloat = 0
    //線的長(zhǎng)度
    var lineLength:CGFloat = 0
    //邊距
    var margin:CGFloat = 0
    //動(dòng)畫時(shí)間
    var duration:Double = 2
    //動(dòng)畫的間隔時(shí)間
    var interval:Double = 1
    //四條線的顏色
    var colors:[UIColor] = [UIColor.init(rgba: "#9DD4E9") , UIColor.init(rgba: "#F5BD58"),  UIColor.init(rgba: "#FF317E") , UIColor.init(rgba: "#6FC9B5")]
    //動(dòng)畫的狀態(tài)
    private(set) var status:AnimationStatus = .Normal
    //四條線
    private var lines:[CAShapeLayer] = []
    
    enum AnimationStatus {
        //普通狀態(tài)
        case Normal
        //動(dòng)畫中
        case Animating
        //暫停
        case pause
    }
    
     //MARK: Initial Methods
    convenience init(fram: CGRect , colors: [UIColor]) {
        self.init()
        self.frame = frame
        self.colors = colors
        config()
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        config()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        config()
    }
    
    private func config() {
        lineLength = max(frame.width, frame.height)
        lineWidth  = lineLength/6.0
        margin     = lineLength/4.5 + lineWidth/2
        drawLineShapeLayer()
        transform = CGAffineTransformRotate(CGAffineTransformIdentity, angle(-30))
    }

通過(guò)CAShapeLayer繪制線條

看到這個(gè)線條我就想到了用CAShapeLayer來(lái)處理,因?yàn)?code>CAShapeLayer完全可以實(shí)現(xiàn)這種效果,而且它的strokeEnd的屬性可以用來(lái)實(shí)現(xiàn)線條的長(zhǎng)度變化的動(dòng)畫,下面上繪制四根線條的代碼:

這里寫圖片描述
//MARK: 繪制線
    /**
     繪制四條線
     */
    private func drawLineShapeLayer() {
        //開(kāi)始點(diǎn)
        let startPoint = [point(lineWidth/2, y: margin),
                          point(lineLength - margin, y: lineWidth/2),
                          point(lineLength - lineWidth/2, y: lineLength - margin),
                          point(margin, y: lineLength - lineWidth/2)]
        //結(jié)束點(diǎn)
        let endPoint   = [point(lineLength - lineWidth/2, y: margin) ,
                         point(lineLength - margin, y: lineLength - lineWidth/2) ,
                         point(lineWidth/2, y: lineLength - margin) ,
                         point(margin, y: lineWidth/2)]
        for i in 0...3 {
            let line:CAShapeLayer = CAShapeLayer()
            line.lineWidth   = lineWidth
            line.lineCap     = kCALineCapRound
            line.opacity     = 0.8
            line.strokeColor = colors[i].CGColor
            line.path        = getLinePath(startPoint[i], endPoint: endPoint[i]).CGPath
            layer.addSublayer(line)
            lines.append(line)
        }
        
    }
    
    /**
     獲取線的路徑
     
     - parameter startPoint: 開(kāi)始點(diǎn)
     - parameter endPoint:   結(jié)束點(diǎn)
     
     - returns: 線的路徑
     */
    private func getLinePath(startPoint: CGPoint, endPoint: CGPoint) -> UIBezierPath {
        let path = UIBezierPath()
        path.moveToPoint(startPoint)
        path.addLineToPoint(endPoint)
        return path
    }
    
    private func point(x:CGFloat , y:CGFloat) -> CGPoint {
        return CGPointMake(x, y)
    }
    
    private func angle(angle: Double) -> CGFloat {
        return CGFloat(angle *  (M_PI/180))
    }

執(zhí)行完后就跟上圖一樣的效果了~~~

動(dòng)畫分解

經(jīng)過(guò)分析,可以將動(dòng)畫分為四個(gè)步驟:

  • 畫布的旋轉(zhuǎn)動(dòng)畫,旋轉(zhuǎn)兩圈
  • 線條由長(zhǎng)變短的動(dòng)畫,更畫布選擇的動(dòng)畫一起執(zhí)行,旋轉(zhuǎn)一圈的時(shí)候結(jié)束
  • 線條的位移動(dòng)畫,線條逐漸向中間靠攏,再畫筆旋轉(zhuǎn)完一圈的時(shí)候執(zhí)行,兩圈的時(shí)候結(jié)束
  • 線條由短變長(zhǎng)的動(dòng)畫,畫布旋轉(zhuǎn)完兩圈的時(shí)候執(zhí)行

第一步畫布旋轉(zhuǎn)動(dòng)畫

這里我們使用CABasicAnimation基礎(chǔ)動(dòng)畫,keyPath作用于畫布的transform.rotation.z,以z軸為目標(biāo)進(jìn)行旋轉(zhuǎn),下面是效果圖和代碼:

這里寫圖片描述
//MARK: 動(dòng)畫步驟
    /**
     旋轉(zhuǎn)的動(dòng)畫,旋轉(zhuǎn)兩圈
     */
    private func angleAnimation() {
        let angleAnimation                 = CABasicAnimation.init(keyPath: "transform.rotation.z")
        angleAnimation.fromValue           = angle(-30)
        angleAnimation.toValue             = angle(690)
        angleAnimation.fillMode            = kCAFillModeForwards
        angleAnimation.removedOnCompletion = false
        angleAnimation.duration            = duration
        angleAnimation.delegate            = self
        layer.addAnimation(angleAnimation, forKey: "angleAnimation")
    }

第二步線條由長(zhǎng)變短的動(dòng)畫

這里我們還是使用CABasicAnimation基礎(chǔ)動(dòng)畫,keyPath作用于線條的strokeEnd屬性,讓strokeEnd從1到0來(lái)實(shí)現(xiàn)線條長(zhǎng)短的動(dòng)畫,下面是效果圖和代碼:

這里寫圖片描述
/**
     線的第一步動(dòng)畫,線長(zhǎng)從長(zhǎng)變短
     */
    private func lineAnimationOne() {
        let lineAnimationOne                 = CABasicAnimation.init(keyPath: "strokeEnd")
        lineAnimationOne.duration            = duration/2
        lineAnimationOne.fillMode            = kCAFillModeForwards
        lineAnimationOne.removedOnCompletion = false
        lineAnimationOne.fromValue           = 1
        lineAnimationOne.toValue             = 0
        for i in 0...3 {
            let lineLayer = lines[i]
            lineLayer.addAnimation(lineAnimationOne, forKey: "lineAnimationOne")
        }
    }

第三步線條的位移動(dòng)畫

這里我們也是使用CABasicAnimation基礎(chǔ)動(dòng)畫,keyPath作用于線條的transform.translation.xtransform.translation.y屬性,來(lái)實(shí)現(xiàn)向中間聚攏的效果,下面是效果圖和代碼:

這里寫圖片描述
/**
     線的第二步動(dòng)畫,線向中間平移
     */
    private func lineAnimationTwo() {
        for i in 0...3 {
            var keypath = "transform.translation.x"
            if i%2 == 1 {
                keypath = "transform.translation.y"
            }
            let lineAnimationTwo = CABasicAnimation.init(keyPath: keypath)
            lineAnimationTwo.beginTime = CACurrentMediaTime() + duration/2
            lineAnimationTwo.duration = duration/4
            lineAnimationTwo.fillMode = kCAFillModeForwards
            lineAnimationTwo.removedOnCompletion = false
            lineAnimationTwo.autoreverses = true
            lineAnimationTwo.fromValue = 0
            if i < 2 {
                lineAnimationTwo.toValue = lineLength/4
            }else {
                lineAnimationTwo.toValue = -lineLength/4
            }
            let lineLayer = lines[i]
            lineLayer.addAnimation(lineAnimationTwo, forKey: "lineAnimationTwo")
        }
        
        //三角形兩邊的比例
        let scale = (lineLength - 2*margin)/(lineLength - lineWidth)
        for i in 0...3 {
            var keypath = "transform.translation.y"
            if i%2 == 1 {
                keypath = "transform.translation.x"
            }
            let lineAnimationTwo = CABasicAnimation.init(keyPath: keypath)
            lineAnimationTwo.beginTime = CACurrentMediaTime() + duration/2
            lineAnimationTwo.duration = duration/4
            lineAnimationTwo.fillMode = kCAFillModeForwards
            lineAnimationTwo.removedOnCompletion = false
            lineAnimationTwo.autoreverses = true
            lineAnimationTwo.fromValue = 0
            if i == 0 || i == 3 {
                lineAnimationTwo.toValue = lineLength/4 * scale
            }else {
                lineAnimationTwo.toValue = -lineLength/4 * scale
            }
            let lineLayer = lines[i]
            lineLayer.addAnimation(lineAnimationTwo, forKey: "lineAnimationThree")
        }
    }

第四步線條恢復(fù)的原來(lái)長(zhǎng)度的動(dòng)畫

這里我們還是使用CABasicAnimation基礎(chǔ)動(dòng)畫,keyPath作用于線條的strokeEnd屬性,讓strokeEnd從0到1來(lái)實(shí)現(xiàn)線條長(zhǎng)短的動(dòng)畫,下面是效果圖和代碼:

這里寫圖片描述
/**
     線的第三步動(dòng)畫,線由短變長(zhǎng)
     */
    private func lineAnimationThree() {
        //線移動(dòng)的動(dòng)畫
        let lineAnimationFour                 = CABasicAnimation.init(keyPath: "strokeEnd")
        lineAnimationFour.beginTime            = CACurrentMediaTime() + duration
        lineAnimationFour.duration            = duration/4
        lineAnimationFour.fillMode            = kCAFillModeForwards
        lineAnimationFour.removedOnCompletion = false
        lineAnimationFour.fromValue           = 0
        lineAnimationFour.toValue             = 1
        for i in 0...3 {
            if i == 3 {
                lineAnimationFour.delegate = self
            }
            let lineLayer = lines[i]
            lineLayer.addAnimation(lineAnimationFour, forKey: "lineAnimationFour")
        }
    }

最后一步需要將動(dòng)畫組合起來(lái)

關(guān)于動(dòng)畫組合我沒(méi)用到CAAnimationGroup,因?yàn)檫@些動(dòng)畫并不是加到同一個(gè)layer上,再加上動(dòng)畫類型有點(diǎn)多加起來(lái)也比較麻煩,我就通過(guò)動(dòng)畫的beginTime屬性來(lái)控制動(dòng)畫的執(zhí)行順序,還加了動(dòng)畫暫停喝繼續(xù)的功能,效果和代碼見(jiàn)下圖:

這里寫圖片描述
//MARK: Public Methods
    /**
     開(kāi)始動(dòng)畫
     */
    func startAnimation() {
        angleAnimation()
        lineAnimationOne()
        lineAnimationTwo()
        lineAnimationThree()
    }
    
    /**
      暫停動(dòng)畫
     */
    func pauseAnimation() {
        layer.pauseAnimation()
        for lineLayer in lines {
            lineLayer.pauseAnimation()
        }
        status = .pause
    }
    
    /**
     繼續(xù)動(dòng)畫
     */
    func resumeAnimation() {
        layer.resumeAnimation()
        for lineLayer in lines {
            lineLayer.resumeAnimation()
        }
        status = .Animating
    }
    
    extension CALayer {
    //暫停動(dòng)畫
    func pauseAnimation() {
        // 將當(dāng)前時(shí)間CACurrentMediaTime轉(zhuǎn)換為layer上的時(shí)間, 即將parent time轉(zhuǎn)換為localtime
        let pauseTime = convertTime(CACurrentMediaTime(), fromLayer: nil)
        // 設(shè)置layer的timeOffset, 在繼續(xù)操作也會(huì)使用到
        timeOffset    = pauseTime
        // localtime與parenttime的比例為0, 意味著localtime暫停了
        speed         = 0;
    }
    
    //繼續(xù)動(dòng)畫
    func resumeAnimation() {
        let pausedTime = timeOffset
        speed          = 1
        timeOffset     = 0;
        beginTime      = 0
        // 計(jì)算暫停時(shí)間
        let sincePause = convertTime(CACurrentMediaTime(), fromLayer: nil) - pausedTime
        // local time相對(duì)于parent time時(shí)間的beginTime
        beginTime      = sincePause
    }
}

//MARK: Animation Delegate
    override func animationDidStart(anim: CAAnimation) {
        if let animation = anim as? CABasicAnimation {
            if animation.keyPath == "transform.rotation.z" {
                status = .Animating
            }
        }
    }

    override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
        if let animation = anim as? CABasicAnimation {
            if animation.keyPath == "strokeEnd" {
                if flag {
                    status = .Normal
                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(interval) * Int64(NSEC_PER_SEC)), dispatch_get_main_queue(), {
                        if self.status != .Animating {
                            self.startAnimation()
                        }
                    })
                }
            }
        }
    }
    
     //MARK: Override
    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        switch status {
        case .Animating:
            pauseAnimation()
        case .pause:
            resumeAnimation()
        case .Normal:
            startAnimation()
        }
    }

總結(jié)

動(dòng)畫看起來(lái)挺復(fù)雜,但是細(xì)細(xì)劃分出來(lái)也就那么回事,在寫動(dòng)畫之前要先想好動(dòng)畫的步驟,這個(gè)很關(guān)鍵,希望大家通過(guò)這篇博客可以學(xué)到東西,有什么好的建議可以隨時(shí)提出來(lái),謝謝大家閱讀~~demo地址

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

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

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