服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - IOS - iOS动画教你编写Slack的Loading动画进阶篇

iOS动画教你编写Slack的Loading动画进阶篇

2021-01-27 15:56wang631106979 IOS

这篇文章主要为大家进一步详细介绍了iOS动画教你编写Slack的Loading动画,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

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

老规矩先上图和demo地址:

iOS动画教你编写Slack的Loading动画进阶篇

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

相关配置和初始化方法

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//线的宽度
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的属性可以用来实现线条的长度变化的动画,下面上绘制四根线条的代码:

iOS动画教你编写Slack的Loading动画进阶篇

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//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轴为目标进行旋转,下面是效果图和代码:

iOS动画教你编写Slack的Loading动画进阶篇

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//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来实现线条长短的动画,下面是效果图和代码:

iOS动画教你编写Slack的Loading动画进阶篇

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
   线的第一步动画,线长从长变短
   */
  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.x和transform.translation.y属性,来实现向中间聚拢的效果,下面是效果图和代码:

iOS动画教你编写Slack的Loading动画进阶篇

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
   线的第二步动画,线向中间平移
   */
  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来实现线条长短的动画,下面是效果图和代码:

iOS动画教你编写Slack的Loading动画进阶篇

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
   线的第三步动画,线由短变长
   */
  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属性来控制动画的执行顺序,还加了动画暂停和继续的功能,效果和代码见下图:

iOS动画教你编写Slack的Loading动画进阶篇

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//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地址

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

延伸 · 阅读

精彩推荐
  • IOSIOS 屏幕适配方案实现缩放window的示例代码

    IOS 屏幕适配方案实现缩放window的示例代码

    这篇文章主要介绍了IOS 屏幕适配方案实现缩放window的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要...

    xiari5772021-06-01
  • IOS关于iOS自适应cell行高的那些事儿

    关于iOS自适应cell行高的那些事儿

    这篇文章主要给大家介绍了关于iOS自适应cell行高的那些事儿,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的...

    daisy6092021-05-17
  • IOSIOS开发之字典转字符串的实例详解

    IOS开发之字典转字符串的实例详解

    这篇文章主要介绍了IOS开发之字典转字符串的实例详解的相关资料,希望通过本文能帮助到大家,让大家掌握这样的方法,需要的朋友可以参考下...

    苦练内功5832021-04-01
  • IOS解析iOS开发中的FirstResponder第一响应对象

    解析iOS开发中的FirstResponder第一响应对象

    这篇文章主要介绍了解析iOS开发中的FirstResponder第一响应对象,包括View的FirstResponder的释放问题,需要的朋友可以参考下...

    一片枫叶4662020-12-25
  • IOSiOS 雷达效果实例详解

    iOS 雷达效果实例详解

    这篇文章主要介绍了iOS 雷达效果实例详解的相关资料,需要的朋友可以参考下...

    SimpleWorld11022021-01-28
  • IOSiOS布局渲染之UIView方法的调用时机详解

    iOS布局渲染之UIView方法的调用时机详解

    在你刚开始开发 iOS 应用时,最难避免或者是调试的就是和布局相关的问题,下面这篇文章主要给大家介绍了关于iOS布局渲染之UIView方法调用时机的相关资料...

    windtersharp7642021-05-04
  • IOSiOS通过逆向理解Block的内存模型

    iOS通过逆向理解Block的内存模型

    自从对 iOS 的逆向初窥门径后,我也经常通过它来分析一些比较大的应用,参考一下这些应用中某些功能的实现。这个探索的过程乐趣多多,不仅能满足自...

    Swiftyper12832021-03-03
  • IOSiOS中tableview 两级cell的展开与收回的示例代码

    iOS中tableview 两级cell的展开与收回的示例代码

    本篇文章主要介绍了iOS中tableview 两级cell的展开与收回的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...

    J_Kang3862021-04-22