• 238
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

8/19担当、Qiita初投稿の@inamiyです、こんにちは。

今回は、iOS/OSXアプリのUXの根幹である「Core Animation」について、
ハマりやすい点や、意外と知られていないtipsなどについて書きたいと思います。

アニメーション完了時のコールバック

Delegateでif分岐しながら処理する方法もありますが、
[CATransaction setCompletionBlock:]を使う方が、より見通しの良いコードになります。

問題は、アニメーションが正常終了したかどうかのfinishedフラグがないという点ですが、
下記のようなコードを書くと、上手く判別できます。

[CATransaction begin];
[CATransaction setCompletionBlock:^{

    CAAnimation* animation = [layer animationForKey:@"myAnimation"];

    if (animation) {
        // -animationDidStop:finished: の finished=YES に相当

        [layer removeAnimationForKey:@"myAnimation"]   // 後始末
    }
    else {
        // -animationDidStop:finished: の finished=NO に相当
    }

}];

CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"position.x"];
animation.duration = 3;
animation.byValue = [NSNumber numberWithDouble:100.0];

// completion処理用に、アニメーションが終了しても登録を残しておく
animation.removedOnCompletion = NO; 
animation.fillMode = kCAFillModeForwards;

[layer addAnimation:animation forKey:@"myAnimation"];

[CATransaction commit];

もし、CATransactionで毎回wrapするのが気に入らない場合は、
animationのdelegateオブジェクトを保持してblocksを実現する方法もあります。
CAAnimationBlocksというライブラリを使えば、CAAnimation毎に、
-setCompletion:でblockを登録することができます。

アニメーションを一時中断する

Technical Q&A QA1673: How to pause the animation of a layer treeが参考になります。
(下記例は CALayerのカテゴリで実装しています)

@implementation CALayer (TheWorld)

- (void)pauseAnimations 
{
    CFTimeInterval pausedTime = [self convertTime:CACurrentMediaTime() fromLayer:nil];
    self.speed = 0.0;    // 時よ止まれ
    self.timeOffset = pausedTime;
}

- (void)resumeAnimations
{
    CFTimeInterval pausedTime = [self timeOffset];
    self.speed = 1.0;    // そして時は動き出す
    self.timeOffset = 0.0;
    CFTimeInterval timeSincePause = [self convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
    self.beginTime = timeSincePause;
}

ここで、CAAnimationbeginTimeは通常、
親のCAAnimationGroupの時間軸におけるオフセットとして各animationに対して設定しますが、
上記例のようにグループを使わない場合は、レイヤー自体が親になるので、
CACurrentMediaTime()を使ってレイヤー時間に変換して計算する必要があります。

fillModeについて

普段、何気なく使っている(かもしれない)下記のおまじない

// よく分からないけど、アニメーションが終了してもちゃんと残る
animation.removedOnCompletion = NO; 
animation.fillMode = kCAFillModeForwards;

ですが、これもCAAnimationGroupを使って考えると分かりやすいです。

例えば、

CABasicAnimation* animation = [CABasicAnimation animationForKeyPath:@"position.x"];
animation.beginTime = 3;
animation.duration = 5;
animation.byValue = [NSNumber numberWithDouble:100.0];

CAAnimationGroup* group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObject:animation];
group.duration = 10;
[layer addAnimation:group forKey:@"myGroupAnimation"];

を実行した場合、t=3〜8秒の間はアニメーションが実行されて表示されますが、
0〜3秒と8〜10秒は特に指定がなく、あいまいな状態になっています。
この場合、animation.fillModeの設定によって、

  • kCAFillModeRemoved ・・・表示しない(デフォルト)
  • kCAFillModeForwards・・・8〜10秒が終状態として表示される
  • kCAFillModeBackwards・・・0〜3秒が始状態として表示される
  • kCAFillModeBoth・・・backwards+forwards
  • kCAFillModeFrozen・・・(OSX 10.5でDeprecated)

となります。

なので、もしgroupを使わず、かつanimationのbeginTimeがデフォルトの0の
(前節のように、がんばって変換して代入でもしない)場合、
fillModeにforwards以外を設定する意味は特にないので、
基本的に上記おまじないを「使う」か「使わない」かの2択で考えて良いでしょう。
(使う場合は、あとでしっかり後始末しましょう)

暗黙的アニメーションが内部でやっていること

AppleのドキュメントCALayer Class Referenceによると、
暗黙的アニメーション(implicit animation)を実行したとき、
内部ではCALayer-actionForKey:が呼ばれ、
下記の順番でid <CAAction>オブジェクト(実質CAAnimation)を探索します。

  1. layer.delegateの-actionForLayer:forKey:
  2. layer.actions
  3. layer.style
  4. [CALayer defaultActionForKey:]

(途中で[NSNull null]が来ると探索をストップしてnil扱いになる)

もし、id <CAAction>オブジェクトが見つかった場合、-runActionForKey:object:arguments:が実行され、
これが

// selfは(id <CAAction>)オブジェクト、objectはlayer
[object addAnimation:self forKey:actionKey];

相当のコードを実行します。

ここで、Core Animationの特殊な挙動として、
もしCAPropertyAnimation (CAAnimationのサブクラス)に
keyPathが設定されないまま-addAnimation:forKey:が実行された場合、
そこで使われたanimationKeyがそのままkeyPathに代入されるので、例えば、

layer.opacity = 0.5

という暗黙的アニメーションを実行したら、
layer.animationKeysopacityキーが登録されつつ、opacity値を変化させるアニメーションが実行されます。

暗黙的アニメーションをデフォルトでOFFにする

毎回、[CATransaction setDisableActions:YES]を書くのは面倒なので、
前節の手順のどこかで[NSNull null]を渡してあげると良いです。
その場で簡単にセットできるのは、2.の方法になります。

layer.actions = [NSDictionary dictionaryWithObjectsAndKeys:
                 [NSNull null],@"onOrderIn",    // visibleになったときのフェードイン
                 [NSNull null],@"onOrderOut",   // invisibleになったときのフェードアウト
                 [NSNull null],@"sublayers",
                 [NSNull null],@"contents",
                 [NSNull null],@"position",
                 [NSNull null],@"bounds",
                 [NSNull null],@"transform",
                 nil];

なお、UIViewがwrapしているレイヤー(view.layer)が暗黙的アニメーションをしないのは、1.の方法によるもので、
view.layer.delegate(=view)が-actionForLayer:forKey:内で常にNSNullを返すためです。

カスタムプロパティをanimatableにする

CALayerのサブクラスを作り、+needsDisplayForKey:
-actionForKey:-drawInContext:をオーバーライドして、
再描画を繰り返すと、animatableに見せることができます。

@implementation MyLayer

+ (BOOL)needsDisplayForKey:(NSString*)key
{
    if ([key isEqualToString:@"angle"]) {
        return YES;
    }

    return [super needsDisplayForKey:key];
}

- (id <CAAction>)actionForKey:(NSString*)key
{
    if ([key isEqualToString:@"angle"]) {

        CABasicAnimation* animation = [CABasicAnimation animation];
        animation.duration = 1; // デフォルトを通常(0.25)より少し遅めにセット
        return animation;

    }

    return [super actionForKey:key];
}

- (void)drawInContext:(CGContextRef)context
{
    // angle値に応じて描画する
}

@end

おわりに(ライブラリ紹介)

以前、Core Animationを使って、起動画面(スプラッシュ)を好きなようにトランジションさせながら
アプリを開始する簡単なクラスを書いたので、良かったら使ってみてください。

その他にも、アニメーションまわりの良ライブラリを、いくつか紹介します。

下2つは特に有名ですね。ぜひ、お時間のあるときにソースを読んでみましょう。

では、良い月曜日を!

この投稿は iOS Advent Calendar 2012 / Aug19日目の記事です。