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