LIFULL Creators Blog

LIFULL Creators Blogとは、株式会社LIFULLの社員が記事を共有するブログです。自分の役立つ経験や知識を広めることで世界をもっとFULLにしていきます。

Facebook Paperトップ画面の動きを再現するために辿った長い道のりと更に遠いゴール

Apple原理主義者であり、Paper原理主義者でもある大坪です。

FacebookのPaperを起動してまず気がつくのがトップ画面のアニメーションでしょう。↓のビデオで親指を下から上に動かすとそれに合わせて、長方形型の写真やら文章がが「ぶわっ」と大きくなるところです。

このように特徴的なUIをもったアプリの発表から、それを再現するコードの公開までの速さには驚かされます。CocoaControlsには同じアニメーションを再現しようとしているプログラムが二つ公開されています。

両方とも希望をあたえてくれると共に、Facebook Paperまでの距離の遠さを思い知らされることにもなる。なぜそう考えるのか、そして私の試みがどの程度進んだのかについて書きます。

最近発表されたMMPaperの方を実際に動かしてみましょう。Facebook paperに比べていくつかの部分が気になります。

  • ズームするときの動きがスムーズではない(To doにもあげられていますが)
  • 指を離した後、自動的にズームしている間、およびそのあと少しの間はジェスチャを受け付けない。つまりジェスチャが時々空振りする。

(他にもありますが、ひとまずここまで)

これらの問題をできるだけ解消することを目指しました。

ーーー

コードはGitHubに置きました。実行する前に"Podfile"のあるディレクトリで"pods install"とするのを忘れないでください。

アプリを起動するとこんな風に動きます

Demo GPaperTrans

起動すると二つボタンが並んだ画面がでてきます。"Responsive"が今回あれこれ工夫したつもりのバージョン、"Standard"が「普通に」実装したバージョンです。"Responsive"のほうが何かとスムーズに動きますよね。ね?ね?(血走った目で懇願)

このデモを見てもなお読み続けてくれる心の広い方がいると信じて細かい説明をします。

ーーー

Paperトップ画面の動きをみると

「これはUICollectionViewを使ったCustom Transitionだ」

と思う。さて、どのような方式で実装するか?方法は私が思いつくところでは二つあり

ちなみに先ほど挙げたMMPaperは後者の方式で実装されています。厳密に比較したわけではないのですが、普通に実装すると UICollectionViewController-to-UICollectionViewControllerのほうがスムーズに動くようです。

私も最初UICollectionViewController-to-UICollectionViewControllerで実装したのですが、後述する理由によりLayout-to-layout を使っています。とはいえ普通に実装したのでは"Standard"を選択した時のような動きにしかならない。ではどうするか。

デモプログラムの"Standard"と"Responsive"の違いは二つで

  • "Standard"ではイメージ表示にUIImageViewを、"Responsive"ではASImageNode(in AsyncDisplayKit)を使っている。
  • UIPanGestureRecognizerがUIGestureRecognizerState.Endedを返してきた後(つまりユーザがスワイプジェスチャを終了した後)"Standard"ではfinishInteractiveTransition()を呼んでいる。"Responsive"ではFacebook Popを使って終了位置までのアニメーションを制御し、その間に新たにタッチが始まった場合にはアニメーションを中断するようにしている。

他の処理は全て同一。ではどのような狙いでこのような「変更」を加えたか。

一点目、レスポンスをあげるため、ジェスチャに対応したセルの変形処理が少しでも軽くなるように、ASImageNodeを用いました。しかし正直に言えば今回のような使い方でどれだけ効果があるのかは今ひとつわかりません。触っていると、確かに少しレスポンスが良いような気がしますが、製作者の贔屓目からもしれません。ASyncDisplayKitのページに単にUIImageViewをASImageNodeに置き換えただけでも一部の処理がバックグラウンドで処理されるため効果がある、と記載されていますが...

二点目。"Standard"ではUIPanGestureRecognizerのコールバック関数で、UIGestureRecognizerのstateがUIGestureRecognizerState.Endedだった時にこんな処理を行っています。

if success {
 self.collectionView?.finishInteractiveTransition()
 self.toBeExpandedFlag = !self.toBeExpandedFlag
}
else{
 self.collectionView?.cancelInteractiveTransition()
}

遷移を完了させたければfinishInteractiveTransition()を呼び、キャンセルならばcancelInteractiveTransition()を呼ぶ。コードは簡単明瞭ですがここに問題がある。前掲のサンプルを解説しているページから引用します。

ここで注意するのは、ジェスチャーの終了からアニメーションの完了までに隙間時間(A)がある事です。progressが0.6等の半端な場合にfinishInteractiveTransitionやcancelInteractiveTransitionを呼び出すと、progress1.0になるまでアニメーションして(体感1秒程度かかるときがある)、その後にcompletionブロックが呼び出されます。
この隙間時間に次のtransitionを開始しようとするとクラッシュします。

引用元:UICollectionViewをジェスチャーで拡大縮小したい(iOS7〜) - Qiita

このサンプルに習い、クラッシュを避けるため"Standard"ではジェスチャーの終了後UIGestureRecognizerを停止しています。こうすると確かにクラッシュしないのですが、その間ユーザの操作を受け付けることができない。そして時としてこのfinishInteractiveTransition()を呼んだことによるアニメーションは見かけより長くかかる。反応しない画面上をひたすらスワイプするのはどう考えても楽しくない。

というわけで"Responsive"では、ジェスチャーを終了した後のアニメーションを自前で実装しています。(参考にしたのはこのサイトUIKitDynamicsを使ってもいいとは思うのですが、ここは同じくFacebookから公開されているPopを使いました。*1

今のプログラムは指を離してから拡大または縮小が確定するまでのわずかな時間に再度スワイプを始めた場合、進行中のアニメーションをキャンセルし、ユーザのタッチに反応します。(わずかな例外を除いて)

正直に書きますが、まだ「不感帯」が残っておりswipeが空振りになることがある。しかし"Standard"より「マシ」になっている、というのはある程度の自信を持って言えます。いや、素直にUICollectionViewController-to-UICollectionViewControllerで実装したの(例えばMMPaper)に比べてどうなんだ、と問われると下を向いてしまいますが..

ーーー

Facebook paperチームのエンジニアはプレゼンでこう述べています。

最初にiPhoneに触った時、まるで魔法のように感じた。Appleは表示するコンテンツとユーザの間にあるバリアを取り除き、ユーザが直接コンテンツを操作することを可能にしたからだ。しかしユーザのジェスチャが認識されなかったり、アニメーションが滑らかでないとそうした魔法は消え去ってしまう。

そうして作られたPaperではすべての動きが軽く、そして連続的に動く。それは初めてIPhoneの慣性スクロールに触った時と同じ驚きを与えてくれる。

今回の試みには「不感帯」の他にも大きな問題が存在しています。Paperでは縦方向の拡大ジェスチャと横方向のスクロールジェスチャを両方同時に行うことができる。今のところこれを実現させる方法がわかりません。未だ撲滅できない「不感帯」があることも考えると、ゴールははるか彼方に霞んでいるとしか思えない。

そもそもFacebookが公開したライブラリを使いながら、依然として存在しているこの距離は一体どういうことか、という根源的な問題からは目をそらし、もう少ししつこく

「魔法」

の再現に努力したいと思っています。

*1:ちなみにUICollectionViewController-to-UICollectionViewControllerで実装すると、指を離した後のアニメーション時にUIGestureRecognizerがそもそも反応してくれません。クラッシュしないのはいいのですが、工夫のしようもない。というわけでLayout-to-layout を使っています。