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;
}
ここで、CAAnimation
のbeginTime
は通常、
親の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
)を探索します。
- layer.delegateの
-actionForLayer:forKey:
layer.actions
layer.style
[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.animationKeys
にopacity
キーが登録されつつ、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を使って、起動画面(スプラッシュ)を好きなようにトランジションさせながら
アプリを開始する簡単なクラスを書いたので、良かったら使ってみてください。
その他にも、アニメーションまわりの良ライブラリを、いくつか紹介します。
- https://github.com/warrenm/AHEasing
- https://github.com/dominikhofmann/PRTween
- https://github.com/nicklockwood/iCarousel
- https://github.com/mpospese/MPFoldTransition
下2つは特に有名ですね。ぜひ、お時間のあるときにソースを読んでみましょう。
では、良い月曜日を!