※この記事は7年以上前の記事です。
現在は状況が異なる可能性がありますのでご注意ください。
どうも、なぜだか本当に駐車場が見つからないみやびです。
どうなってるの、札幌中央区!?
さて、表記の件。
よくある円グラフにアニメーションをつけるってやつなのですが、
下記エントリーが非常に参考になった。
(というか、ほぼほぼパクり)
さすが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で、このイージングをつかってみるのもいいかもしれない。
ではまた。
コメントする