Dashboardにあるような裏返しアニメーションをCALayerを用いてやってみる。
単にアニメーションするだけならtransform.rotation.yの値をいじってやれば暗黙的なアニメーションを行うのだが、表と裏で違う情報を表示したい。そこで、CALayerのdoubleSidedにNOを指定すると、レイヤーが裏返しになったときに描画されなくなることを利用して、表面と裏面それぞれにレイヤーを用意し、同時に裏返すアニメーションをすることで実装する。
以下はARC下での実装例。テストはしてない。
@interface MyLayer : CALayer {
@private
BOOL faceup_;
CALayer *frontLayer_;
CALayer *backLayer_;
}
@property BOOL faceup;
@end
@implementation MyLayer
@dynamic faceup;
- (BOOL)faceup {
return faceup_;
}
- (void)setFaceup:(BOOL)faceup {
if (faceup_ == faceup) return;
faceup_ = faceup;
// この設定値は試行錯誤の結果生まれました。
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform.rotation.y"];
anim.fromValue = @(M_PI);
anim.byValue = (faceUp) ? @(M_PI) : @(-M_PI); //表にするのと裏にするので回転方向を変える。
anim.duration = .25;
[frontLayer_ addAnimation:anim forKey:nil];
[backLayer_ addAnimation:anim forKey:nil];
// 新しいfaceupの値によってどちらかを表にする。
[CATransaction setDisableActions:YES];
CATransform3D t;
t = CATransform3DMakeRotation((faceup) ? 0 : M_PI, 0, 1, 0);
t.m34 = 1.0f / -420.0f;
[frontLayer_ setTransform:t];
t = CATransform3DMakeRotation((faceup) ? M_PI : 0, 0, 1, 0);
t.m34 = 1.0f / -420.0f;
[backLayer_ setTransform:t];
[CATransaction setDisableActions:NO];
}
- (id)init {
self = [super init];
if (!self) return nil;
frontLayer_ = [CALayer layer];
backLayer_ = [CALayer layer];
// 裏返し時に描画しない。
frontLayer_.doubleSided = NO;
backLayer_.doubleSided = NO;
// あらかじめ裏返しておく。
[backLayer_ setValue:@(M_PI) forKeyPath:@"transform.rotation.y"];
[self addSublayer:frontLayer_];
[self addSublayer:backLayer_];
return self;
}
- (void)resizeSublayersWithOldSize:(CGSize)size {
frontLayer_.frame = self.bounds;
backLayer_.frame = self.bounds;
}
@end
frontLayerとbackLayerの公開範囲と描画方法は自由。
以下補足。(自分用メモ)
わざわざ明示的なアニメーションを使っているのは回転方向を指定するため。始めは暗黙的なアニメーションを使っていたのだが、frontLayerとbackLayerの回転方向が逆になってしまい、奇妙な動きをしてしまった。そこで明示的なアニメーションで過程を細かく制御し、暗黙的なアニメーションをオフにした。アニメーションのfromValueはfaceupの値で変えたり色々やったが、これで上手くいったのでこれにしておく。
CATransform3Dのm34の値は木下誠氏のLeopardのアニメーションを簡単実装! Core Animationを使いこなすを参考にした。
frontLayerとbackLayerを非公開にしたときに、-[CALayer hitTest:]でfrontLayerやbackLayerが返されるのが不都合な場合は、hitTest:を次の様にオーバーライドする。
- (CALayer *)hitTest:(CGPoint)p {
CALayer *layer = [super hitTest:p];
if (layer == frontLayer_ || layer == backLayer_) return self;
else return layer;
}
何か不安な実装だが、これで動く。