おばんです、Oxygen Not Includedにハマってしまって、年末年始を溶かすことが確定している田中です。
先日リリースしたBOOTH iOS v2.18.0から購入完了時にアニメーションが再生されるようになりました。 今回はその実装に関する話として以下をまとめていきます。
- 作ったもののドヤリング
- Lottieとは
- BOOTH iOSの購入完了アニメーション実装のざっくり解説
- アニメーションの実装時にハマったポイントの解説
作ったもののドヤリング
ドヤアアアァァァァ!
実装に踏み切った経緯としては、「もともとのBOOTHの購入完了画面が殺風景だった」&「これから機能改修が入る」という話があり、機能改修に合わせてデザインもハレ感を出して修正しよう!となったことからでした。
購入するという大切な瞬間をよりリッチなアニメーションによって演出してあげたい想いと、創作好きのユーザーを相手にしているのだからアプリの作り手も楽しいと思えるデザインを取り入れたいという開発側の想いがありました。
ちょうどチームにインタラクションデザインに強いメンバーがいたため、このようなハレやかなアニメーションを実現できました🎉
Lottieとは
AirbnbがOSSとして提供している、Android, iOS, React Native, Web向けのアニメーションパースライブラリです。
Adobe After Effectsで作成したアニメーションはjson形式でファイル出力が可能です。 Lottieはそのjsonファイルをパースして、作成したアニメーションを再生させます。
iOS用ライブラリのGitHub Repositoryは以下です。
airbnb/lottie-ios: An iOS library to natively render After Effects vector animations
非常に簡単な例として、次のようなコードだけでアニメーションを再生できます。
ね、簡単でしょう?
BOOTH iOSの購入完了アニメーション実装のざっくり解説
使った技術
- 地道なframe計算
- lottie-ios
- CGAffineTransform
- UIView.animate()
- CATextLayer
実際にはLottie以外の既存のiOS技術の利用が多かったです。 しかしLottieの制約×既存のiOS技術の制約を組み合わせたパターンの紹介がネットを調べてもあまり出てこなかったので参考になればと思い、まとめるに至りました。
簡単な構成の説明
アニメーションは二段階に別れています。 一つはLottieのアニメーションで、続いてLottieのアニメーションが完了したタイミングで行われるUIView.animateによるアニメーション。 UIView.animateではUICollectionViewの座標移動や、LOTAnimationView(Lottieのアニメーション再生用のView)の座標移動と縮小を行なっています。
初期状態ではNSLayoutConstraint.constantを画面のheightに合わせて、UICollectionViewを画面外に移動させておきます。 画面中央にはLOTAnimationViewとCATextLayerをFrame Layoutし、画面中央にaddSubviewします。 Frame Layoutでなければいけない理由と、CATextLayerを利用している理由はのちほど説明します。
Lottieのアニメーション完了後には、UICollectionViewを画面一番上まで移動させています。 この時、CATextLayer on UIViewとLOTAnimationViewはアニメーションを完了時にremoveFromSuperviewさせて、 UICollectionViewHeader上に置かれたUIImageViewとUILabelのisHiddenをfalseにして、レイアウトにおける見かけ上の入れ替えを行なっています。
実装時にハマったポイント
ただLottieを使ってアニメーションを再生するだけならば、Web上に無限にサンプルが落ちていて、公式からも多くの説明があるため簡単です。 しかし今回はLOTAnimationViewのサイズ縮小と移動アニメーションと、それに付随して他のUI要素もアニメーションさせる必要があったため、コンビネーションによる複雑さがあり、難しかったです。
コンビネーションでハマったポイントを、一つずつ解説していきます。
UICollectionViewHeaderの動的なサイズ変更
UICollectionViewHeaderのサイズは以下のUICollectionViewDelegateFlowLayoutによって処理されます。 これはDelegateパターンによって処理されるUIKitの設計に依存した作りです。
Delegateパターンでサイズ指定のメソッドが定義されているため、UIView.animateによる動的なアニメーション処理で高さを変更することが困難でした。 これは以下の方法で対処しました。
- UICollectionView自体を最初は画面外に配置し、safeAreaのtopに貼り付けたNSLayoutConstraint.constantの値をUIView.animateのクロージャ内で操作してアニメーションさせた。
- UICollectionViewHeader上のUI要素が表示されたままだと、画面中央に配置されたLOTAnimationViewとCATextLayerに表示されている要素と同じものが同時に見えてしまうので、isHiddenプロパティを操作してアニメーション完了時に指定座標に画像とテキストが表示されているように工夫した。
LOTAnimationViewのサイズ変更によるアニメーションのサイズ変更
LOTAnimationView.playのアニメーション完了クロージャの中でUIView.animateを実行した際に、NSLayoutConstraint.constantの値を変更させた場合に、サイズ変更がアニメーションとともに行われない問題がありました。
例えば以下のコードように、アニメーションクロージャ内で1.0秒かけてLOTAnimationViewのサイズ変更をしたいような場合です。
topやleadingなどのNSLayoutConstraint.constantの操作をして座標を移動することは成功しました。 しかしwidthやheightのサイズ系のNSLayoutConstraint.constantを操作した場合に、1.0秒かけたアニメーション内で処理を行わず、即時に指定したサイズに変更されてしまうということが起きました。
最終的には座標移動とアニメーションのサイズ変更を同時に行う必要があったため、Auto LayoutからFrame Layoutに切り替えて、CGAffineTransformを使って座標移動と縮小の行列計算を行なって解決させました。
CGAffineTransformの行列計算の罠
これは自分が行列という概念を正しく理解できていないために起きた問題です。
行列計算する順序によって結果が変わるという罠を踏みました。 これによって移動量の計算が狂ってしまい、animationViewが意図しない座標に移動してしまい、苦労しました。
正:
誤:
これは宿題です。行列勉強します。
文字の色をアニメーションで変更する
UILabel.textColorはanimatableなプロパティではないため、UIView.animateで時間経過と共に色を変更させるアニメーションができないことがわかりました。 そのため、CATextLayerを用いて以下のように文字色の変更アニメーションを行いました。
実装時に参考になったサイト
- Lottieでアプリにアニメーションを組み込む話(iOSプログラマー編) • Yuta Tokoro
- ios - How to change the text color in a CATextLayer in Swift - Stack Overflow
さいごに
今回ハマった問題とそれに対する実装の工夫のようなものは、調べてもなかなか記事が出てこなかったので挑戦しがいがありました。
実装頑張ってので、ぜひBOOTH iOSで購入してみてください。:)
年末年始は技術同人誌を買って読むのはいかがでしょう。 電子書籍提供されている技術同人誌であれば、買ってすぐ読めるのでオススメです。
BOOTH iOSアプリはどうやって有料ダウンロード商品の販売を解禁したか? #booth_pm #booth - pixiv inside
もし今回紹介したやり方よりも良いものがある場合、ぜひTwitterなどで教えて欲しいです🙏(@ktanaka117)
あるいはBOOTHではモバイルエンジニアを募集していますので、ぜひこちらから応募して直してください!!!💪