iOSエンジニアの庄司です。最近Android開発をはじめて、Android Studioのコード補完力の高さに驚かされています。
iOS11のリリースが間近ですが、今回は最近開発したiOSアプリで実装したアニメーションについてご紹介します。
こんなものを作りました
GitHubにサンプルプロジェクトを上げておきました。
https://github.com/WorldDownTown/CurvingProgressBarSample
ポイント残高や、工程の進捗率を表現したりするのに使えるViewです。 一見すると動きはシンプルなのですが、意外と複雑な実装になっているため説明していきます。
このアニメーションの動作ポイントは下記の4点です。
- 数値がパラパラと増える
- ゲージが増加する
- 数値によってテキストとゲージの色が変わる
- アニメーションにイージングをかける
何が面倒なのか
アニメーション中に色が複数回切り替わる
アニメーションの実装といえば、UIView
のクラスメソッドであるanimate(withDuration:animations:)
が手っ取り早い方法です。
ですが、このメソッド実現できるアニメーションはframe
やbackgroundColor
などの状態をA→Bに変更することです。
ゲージの長さを0→100にアニメーションさせている最中に、色が複数回切り替わるような実装はanimate(withDuration:animations:)
では実装できません。
UILabelの増加具合にもイージングをかける
上で述べたように、UIView
のanimate(withDuration:animations:)
は状態をA→Bに変更させることができます。
しかし、UILabel
のtext
をパラパラと切り替わるようなアニメーションを実現できません。
そこにさらにイージングをかけようと思うとひと苦労です。
どう解決するか
CADisplayLink
ゲージが増加する途中でゲージや文字の色を変更するためにCoreAnimation.framework
のCADisplayLinkを使いました。
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
) を計算してクロージャーに渡します。
クロージャー側では、渡された進捗率を元にゲージの量や色、ラベルのテキストを描画します。
イージングを実装
ゲージの増加だけであれば、CAAnimationとCAMediaTimingFunctionを組み合わせて使うことで幾つかの用意されたイージングを実装することができます。
しかし最初の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)
を使って、ベジェ曲線の制御点を設定することで自由にイージング具合を調整することができます。
まとめ
今回は複雑な変化を発生させるアニメーションの実装方法を紹介しました。
CADisplayLink
とベジェ曲線をつかってイージングを自由にカスタマイズすることで、複数の要素に多方面にイージングを書けることができるようになります。
ゲージの描画やアニメーション開始までの具体的な処理など、紹介しきれていない実装がいくつもあります。サンプルプロジェクトの方も覗いてみてください。
また、アニメーションについて興味があれば、先月公開されたLottieの記事も御覧ください。
さいごに
弊社では凝ったアニメーション実装が得意なアプリエンジニアを大募集しています。 興味をもっていただけましたら、是非Wantedlyからご応募お願いいたします。