前几天看了一篇关于动画的博客叫手摸手教你写 slack 的 loading 动画,看着挺炫,但是是安卓版的,寻思的着仿造着写一篇ios版的,下面是我写这个动画的分解~
老规矩先上图和demo地址:
刚看到这个动画的时候,脑海里出现了两个方案,一种是通过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的属性可以用来实现线条的长度变化的动画,下面上绘制四根线条的代码:
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轴为目标进行旋转,下面是效果图和代码:
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来实现线条长短的动画,下面是效果图和代码:
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属性,来实现向中间聚拢的效果,下面是效果图和代码:
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来实现线条长短的动画,下面是效果图和代码:
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属性来控制动画的执行顺序,还加了动画暂停和继续的功能,效果和代码见下图:
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地址
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持快网idc。
相关文章
- ASP.NET本地开发时常见的配置错误及解决方法? 2025-06-10
- ASP.NET自助建站系统的数据库备份与恢复操作指南 2025-06-10
- 个人网站服务器域名解析设置指南:从购买到绑定全流程 2025-06-10
- 个人网站搭建:如何挑选具有弹性扩展能力的服务器? 2025-06-10
- 个人服务器网站搭建:如何选择适合自己的建站程序或框架? 2025-06-10
- 2025-07-10 怎样使用阿里云的安全工具进行服务器漏洞扫描和修复?
- 2025-07-10 怎样使用命令行工具优化Linux云服务器的Ping性能?
- 2025-07-10 怎样使用Xshell连接华为云服务器,实现高效远程管理?
- 2025-07-10 怎样利用云服务器D盘搭建稳定、高效的网站托管环境?
- 2025-07-10 怎样使用阿里云的安全组功能来增强服务器防火墙的安全性?
快网idc优惠网
QQ交流群
-
Linux中openssl/opensslv.h找不到问题的解决方法
2025-05-27 97 -
2025-05-25 76
-
2025-05-29 87
-
2025-06-04 115
-
SpringBoot集成WebSocket【基于纯H5】进行点对点[一对一]和广播[一对多]实时推送
2025-05-29 99