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

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

    (转载自:http://blog.csdn.net/wang631106979/article/details/52473985) 如果移动端访问不佳,可以访问我的个人博客 前几天看了一篇关于动画的博 ...

  2. 手摸手教你如何在 Python 编码中做到小细节大优化

    手摸手教你如何在 Python 编码中做到小细节大优化 在列表里计数 """ 在列表里计数,使用 Python 原生函数计数要快很多,所以尽量使用原生函数来计算. &qu ...

  3. 手摸手教你微信小程序开发之自定义组件

    前言 相信大家在开发小程序时会遇到某个功能多次使用的情况,比如弹出框.这个时候大家首先想到的是组件化开发,就是把弹出框封装成一个组件,然后哪里使用哪里就调用,对,看来大家都是有思路的人,但是要怎样实现 ...

  4. 手摸手教你让Laravel开发Api更得心应手

    https://www.guaosi.com/2019/02/26/laravel-api-initialization-preparation/ 1. 起因 随着前后端完全分离,PHP也基本告别了v ...

  5. 【转】手摸手,带你用vue撸后台 系列一

    前言 说好的教程终于来了,第一篇文章主要来说一说在开始写业务代码前的一些准备工作吧,但这里不会教你webpack的基础配置,热更新怎么做,webpack速度优化等等,有需求的请自行google. 目录 ...

  6. 手摸手带你用Hexo撸博客(一)

    原文地址 手摸手带你用Hexo撸博客(一) 环境搭建 安装 node 狂点下一步 命令行输入此条命令 如果能看到版本号则安装成功 node -v 安装Git (同上) 实在不会的小伙伴百度一下,教程很 ...

  7. 手摸手,和你一起学习 UiPath Studio

    学习 RPA 的路上坑比较多,让我们手摸手,一起走…… 以下是一些学习 UiPath 和 RPA 的资源, 拿走不用谢! UiPath Studio 中文文档 机器人流程自动化其实是很好的概念和技术, ...

  8. 【转】手摸手,带你用vue撸后台 系列二(登录权限篇)

    前言 拖更有点严重,过了半个月才写了第二篇教程.无奈自己是一个业务猿,每天被我司的产品虐的死去活来,之前又病了一下休息了几天,大家见谅. 进入正题,做后台项目区别于做其它的项目,权限验证与安全性是非常 ...

  9. 【转】手摸手,带你用vue撸后台 系列三(实战篇)

    前言 在前面两篇文章中已经把基础工作环境构建完成,也已经把后台核心的登录和权限完成了,现在手摸手,一起进入实操. Element 去年十月份开始用vue做管理后台的时候毫不犹豫的就选择了Elemen, ...

随机推荐

  1. Flash Builder 相关

    1.Flex SDK 4.1 兼容性 Flex SDK 4.1 兼容 Flash Builder 4.0 ,因此在 Flash Builder 4.0 中使用 4.1 SDK 时可以使用设计视图 Fl ...

  2. uiautomatorviewer.bat使用方法

    在android目录下找到uiautomatorviewer.bat,然后双击,页面的第二个按钮连接设备 D:\Program Files\android-sdk-windows\tools\uiau ...

  3. Plug组件(不断跟新)

    这个plug组件不知到底是什么东西,不知何com组件什么区别 #include <iostream> #include <plug/plug.h> #include " ...

  4. 【BZOJ4621】Tc605 DP

    [BZOJ4621]Tc605 Description 最初你有一个长度为 N 的数字序列 A.为了方便起见,序列 A 是一个排列. 你可以操作最多 K 次.每一次操作你可以先选定一个 A 的一个子串 ...

  5. HTML中条件注释的高级应用

    在页面头部加入 <!--[if lt IE 9]><html class="ie"><![endif]--> 可简单CSS Hack,IE6.I ...

  6. orchestrator-Raft集群部署

    本文简要说明下orchestrator的Raft集群部署,其实部署很简单主要是好好研究下配置文件的配置,这里我的样例配置文件暂时只适用于我们这块业务 如果您自己使用请根据情况自行修改. 主要通过配置文 ...

  7. boost:property_tree::ini_parser:::read_ini 读取ini时崩溃

    原因: 1 路径错误 2 配置文件中某一行缺少=,例如用// 做注释的,前面应该加";" 解决办法: 添加异常处理,实例代码如下: #include <boost/prope ...

  8. 如何让socket编程非阻塞?

    import socket # 创建socket client = socket.socket() # 将原来阻塞的位置变成非阻塞(报错) client.setblocking(False) # 百度 ...

  9. Android项目使用Ant多渠道打包(最新sdk)

    参考文章: http://blog.csdn.net/liuhe688/article/details/6679879 http://www.eoeandroid.com/thread-323111- ...

  10. tomcat 配置文件 介绍

    [root@mysql logs]# cd ../conf/ [root@mysql conf]# ll总用量 228drwxr-x---. 3 root root 4096 11月 15 2018 ...