(转载自:http://blog.csdn.net/wang631106979/article/details/52473985)

如果移动端访问不佳,可以访问我的个人博客

前几天看了一篇关于动画的博客叫手摸手教你写 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 动画的更多相关文章

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

    如果移动端访问不佳,可以访问我的个人博客 前几天看了一篇关于动画的博客叫手摸手教你写 Slack 的 Loading 动画,看着挺炫,但是是安卓版的,寻思的着仿造着写一篇iOS版的,下面是我写这个动画 ...

  2. 用svg制作loading动画

    首先说明:由于各浏览器对svg动画事件支持不统一,此loading动画在Firefox,Opera,Chrome中均没有问题,IE和Safari中有问题,可能是不支持SIML写动画的语法, 但是用Ca ...

  3. 巧用 CSS 实现动态线条 Loading 动画

    有群友问我,使用 CSS 如何实现如下 Loading 效果: 这是一个非常有意思的问题. 我们知道,使用 CSS,我们可以非常轻松的实现这样一个动画效果: <div></div&g ...

  4. [iOS UI进阶 - 6.1] 核心动画CoreAnimation

    A.基本知识 1.概念 Core Animation是一组非常强大的动画处理API,使用它能做出非常炫丽的动画效果,而且往往是事半功倍,使用它需要先添加QuartzCore.framework和引入对 ...

  5. 手把手教你写css3通用动画

    之前接了几个微信里的项目,类似电子邀请函,什么分析报告这样的项目, 对css3动画要求十分高,每个页面客户几乎都有天马行空的想法,或者说设计师有这样的想法.众所周知css3里的keyframe写好了就 ...

  6. skyweaver 手把手教你写css3通用动画

    之前接了几个微信里的项目,类似电子邀请函,什么分析报告这样的项目, 对css3动画要求十分高,每个页面客户几乎都有天马行空的想法,或者说设计师有这样的想法.众所周知css3里的keyframe写好了就 ...

  7. iOS开发——UI进阶篇(十八)核心动画小例子,转盘(裁剪图片、自定义按钮、旋转)图片折叠、音量震动条、倒影、粒子效果

    一.转盘(裁剪图片.自定义按钮.旋转) 1.裁剪图片 将一张大图片裁剪为多张 // CGImageCreateWithImageInRect:用来裁剪图片 // image:需要裁剪的图片 // re ...

  8. [iOS UI进阶 - 6.2] 核心动画CoreAnimation 练习代码

    A.基本用法 1.CABasicAnimation // // ViewController.m // CoreAnimationTest // // Created by hellovoidworl ...

  9. iOS UI进阶-3.0 核心动画

    Core Animation是一组非常强大的动画处理API,使用它能做出非常炫丽的动画效果,而且往往是事半功倍,使用它需要先添加QuartzCore.framework和引入对应的框架<Quar ...

随机推荐

  1. Miller-Rabin,Pollard-Rho(BZOJ3667)

    裸题直接做就好了. #include <cstdio> #include <cstdlib> #include <algorithm> using namespac ...

  2. glusterfs 4.0.1 event模块 分析笔记1

    1. 前言 在C语言i中,存储变量的结构体加上一组函数指针,大概就可以算是一个对象模型了:如果将一组函数指针捆绑为结构体, 后期根据配置或者环境需要绑定到不同实现模块中的一组函数,可以认为是C语言面对 ...

  3. Linux常用命令大全(归类)

    最近都在和Linux打交道,这方面基础比较薄弱的我只好买了本鸟哥的书看看,感觉还不错.我觉得Linux相比windows比较麻烦的就是很多东西都要用命令来控制,当然,这也是很多人喜欢linux的原因, ...

  4. avalon,xmp

  5. 关于一些基础的Java问题的解答(四)

    16. Java面向对象的三个特征与含义 java中的面向对象的三大基本特征分别是:封装.继承.多态: 封装:把过程和数据包围起来,对数据的访问只能通过已定义的界面,主要是方便类的修改 继承:对象的一 ...

  6. compress函数用法详解

    compress函数: 主要用来删除字符串中的特定字符. 1.compress函数的基本形式compress(<source><, chars><, modifiers& ...

  7. C语言多维数组的指针传递

    在C语言中为了节省空间,提高运行速度经常使用指针来完成数组的传递. 对于一维数组而言可以直接传递首地址 而对于二维数组必须在传递时声明是二维数组的指针,并且调用时也要经过一些运算 首先是定义形参: 函 ...

  8. ES6(数据结构_2)

    数据结构—2 Map 与 Array 的对比 Set 与 Array的对比 Map 与 Object 的对比 Set 与 Object 的对比 (增.查.改.删) 一.Map 与 Array 的对比 ...

  9. iOS控制反转(IoC)与依赖注入(DI)的实现

    背景 最近接触了一段时间的SpringMVC,对其控制反转(IoC)和依赖注入(DI)印象深刻,此后便一直在思考如何使用OC语言较好的实现这两个功能.Java语言自带的注解特性为IoC和DI带来了极大 ...

  10. 使用Kubernetes需要注意的一些问题(FAQ of k8s)

    本篇文章并不是介绍K8S 或者Docker的,而仅仅是使用过程中一些常见问题的汇总. 重启策略:http://kubernetes.io/docs/user-guide/pod-states/, 对于 ...