Rhythm & Biology

Engineering, Science, et al.

CAAnimationでCALayerを動かす

CALayerは単純な位置の移動だけなら、

layer.position = CGPointMake(x, y);

というようにすれば、アニメーションとともに移動してくれます。position以外にも、opacityの変更などでもアニメーションが発生します。
しかし、アニメーションの時間を細かく決めたいといった場合にはCATransactionやCAAnimationを使う必要があります。

CATransactionを使った例。

CAMediaTimingFunction *tf;
tf = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];

[CATransaction begin];
[CATransaction setAnimationDuration:1.0f];
[CATransaction setAnimationTimingFunction:tf];
layer.position = CGPointMake(x, y);
[CATransaction commit];

setAnimationDurationでアニメーションの時間を決め、setAnimationTimingFunctionでアニメーションの速度の変化を決めています。今回はkCAMediaTimingFunctionEaseOutを設定していて、これは時間とともにアニメーションの速度がゆっくりになっていくものです。

CATransactionはかなり簡便に使えますが、アニメーションの終了通知を受け取ることができません(iOS4.0以降であれば、blockを使えばできます)。そこで、今度はCAAnimation(CABasicAnimation)を使ってみます。

- (void)moveLayer {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
    animation.delegate = self;
    animation.fromValue = [NSValue valueWithCGPoint:layer.position];
    animation.toValue = [NSValue valueWithCGPoint:CGPointMake(x, y)];
    animation.duration = 1.0f;
    [layer addAnimation:animation forKey:@"changePosition"];
}

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
    NSLog(@"layer moved");
}

1秒間かけてlayerを元の位置から(x, y)に移動させています。また、デリゲートで終了通知を受け取っています。

これは一見ちゃんと動くように見えますが、実はCAAnimationの場合はlayerのpositionを書き換えず、アニメーションの終了と同時にlayerが元の位置に戻ってしまいます。そこで、移動先で停止させる方法を2つ紹介します。

1つ目。removedOnCompletionとfillModeを使う方法。

- (void)moveLayer {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
    animation.delegate = self;
    animation.fromValue = [NSValue valueWithCGPoint:layer.position];
    animation.toValue = [NSValue valueWithCGPoint:CGPointMake(x, y)];
    animation.duration = 1.0f;
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeForwards;
    [layer addAnimation:animation forKey:@"changePosition"];
}

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
    NSLog("moved to x: %lf, y: %lf", layer.position.x, layer.position.y);
    [CATransaction begin];
    [CATransaction setAnimationDuration:0.0f];
    layer.position = CGPointMake(x, y);
    [CATransaction commit];
}

removedOnCompletionとfillModeを上記のように設定することで、layerをアニメーションの終了位置に固定することが出来ます。しかし、デリゲートメソッドで確認すると分かるのですが、layerのposition自体は書き換わっていません。そのため、改めてposition値を書き換える作業が必要になります。position値の書き換え時には再びアニメーションが動いてしまうので、CATransactionを利用してアニメーション時間を0秒にしています。あまり良いコードとは思えないですね。

2つ目。先にposition値を書き換えてからアニメーションを動かす。

- (void)moveLayer {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
    animation.delegate = self;
    animation.fromValue = [NSValue valueWithCGPoint:layer.position];
    animation.toValue = [NSValue valueWithCGPoint:CGPointMake(x, y)];
    animation.duration = 1.0f;
    layer.position = CGPointMake(x, y);
    [layer addAnimation:animation forKey:@"changePosition"];
}

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
    NSLog("moved to x: %lf, y: %lf", layer.position.x, layer.position.y);
}

発想の転換です。元の位置に戻ってしまうのであれば「元の位置を移動先に」してからアニメーションを動かせばいいわけです。

大量のアニメーションを扱うのであればCore Animationを使うほうが良いのですが、たった1個動かすという程度であれば、Cocoa(UIView)の機能を使ったほうが楽です。分かってしまえばCore Animationも特別複雑には感じませんが。

デベロッパドキュメントにCore AnimationのProgramming GuideとCook Bookがあるので、詳しく知りたいのであればそれを読むべきですね。私もちゃんと読むことにします。