アニメーションのイージングを自由に作る

f:id:vasilyjp:20170914231952j:plain

iOSエンジニアの庄司です。最近Android開発をはじめて、Android Studioのコード補完力の高さに驚かされています。
iOS11のリリースが間近ですが、今回は最近開発したiOSアプリで実装したアニメーションについてご紹介します。

こんなものを作りました

curving_progress_bar.gif
GitHubにサンプルプロジェクトを上げておきました。 https://github.com/WorldDownTown/CurvingProgressBarSample

ポイント残高や、工程の進捗率を表現したりするのに使えるViewです。 一見すると動きはシンプルなのですが、意外と複雑な実装になっているため説明していきます。

このアニメーションの動作ポイントは下記の4点です。

  • 数値がパラパラと増える
  • ゲージが増加する
  • 数値によってテキストとゲージの色が変わる
  • アニメーションにイージングをかける

何が面倒なのか

アニメーション中に色が複数回切り替わる

gage.gif

アニメーションの実装といえば、UIViewのクラスメソッドであるanimate(withDuration:animations:)が手っ取り早い方法です。
ですが、このメソッド実現できるアニメーションはframebackgroundColorなどの状態をA→Bに変更することです。

ゲージの長さを0→100にアニメーションさせている最中に、色が複数回切り替わるような実装はanimate(withDuration:animations:)では実装できません。

UILabelの増加具合にもイージングをかける

label.gif

上で述べたように、UIViewanimate(withDuration:animations:) は状態をA→Bに変更させることができます。
しかし、UILabeltextをパラパラと切り替わるようなアニメーションを実現できません。
そこにさらにイージングをかけようと思うとひと苦労です。

どう解決するか

CADisplayLink

ゲージが増加する途中でゲージや文字の色を変更するためにCoreAnimation.frameworkCADisplayLinkを使いました。
CADisplayLinkは画面のリフレッシュレートと同期して描画させるタイマーオブジェクトです。
ざっくり下記のようなコードになります。 (サンプルプロジェクト上ではこちら)

displayLink = CADisplayLink(target: self, selector: #selector(updateTimer))    // ディスプレイ描画ごとに updateTimer が実行される
displayLink.preferredFramesPerSecond = 60
displayLink.add(to: .current, forMode: .commonModes)
displayLink.isPaused = false // アニメーション開始

@objc private func updateTimer() {
    let duration: TimeInterval = 1.0    // アニメーションは1.0秒
    let elapsed: TimeInterval = Date.timeIntervalSinceReferenceDate - startTimeInterval
    let progress: CGFloat = (elapsed > duration) ? 1.0 : CGFloat(elapsed / duration)  // アニメーション時間の進捗率

    // cubic bezier
    let y: CGFloat = unitBezier.solve(t: progress)    // 0.0〜1.0 下で詳細を説明します
    progressBlock?(y)

    if progress == 1.0 {
        displayLink.isPaused = true    // 一周したらアニメーションを止める
    }
}

ディスプレイの描画ごとに updateTimer() が呼ばれ、アニメーション時間に対する進捗率 (y) を計算してクロージャーに渡します。
クロージャー側では、渡された進捗率を元にゲージの量や色、ラベルのテキストを描画します。

イージングを実装

ゲージの増加だけであれば、CAAnimationCAMediaTimingFunctionを組み合わせて使うことで幾つかの用意されたイージングを実装することができます。
しかし最初のGIFのように、ゲージの増加、色の変更、ラベルテキストの更新をイージングをかけながらアニメーションさせることはできませんでした。

この条件を満たしつつイージングをかけるために、自前のイージング処理を実装しました。 「自前のイージング処理」とは、アニメーションの経過時間を元にアニメーション自体の進捗率を計算する処理のことです。

ベジェ曲線

CAMediaTimingFunctionと同じようなイージングを実装するには、ベジェ曲線の計算をすることになります。 (ベジェ曲線の基本はこちら)
ベジェ曲線といえばUIBezierPathを思い出します。UIBezierPathはベジェ曲線を「描く」ことはできますが、描画したりアニメーションのパスに使うことしかできません。

今回イージングを実装するにあたって、WebKitのベジェ曲線のC++実装をSwiftで書き換えてみました。
処理が複雑で長いので、リンクだけ貼っておきます。
UnitBezier.swift

こんな風に使います。

let curve: AnimationCurve = .easeInOut
let unitBezier = UnitBezier(p1: curve.p1, p2: curve.p2)

@objc private func updateTimer() {
    let elapsed: TimeInterval = Date.timeIntervalSinceReferenceDate - startTimeInterval
    let progress: CGFloat = (elapsed > duration) ? 1.0 : CGFloat(elapsed / duration)

    // アニメーション進捗率を計算
    let animationProgress: CGFloat = unitBezier.solve(t: progress)
    progressBlock?(y)

    if progress == 1.0 {
        displayLink.isPaused = true
    }
}

さらに

サンプルコードには、数種類のアニメーションカーブの種類をenumで用意しています。

enum AnimationCurve {
    case linear, ease, easeIn, easeOut, easeInOut, original(CGPoint, CGPoint)

original(CGPoint, CGPoint) を使って、ベジェ曲線の制御点を設定することで自由にイージング具合を調整することができます。

original.gif

まとめ

今回は複雑な変化を発生させるアニメーションの実装方法を紹介しました。 CADisplayLinkとベジェ曲線をつかってイージングを自由にカスタマイズすることで、複数の要素に多方面にイージングを書けることができるようになります。
ゲージの描画やアニメーション開始までの具体的な処理など、紹介しきれていない実装がいくつもあります。サンプルプロジェクトの方も覗いてみてください。

また、アニメーションについて興味があれば、先月公開されたLottieの記事も御覧ください。

さいごに

弊社では凝ったアニメーション実装が得意なアプリエンジニアを大募集しています。 興味をもっていただけましたら、是非Wantedlyからご応募お願いいたします。