イラストレーターみやびの漫画館 作品集 - 月の高いところのロゴマーク

今日のプリン言

謎のプリン語る。
一人書く人増えました。

【Swift4】円のアニメーションにイージングをつける

2018年03月01日

みやびプリン 140 87

500 320

【Swift4】円のアニメーションにイージングをつける - サムネイル

どうも、なぜだか本当に駐車場が見つからないみやびです。
どうなってるの、札幌中央区!?

さて、表記の件。
よくある円グラフにアニメーションをつけるってやつなのですが、
下記エントリーが非常に参考になった。
(というか、ほぼほぼパクり)

【iOS】【swift】アニメーション付き円グラフ

さすがQiitaさんです。
上記のものを、Swift4用に若干変えたのが下記だ。

import UIKit

struct CircleParamStruct {
  let value: Float
  let color: UIColor
  
  init(setValue: Float, setColor: UIColor){
    self.value = setValue
    self.color = setColor
  }
}

class CircleAnimationView: UIView {
  
  private let params: Array<CircleParamStruct>
  private var endAngle: CGFloat = CGFloat(Double.pi / 2.0) * -1
  private let endAngleFirst: CGFloat = CGFloat(Double.pi / 2.0) * -1
  private let maxAngle: CGFloat
  private let allCount: CGFloat
  private let holeFlg: Bool
  private let holeWidth: CGFloat
  private var animationTimer: Timer?
  private var animationWait: CGFloat
  private let animationFrame: Int
  private let frameCountAll: Double
  private var frameCountNow: Int = 0
  
  required init(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  init(frame: CGRect, params: Array<CircleParamStruct>, setHoleFlg: Bool = false, allCount: CGFloat = 0, setWait: CGFloat = 1.0, setHoleWidth: CGFloat = 0.0, setFps: Int = 60) {
    self.params = params
    self.holeWidth = setHoleWidth
    self.holeFlg = setHoleFlg
    self.allCount = allCount
    self.animationWait = setWait
    self.animationFrame = setFps
    
    self.frameCountAll = Double(Float(setWait) / (1.0 / Float(setFps)))
    
    if params.count == 1 && allCount != 0 {
      self.maxAngle = CGFloat(Double.pi * 2) * CGFloat(params[0].value) / CGFloat(allCount)
    } else {
      self.maxAngle = CGFloat(Double.pi * 2)
    }
    
    super.init(frame: frame)
    self.isOpaque = false
    self.backgroundColor = UIColor.init(red: 0, green: 0, blue: 0, alpha: 0)
    
    
  }
  
  @objc private func update(){
    let angle = CGFloat(Double.pi * 2.0 / self.frameCountAll)
    self.endAngle = angle
    if(self.endAngle > self.maxAngle) {
      //終了
      self.animationTimer?.invalidate()
    } else {
      self.setNeedsDisplay()
    }
    self.frameCountNow += 1
  }
  
  func startAnimatin(){
    self.animationTimer = Timer.scheduledTimer(timeInterval: TimeInterval(1.0 / CGFloat(self.animationFrame)), target: self, selector: #selector(self.update), userInfo: nil, repeats: true)
  }
  
  override func draw(_ rect: CGRect) {
    // Drawing code
    let context: CGContext = UIGraphicsGetCurrentContext()!
    var x: CGFloat = rect.origin.x
    x += rect.size.width / 2
    var y: CGFloat = rect.origin.y
    y += rect.size.height / 2
    var max: CGFloat = 0
    if self.allCount == 0 {
      for dic: CircleParamStruct in self.params {
        let value = CGFloat(dic.value)
        max += value
      }
    } else {
      max = self.allCount
    }
    
    var startAngleInner: CGFloat = CGFloat(Double.pi / 2) * -1
    var endAngleInner: CGFloat = 0
    let radius: CGFloat  = self.frame.size.width / 2
    let radiusInner: CGFloat = self.frame.size.width / 2
    for dic: CircleParamStruct in self.params {
      let value = CGFloat(dic.value)
      endAngleInner = startAngleInner + CGFloat(Double.pi * 2) * (value / max)
      if(endAngleInner > self.endAngle) {
        endAngleInner = self.endAngle
      }
      let color: UIColor = dic.color
      
      context.move(to: CGPoint(x: x, y: y))
      context.addArc(center: CGPoint(x: x, y: y), radius: radius, startAngle: startAngleInner, endAngle: endAngleInner, clockwise: false)
      
      if self.holeFlg {
        context.addArc(center: CGPoint(x: x, y: y), radius: radius - self.holeWidth, startAngle: endAngleInner, endAngle: startAngleInner, clockwise: true)
      }
      
      context.setFillColor(color.cgColor)
      context.closePath()
      context.fillPath()
      startAngleInner = endAngleInner
    }
  }
}

エントリーとの相違点は、
パスを生成するさいのメソッドの実行の仕方と、
アニメーションの実行を、繰り返しのTimerを使うってのに変更してる点、
一個一個のデータの持ち方を、構造体(struct)に変更してる点、
それに伴って、イニシャライザをいじっている。

これを使用するには、下記のようにする。

import UIKit

class ViewController: UIViewController {
    
  var graphView: CircleAnimationView!

  override func viewDidLoad() {
    super.viewDidLoad()
    var params = Array<CircleParamStruct>()
    params.append(CircleParamStruct(setValue: 2, setColor: UIColor.blue))
    params.append(CircleParamStruct(setValue: 1, setColor: UIColor.red))
    params.append(CircleParamStruct(setValue: 4, setColor: UIColor.yellow))
    self.graphView = CircleAnimationView(frame: CGRect(x: 0, y: 0, width: 320, height: 320), params: params)
    
    self.view.addSubview(graphView)
    
    graphView.startAnimatin()
  }
  
  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
  }
}

これを、真ん中に穴を開ける場合は、イニシャライザを下記のようにする。

self.graphView = CircleAnimationView(frame: CGRect(x: 0, y: 0, width: 320, height: 320), params: params, setHoleFlg: true, allCount: 0, setWait: 1.0, setHoleWidth: 20)

イニシャライザめんどくさい感じなのはごめんなさない。
もっといいやり方あると思う。

さて、ここまでやって気づいた方もいるでしょうが、
このままだと、アニメーションにイージングをつけることができない。
タイマーで繰り返しやってるだけだからね。
ではどうすれば、イージングをつけるかというと、
回転の角度をアップデートする時に、その角度の数値に対して、
イージングの計算をさせればいいのだ。(当然のことながら、円のアニメーションは、回転の角度によって、アニメーションさせているため)

まず、イージング関数を格納した列挙と構造体を用意する。
下記のエントリーを参照に、計算式を入れていく。

イージング処理の計算式 - 強火で進め

import UIKit

// イージングの種類
public enum EaseType: String {
  case Empty = "null",
  easeIn = "easeIn",
  easeOut = "easeOut",
  easeInOut = "easeInOut",
  easeInCubic = "easeInCubic",
  easeOutCubic = "easeOutCubic",
  easeInOutCubic = "easeInOutCubic",
  easeInQuart = "easeInQuart",
  easeOutQuart = "easeOutQuart",
  easeInOutQuart = "easeInOutQuart",
  easeInQuint = "easeInQuint",
  easeOutQuint = "easeOutQuint",
  easeInOutQuint = "easeInOutQuint"
}

// イージング適用の値を出す構造体、メソッド
struct AnimationEasingValue {
  public static func valueFunc(easeType: EaseType, t: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat) -> CGFloat {
    var ret: CGFloat = 0.0
    var ti = t
    
    switch easeType {
    case .easeIn:
      ti /= d
      ret = c*ti*ti + b
    case .easeOut:
      ti /= d
      ret = -c*ti*(ti - 2.0) + b
    case .easeInOut:
      ti /= d / 2.0
      if ti < 1 {
        ret = c / 2.0 * ti * ti + b
      } else {
        ti = ti - 1
        ret = -c / 2.0 * (ti*(ti - 2) - 1) + b
      }
    case .easeInCubic:
      ti /= d
      ret = c*ti*ti*ti + b
    case .easeOutCubic:
      ti /= d
      ti = ti - 1
      ret = c*(ti*ti*ti + 1) + b
    case .easeInOutCubic:
      ti /= d/2.0
      if ti < 1 {
        ret = c/2.0*ti*ti*ti + b
      } else {
        ti = ti - 2
        ret = c/2.0 * (ti*ti*ti + 2) + b
      }
    case .easeInQuart:
      ti /= d
      ret = c*ti*ti*ti*ti + b
    case .easeOutQuart:
      ti /= d
      ti = ti - 1
      ret = -c*(ti*ti*ti*ti - 1) + b
    case .easeInOutQuart:
      ti /= d/2.0
      if ti < 1 {
        ret = c/2.0*ti*ti*ti*ti + b
      } else {
        ti = ti - 2
        ret = -c/2.0 * (ti*ti*ti*ti - 2) + b
      }
    case .easeInQuint:
      ti /= d
      ret = c*ti*ti*ti*ti*ti + b
    case .easeOutQuint:
      ti /= d
      ti = ti - 1
      ret = c*(ti*ti*ti*ti*ti + 1) + b
    case .easeInOutQuint:
      ti /= d/2.0
      if ti < 1 {
        ret = c/2.0*ti*ti*ti*ti*ti + b
      } else {
        ti = ti - 2
        ret = c/2.0 * (ti*ti*ti*ti*ti + 2) + b
      }
    default:
      ti /= d
      ret = c*ti*ti*ti + b
    }

    return ret
  }
}

さすがに全部のイージングは用意していないが、
列挙型と、関数中身のswitchのcaseを増やしていけば、イージングの種類をなんぼでも作れる。

さて、先ほどの、CircleAnimationViewを変えていこう。

//let angle = CGFloat(Double.pi * 2.0 / self.frameCountAll)
let angle = AnimationEasingValue.valueFunc(easeType: .easeOutQuint, t: CGFloat(self.frameCountNow), b: self.endAngleFirst, c: self.maxAngle, d: CGFloat(self.frameCountAll))

これで、円のアニメーションにもイージングをつけることができた。

これ、もちろん円だけでなくあらゆるもののアニメーションに応用できるので、
いろいろ使ってみてほしい。
UIView.animateメソッドでもイージングは効かせられるけど、なんか物足りないって方は、
Timerで、このイージングをつかってみるのもいいかもしれない。

ではまた。

トラックバック(0)

トラックバックURL:

コメントする