#CGPathそのものをアニメーションしたいけれど……
iOSで自力で折れ線グラフを書こうと思うと、Core Graphicsの出番です。
ただ、今回やりたいのは
折れ線グラフを値の変更に応じて動的にアニメーションさせる
ということでしたので、少し調べてみました。
静的な折れ線グラフを書くだけであれば、方法はいくつかありますが、
私はグラフを表示するUIView
のdrawRect
で以下のように書いていました。
override func drawRect(rect: CGRect) {
let context :CGContextRef! = UIGraphicsGetCurrentContext()
CGContextSetLineWidth(context, 2.0)
// 設定開始
CGContextBeginPath(context)
// グラフ上の座標を設定する
CGContextMoveToPoint(context, 0, 100)
CGContextAddLineToPoint(context, 50, 160)
CGContextAddLineToPoint(context, 100, 120)
// 描画
CGContextStrokePath(context)
}
よくある、CGContext〜
で始まる関数を使ってガリガリPathを繋いでいく方法です。
しかし、これをアニメーションしたい、となった途端に方法がわからなくなりました。
アニメーションするとしても、UIView
やCALayer
のアニメーションは、基本的にはそれらのプロパティ変化前と変化後の値を与えて、その変化をアニメーションさせるというもの。上記のPathそのものを動かすという形でのアニメーションのやり方が思いつきませんでした。
また、CALayer
のプロパティも試しに見てみましたが、Pathを扱えるプロパティが見つかりません。
CABasicAnimation
を使えそうだという見当はついても、それに渡すkeyPath
が分かりません。
#CALayerのサブクラスたち
調べているうちに、CALayerにはたくさんサブクラスがあることが分かりました。
合計14個がAppleのリファレンスには書かれています。
AVCaptureVideoPreviewLayer
AVPlayerLayer
AVSampleBufferDisplayLayer
AVSynchronizedLayer
CAEAGLLayer
CAEmitterLayer
CAGradientLayer
CAMetalLayer
CAReplicatorLayer
CAScrollLayer
CAShapeLayer
CATextLayer
CATiledLayer
CATransformLayer
CABasicAnimation
に渡すkeyPath
は文字列で、基本的にはCALayer
のプロパティ名を書きますが、当然ながら、CALayer
のサブクラス特有のプロパティを渡すこともできるわけです。もちろん、渡すことができても、そのプロパティでアニメーション可能だとは限りませんが。
上記のサブクラスのうち、CAShapeLayer
が、Pathを扱うためのものです。プロパティにも、そのまんまpath
という名前のものがあります。これを使えば、Pathをまるごとアニメーションさせることができるかもしれない……
どういうことかというと、例えばCALayerで拡大・縮小をアニメーションさせたい場合、
func scale() {
// アニメーションオブジェクトを作成する
let anim = CABasicAnimation(keyPath:"contentsScale")
anim.fromValue = @1.0 //変化前の倍率
anim.toValue = @3.0 //変化後の倍率
anim.duration = 0.5
// Layerにアニメーションを設定する
let layer = CALayer()
layer.addAnimation(anim, forKey:nil)
// 本体ViewにLayerを追加
// ここでアニメーションが開始される
self.view.layer.addSubLayer(layer)
}
みたいな感じで、「変化前」「変化後」の値をCABasicAnimationに与えることで実現します。大変わかりやすいです。さて、CAShapeLayer
にpath
プロパティがあるということは、この「変化前」「変化後」にPathを渡すことで、自動的にCore Animationがアニメーションを補完してくれるかもしれない、という推測が成り立つわけです。こんな感じです。
func animPlot() {
// アニメーションオブジェクトを作成する
let anim = CABasicAnimation(keyPath:"path")
anim.fromValue = 変化前のPath
anim.toValue = 変化後のPath
anim.duration = 0.5
// Layerにアニメーションを設定する
let layer = CALayer()
layer.addAnimation(anim, forKey:nil)
// 本体ViewにLayerを追加
// ここでアニメーションが開始される
self.view.layer.addSubLayer(layer)
}
そしてCAShapeLayer
のpath
プロパティの型はCGPathRef
ですので、あらかじめ作っておいたCGPathRef
を渡せば良いのでは、という話です。
func animPlot() {
// 変化前のグラフのPathを作成する
let path1 = UIBezierPath()
path1.moveToPoint(CGPoint(x:0, y:100))
path1.addLineToPoint(CGPoint(x:50, y:160))
path1.addLineToPoint(CGPoint(x:100, y:120))
// 変化後のグラフのPathを作成する
let path2 = UIBezierPath()
path2.moveToPoint(CGPoint(x:0, y:80))
path2.addLineToPoint(CGPoint(x:50, y:200))
path2.addLineToPoint(CGPoint(x:100, y:90))
// アニメーションオブジェクトを作成する
let anim = CABasicAnimation(keyPath:"path")
anim.fromValue = path1 // 変化前のPath
anim.toValue = path2 // 変化後のPath
anim.duration = 0.5
// Layerにアニメーションを設定する
let layer = CALayer()
layer.addAnimation(anim, forKey:nil)
// 本体ViewにLayerを追加
// ここでアニメーションが開始される
self.view.layer.addSubLayer(layer)
}
ドキドキしながら試してみましたが、見事に自分の求めていたアニメーションの動きになりました!!
グラフであれば、元々の値が変化したとしても、グラフ上の点の数はだいたい同じでしょうし、補完もこちらの意図と異なる結果になる余地が少ないので、だいたいの場合において満足いく結果になるのではと思います。ただ、CABasicAnimation
の変化前と変化後には(論理的には)任意のPathを渡すことができるはずなので、全然異なる種類のPathからPathへのアニメーション時に、どのような補完がなされるのかは未知数です(どなたか試してみてくださいな)。
#追記
付け加えると、上記のコードをきちんと読んでいただいた型はお気づきでしょうが、path
プロパティはCGPathRef
型だと書いておきながら、実はUIBezierPath
を渡しています。内部的にUIBezierPath
がCGPathRef
をラップするような形で保持していそうだという推測はできますが、これでうまくアニメーションできるというのは、なかなか不思議です。ま、便利なのでこのままいきます。