LoginSignup
30
27

More than 5 years have passed since last update.

CAShapeLayerで折れ線グラフをアニメーション

Posted at

CGPathそのものをアニメーションしたいけれど……

iOSで自力で折れ線グラフを書こうと思うと、Core Graphicsの出番です。
ただ、今回やりたいのは

折れ線グラフを値の変更に応じて動的にアニメーションさせる

ということでしたので、少し調べてみました。

静的な折れ線グラフを書くだけであれば、方法はいくつかありますが、
私はグラフを表示するUIViewdrawRectで以下のように書いていました。

PlotView.swift
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を繋いでいく方法です。
しかし、これをアニメーションしたい、となった途端に方法がわからなくなりました。
アニメーションするとしても、UIViewCALayerのアニメーションは、基本的にはそれらのプロパティ変化前と変化後の値を与えて、その変化をアニメーションさせるというもの。上記の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で拡大・縮小をアニメーションさせたい場合、

ScaleAnimController.swift
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に与えることで実現します。大変わかりやすいです。さて、CAShapeLayerpathプロパティがあるということは、この「変化前」「変化後」にPathを渡すことで、自動的にCore Animationがアニメーションを補完してくれるかもしれない、という推測が成り立つわけです。こんな感じです。

PlotViewController.swift
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)
}

そしてCAShapeLayerpathプロパティの型はCGPathRefですので、あらかじめ作っておいたCGPathRefを渡せば良いのでは、という話です。

PlotViewController.swift
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を渡しています。内部的にUIBezierPathCGPathRefをラップするような形で保持していそうだという推測はできますが、これでうまくアニメーションできるというのは、なかなか不思議です。ま、便利なのでこのままいきます。

30
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
27