iOS4 での UIView のアニメーション

http://www.yuyarin.net/screenshot/20100709005019.png

最近研究室の色々で地図関係の 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];
}