欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2023-12-18 00:00:40
前几天看了一篇关于动画的博客叫手摸手教你写 slack 的 loading 动画,看着挺炫,但是是安卓版的,寻思的着仿造着写一篇ios版的,下面是我写这个动画的分解~&nb...

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

老规矩先上图和demo地址:

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

刚看到这个动画的时候,脑海里出现了两个方案,一种是通过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的属性可以用来实现线条的长度变化的动画,下面上绘制四根线条的代码:

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

//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动画进阶篇

//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动画进阶篇

/**
   线的第一步动画,线长从长变短
   */
  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动画进阶篇

/**
   线的第二步动画,线向中间平移
   */
  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动画进阶篇

/**
   线的第三步动画,线由短变长
   */
  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动画进阶篇

//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地址

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

上一篇:

下一篇: