iOS4 での UIView のアニメーション
最近研究室の色々で地図関係の iPhone アプリを開発しているのだけど,Map のように自分の位置に青い丸いのを表示して,その周囲に波紋のようなものをアニメーションで表示したいと思って試行錯誤してみた.
UIView のアニメーションについては,iOS4 以降では animateWithDuration:delay:options:animations:completion などの block-based animation methods が推奨されている.従来のbeginAnimations:context: や setAnimationDuration: や setAnimationTransition:forView:cache: は推奨されていない(discouraged).
// block-based animation methods for iOS4 + animateWithDuration:animations: + animateWithDuration:animations:completion: + animateWithDuration:delay:options:animations:completion: + transitionFromView:toView:duration:options:completion: + transitionWithView:duration:options:animations:completion:
この block-based animation methods では
[UIView animateWithDuration:1.5f // 1.5秒おきに delay:0.0f // 0.0秒後から options:UIViewAnimationOptionRepeat // 永遠に繰り返す |UIViewAnimationOptionCurveEaseOut // 初めは速く終わりは遅くなるような変化 |UIViewAnimationOptionAllowUserInteraction // アニメーション中でもユーザによるViewの操作を可能にする animations:^{ // このブロックの中にアニメーションの最終状態を記述する self.alpha = 0.0; // alphaを0にする self.bounds = CGRectMake(0, 0, 192, 192); // 波紋のサイズを192x192でframe全体に } completion:nil]; // アニメーションが終わっても何もしない self.animating = YES;
こんな感じに1文で簡潔にアニメーションが記述できてしまう.今までの書き方と比べるとかなり良い.
さて,波紋のようにずっと繰り返す場合は options で UIViewAnimationOptionRepeat を指定する.
ここで嵌ったのが,これだけを指定してしまうとアニメーションが終わるまで UI 操作に制御が戻らなくなり,この場合,永遠に戻ってこなくなってしまう.アニメーション処理はどうも main thread で行われるみたいなので,ここの関数だけ別スレッドにしてもすぐに終了して,同じ状態になる.View のトランジションなどではなく,自分の位置を表示しながら他の操作ができないといけないので,これでは困る.
Cocoa の NSAnimationNonblocking みたいなものがないのかなーと blocking みたいなキーワードで探していたけど,見つからなかったのでもう一度 options を読み直したら見つかった.UIViewAnimationOptionAllowUserInteraction というオプション.これでアニメーション中でもユーザによる UI 操作が可能になる.
ちなみに初めは frame の値を変更するようなアニメーションを書いていたのだけど,これだとよく分からない動作が起きてしまう.frame は固定したまま描画領域だけを変えるのが安全だけど,この波紋をタップしたい,ってなったときにどうするんだろう...
以下ソースコード
/* MEMyself */ // 青い丸のマーカー @interface MEMyselfMarkerView : UIImageView { } @end // その周りの波紋 @interface MEMyselfRippleView : UIImageView { BOOL animating_; } @property BOOL animating; - (void)startRippling; @end // 自分自身の位置を表すクラス @interface MEMyself : MEUser { MEMyselfMarkerView *markerView_; MEMyselfRippleView *rippleView_; } @property (nonatomic, retain) MEMyselfMarkerView *markerView; @property (nonatomic, retain) MEMyselfRippleView *rippleView; @end
#import "MEMyself.h" @implementation MEMyselfMarkerView @end @implementation MEMyselfRippleView @synthesize animating=animating_; - (id)initWithImage:(UIImage *)image { self = [super initWithImage:image]; self.animating = NO; return self; } // 波紋のアニメーションを開始する - (void)startRippling { if(self.animating) return; // 初期状態の設定 // 最初は frame の中心に 0x0 のサイズで. self.bounds = CGRectMake(self.frame.size.width/2, self.frame.size.height/2, 0, 0); [UIView animateWithDuration:1.5f // 1.5秒置きに delay:0.0f // 0.0秒後から options:UIViewAnimationOptionRepeat // 永遠に繰り返す |UIViewAnimationOptionCurveEaseOut // 初めは早く終わりは遅くなるような変化 |UIViewAnimationOptionAllowUserInteraction // アニメーション中でもユーザによるViewの操作を可能にする animations:^{ // このブロックの中にアニメーションの最終状態を記述する self.alpha = 0.0; // alphaを0にする self.bounds = CGRectMake(0, 0, 192, 192); // 波紋のサイズを192x192でframe全体に } completion:nil]; // アニメーションが終わっても何もしない self.animating = YES; } @end @implementation MEMyself @synthesize markerView=markerView_; @synthesize rippleView=rippleView_; - (id)init { self = [super init]; self.screenCoord = CGPointMake(160, 240); self.frame = CGRectMake(self.screenCoord.x-16, self.screenCoord.y-16, 32, 32); self.backgroundColor = [UIColor clearColor]; self.markerView = [[MEMyselfMarkerView alloc] initWithImage:[UIImage imageNamed:@"BlueDot.png"]]; self.rippleView = [[MEMyselfRippleView alloc] initWithImage:[UIImage imageNamed:@"BlueDotRipple.png"]]; self.markerView.frame = CGRectMake(self.frame.size.width/2-12, self.frame.size.height/2-12, 24, 24); self.rippleView.frame = CGRectMake(self.frame.size.width/2-96, self.frame.size.height/2-96, 192, 192); [self addSubview:self.rippleView]; [self addSubview:self.markerView]; // ここではまだ superview が無いのでアニメーションを開始できない return self; } // マーカーがどこかのViewに追加されたらアニメーションを開始する - (void)didMoveToSuperview { [self.rippleView startRippling]; }