Animating UIView

OSXのLeopardからCoreAnimationというフレームワークが追加された。それによってCoverFlowなんかを実現している。それまでは空間的な管理しか行わなかったGUIシステムにおいて、時間の管理もOSが面倒を見てくれるようになったというのが、CoreAnimationのキモなのではないかと思う。

そして、OSX系の流れをくむiPhoneOSにもCoreAnimationは含まれている。iPhoneOSはOSXの紆余曲折のまだ後の方に出たということもあり、構造的にはモバイルOSにしてはかなりモダンな、OSXの成功も失敗も踏まえた上で設計されていると感じることがある。

さて、iPhoneのCoreAnimationだけど、かなりお手軽に使う方法が用意されている。ダイアログをビヨヨンとだしたり、スライドしてカットインさせたり、はたまた二つのViewをクロスディゾルブさせたり。UIViewControllerのトランジション効果なんかにもプリセットのアニメーションパターンが用意されているのだけど、簡単なお約束でアニメーションさせることが出来るので使わない手はない。

今回デモ用にEscapeButtonというジョークソフトを軽く組んでみた(ほんとに数行しか書いてない)。名前からして出落ちな感もあるけど、実装を見つつご紹介しようと思う。

UIViewをアニメーションさせる

EscapeButtonのソースコードはここ(EscapeButton.zip)にある。

CoreAnimationではCALayerクラスおよび、その派生クラスを扱うのだけど、UIViewにはCoreAnimationを意識せずに使う方法が用意されている。基本のお約束の型はこんな感じ。

[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.25];  // アニメーション時間
// ここにアニメーションさせたいUIViewのゴール時の状態を設定する
[UIView commitAnimations];

とりあえず、これだけで指定のUIViewがアニメーションする。たとえばalphaのプロパティ値を0にすればフェードアウトするといった具合に。

ただし、どのプロパティでもアニメーションできるかというと残念ながらそれはできない。ジオメトリ的な事(位置や回転拡大など)、カラー的な事に限られてると考えておいた方が良いだろう(詳しい話はCoreAnimationを見る必要がある)。

さて、EscapeButtonはボタンが押されたらそのボタンが逃げる。いきなりワープするんじゃなくて、するするとアニメーションしつつ上と下を行ったり来たりする。その部分のソースはこんな感じになっている。

- (IBAction)buttonTouchedDown:(id)sender
{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.25];
CGRect frame = _button.frame;
if (!_side)
frame.origin.y = 360;
else
frame.origin.y = 120;
_side = !_side;
_button.frame = frame;
[UIView commitAnimations];
}

ボタンと、このアクションはInterfaceBuilderにおいてUIControlEventTouchDownイベントで接続している。

やっていることは先ほどの基本の型そのままだ。そして、今いたところとは反対側に移動させる。そのためにボタンのframeを取得してY座標を書き換えてセットしている。これだけなんだけど、ボタンを押すとするすると逃げるという操作が実装できてしまう。

チェーンアニメーション

もう少し複雑なアニメーションの実装をしてみよう。今度はUIViewControllerのviewが表示されるときに回転しながら拡大していき、丁度の位置より行き過ぎて戻るというアニメーションだ。

まずは仕込みだ。初期の段階で縮めておき、さらに90度回転させておく。

- (void)viewWillAppear:(BOOL)animated
{
CGAffineTransform t = CGAffineTransformMakeScale(0.1, 0.1);
self.view.transform = CGAffineTransformRotate(t, M_PI_2);
}

UIViewにはtransformというプロパティがあり、そこに2次元変換行列を設定することが出来る。CGAffineTransform〜という関数が用意されていて移動(Translate)、拡大(Scale)、回転(Rotate)が設定できる。回転の角度の指定は度ではなくラジアンだ。まずMakeがついているものでCGAffineTransformを作成し、必要なだけ行列を掛けていく。行列の掛ける順番は意味があるので注意が必要だ(移動してから回転するのと、回転してから移動したのでは結果が異なる)。

そしてViewが表示されたのと同時にアニメーションを開始する。

- (void)viewDidAppear:(BOOL)animated
{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:1];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(_didEndAnimation)];
CGAffineTransform t = CGAffineTransformMakeScale(1, 1);
self.view.transform = CGAffineTransformRotate(t, -M_PI_2);
[UIView commitAnimations];
}

大体は基本の型どおりで、アニメーション終了後に拡大率等倍(=1)、回転がさらに90度過ぎた位置になるようになっている。

基本と違うのはsetAnimationDelegate:とsetAnimationDidStopSelector:が追加されていることで、これは想像の通り、アニメーションが終了したときに指定したターゲットのメソッドが呼ばれるように設定している。

このように終了時にメソッドを呼び出すことによって、例えば禁止していた処理を許可したり、次のアニメーションの再生を開始させたりすることが出来る。今回はさらに0度の位置に戻すようにアニメーションをさせる。

- (void)_didEndAnimation
{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.25];
self.view.transform = CGAffineTransformMakeScale(1, 1);
[UIView commitAnimations];
}

これは基本通り。単位行列にしている(変形無し)。

これで回転しながら拡大し、ちょっと行き過ぎて戻るというアニメーションが完成した。このView Controllerが表示されるときはこのアニメーションが表示される。プリセットのトランジションにはない効果が簡単に実装できる(今回は重ね合わせに関しては実装をサボっている)。

CoreAnimationを直接使うともっと複雑なことが出来るのだけど、UIViewのラップされたインターフェイスでも、結構動きのあるインターフェイスを実現することが出来る。なかなかお手軽なわりには効果は大きいんじゃないだろうか。