美文网首页CRAnimation好东西小知识点
iOS动画进阶-手摸手教你写-Slack-的-Loading-动

iOS动画进阶-手摸手教你写-Slack-的-Loading-动

作者: W_C__L | 来源:发表于2017-03-11 09:39 被阅读142次

    前几天看了一篇关于动画的博客叫手摸手教你写 Slack 的 Loading 动画,看着挺炫,但是是安卓版的,寻思的着仿造着写一篇iOS版的,下面是我写这个动画的分解~

    老规矩先上图和demo地址

    这里写图片描述

    刚看到这个动画的时候,脑海里出现了两个方案,一种是通过drawRect画出来,然后配合CADisplayLink不停的绘制线的样式;第二种是通过CAShapeLayer配合CAAnimation来实现动画效果。再三考虑觉得使用后者,因为前者需要计算很多,比较复杂,而且经过测试前者相比于后者消耗更多的CPU,下面将我的思路写下来:

    相关配置和初始化方法

    在写这个动画之前,我们把先需要的属性写好,比如线条的粗细,动画的时间等等,下面是相关的配置和初识化方法:

        //线的宽度
        var lineWidth:CGFloat = 0
        //线的长度
        var lineLength:CGFloat = 0
        //边距
        var margin:CGFloat = 0
        //动画时间
        var duration:Double = 2
        //动画的间隔时间
        var interval:Double = 1
        //四条线的颜色
        var colors:[UIColor] = [UIColor.init(rgba: "#9DD4E9") , UIColor.init(rgba: "#F5BD58"),  UIColor.init(rgba: "#FF317E") , UIColor.init(rgba: "#6FC9B5")]
        //动画的状态
        private(set) var status:AnimationStatus = .Normal
        //四条线
        private var lines:[CAShapeLayer] = []
        
        enum AnimationStatus {
            //普通状态
            case Normal
            //动画中
            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))
        }
    

    通过CAShapeLayer绘制线条

    看到这个线条我就想到了用CAShapeLayer来处理,因为CAShapeLayer完全可以实现这种效果,而且它的strokeEnd的属性可以用来实现线条的长度变化的动画,下面上绘制四根线条的代码:

    这里写图片描述
    //MARK: 绘制线
        /**
         绘制四条线
         */
        private func drawLineShapeLayer() {
            //开始点
            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)]
            //结束点
            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: 开始点
         - parameter endPoint:   结束点
         
         - 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))
        }
    

    执行完后就跟上图一样的效果了~~~

    动画分解

    经过分析,可以将动画分为四个步骤:

    • 画布的旋转动画,旋转两圈
    • 线条由长变短的动画,更画布选择的动画一起执行,旋转一圈的时候结束
    • 线条的位移动画,线条逐渐向中间靠拢,再画笔旋转完一圈的时候执行,两圈的时候结束
    • 线条由短变长的动画,画布旋转完两圈的时候执行

    第一步画布旋转动画

    这里我们使用CABasicAnimation基础动画,keyPath作用于画布的transform.rotation.z,以z轴为目标进行旋转,下面是效果图和代码:

    这里写图片描述
    //MARK: 动画步骤
        /**
         旋转的动画,旋转两圈
         */
        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")
        }
    

    第二步线条由长变短的动画

    这里我们还是使用CABasicAnimation基础动画,keyPath作用于线条的strokeEnd属性,让strokeEnd从1到0来实现线条长短的动画,下面是效果图和代码:

    这里写图片描述
    /**
         线的第一步动画,线长从长变短
         */
        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")
            }
        }
    

    第三步线条的位移动画

    这里我们也是使用CABasicAnimation基础动画,keyPath作用于线条的transform.translation.xtransform.translation.y属性,来实现向中间聚拢的效果,下面是效果图和代码:

    这里写图片描述
    /**
         线的第二步动画,线向中间平移
         */
        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")
            }
        }
    

    第四步线条恢复的原来长度的动画

    这里我们还是使用CABasicAnimation基础动画,keyPath作用于线条的strokeEnd属性,让strokeEnd从0到1来实现线条长短的动画,下面是效果图和代码:

    这里写图片描述
    /**
         线的第三步动画,线由短变长
         */
        private func lineAnimationThree() {
            //线移动的动画
            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")
            }
        }
    

    最后一步需要将动画组合起来

    关于动画组合我没用到CAAnimationGroup,因为这些动画并不是加到同一个layer上,再加上动画类型有点多加起来也比较麻烦,我就通过动画的beginTime属性来控制动画的执行顺序,还加了动画暂停喝继续的功能,效果和代码见下图:

    这里写图片描述
    //MARK: Public Methods
        /**
         开始动画
         */
        func startAnimation() {
            angleAnimation()
            lineAnimationOne()
            lineAnimationTwo()
            lineAnimationThree()
        }
        
        /**
          暂停动画
         */
        func pauseAnimation() {
            layer.pauseAnimation()
            for lineLayer in lines {
                lineLayer.pauseAnimation()
            }
            status = .pause
        }
        
        /**
         继续动画
         */
        func resumeAnimation() {
            layer.resumeAnimation()
            for lineLayer in lines {
                lineLayer.resumeAnimation()
            }
            status = .Animating
        }
        
        extension CALayer {
        //暂停动画
        func pauseAnimation() {
            // 将当前时间CACurrentMediaTime转换为layer上的时间, 即将parent time转换为localtime
            let pauseTime = convertTime(CACurrentMediaTime(), fromLayer: nil)
            // 设置layer的timeOffset, 在继续操作也会使用到
            timeOffset    = pauseTime
            // localtime与parenttime的比例为0, 意味着localtime暂停了
            speed         = 0;
        }
        
        //继续动画
        func resumeAnimation() {
            let pausedTime = timeOffset
            speed          = 1
            timeOffset     = 0;
            beginTime      = 0
            // 计算暂停时间
            let sincePause = convertTime(CACurrentMediaTime(), fromLayer: nil) - pausedTime
            // local time相对于parent time时间的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()
            }
        }
    

    总结

    动画看起来挺复杂,但是细细划分出来也就那么回事,在写动画之前要先想好动画的步骤,这个很关键,希望大家通过这篇博客可以学到东西,有什么好的建议可以随时提出来,谢谢大家阅读~~demo地址

    相关文章

      网友评论

        本文标题:iOS动画进阶-手摸手教你写-Slack-的-Loading-动

        本文链接:https://www.haomeiwen.com/subject/xhdagttx.html