Posted at
iOSDay 19

Core Animation 中級編

More than 5 years have passed since last update.

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つは特に有名ですね。ぜひ、お時間のあるときにソースを読んでみましょう。

では、良い月曜日を!