前几天看了一篇关于动画的博客叫手摸手教你写 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地址
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。








发表评论
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。