iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
Basic Animationは、fromValueとtoValueに設定した二つの値を補間するアニメーションだ。
- (CABasicAnimation *)opacityAnimation
{
CABasicAnimation *animation = [CABasicAnimation animation];
animation.duration = 3.0;
animation.fromValue = [NSNumber numberWithFloat:1.0];
animation.toValue = [NSNumber numberWithFloat:0.0];
return animation;
}
これを適用するとアニメーションが開始されると、不透明から、透明に徐々に変わってゆく。
Keyframe Animationは、パラメータの指定方法によって色々な種類があるのだが、最初は3つ以上の値を補間する方法を紹介する。
- (CAKeyframeAnimation *)opacityAnimation
{
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.duration = 4.0;
animation.values = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.75],
[NSNumber numberWithFloat:0.0], nil];
animation.keyTimes = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.25],
[NSNumber numberWithFloat:0.50],
[NSNumber numberWithFloat:0.75], nil];
animation.timingFunctions = [NSArray arrayWithObjects:
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn],
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut], nil];
return animation;
}
指定する値をvaluesに、その時間をkeyTimesに設定する。これらの値は配列の添字で対応している。そして、補間の効果をtimengFunsionsで設定し、補間という事で、区間単位での指定となる。言葉だけの説明だと分かりにくいので、図にすると以下のとおり。
今回はパスを設定する方法を紹介する。
- (CAKeyframeAnimation *)originAnimation
{
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.duration = 4.0;
animation.path = self.heartPath;
animation.calculationMode = kCAAnimationPaced;
return animation;
}
- (CGMutablePathRef)heartPath
{
if (! _heartPath) {
NSRect frame = self.mover.frame;
_heartPath = CGPathCreateMutable();
CGPathMoveToPoint(_heartPath, NULL, NSMinX(frame), NSMinY(frame));
CGPathAddLineToPoint(_heartPath, NULL,
NSMinX(frame) - NSWidth(frame),
NSMinY(frame) + NSHeight(frame) * 0.85);
CGPathAddLineToPoint(_heartPath, NULL,
NSMinX(frame),
NSMinY(frame) - NSHeight(frame) * 1.5);
CGPathAddLineToPoint(_heartPath, NULL,
NSMinX(frame) + NSWidth(frame),
NSMinY(frame) + NSHeight(frame) * 0.85);
CGPathAddLineToPoint(_heartPath, NULL,
NSMinX(frame),
NSMinY(frame));
CGPathCloseSubpath(_heartPath);
}
return _heartPath;
}
前回のvaluesの代わりに、pathにCGPathRefを設定する。今回の場合、keyTimesは不要なので、無効にする為、calculationModeにkCAAnimationPacedを設定している。
複数のアニメーションをグループ化する事が出来る。
- (CAAnimationGroup *)groupAnimation
{
CAAnimationGroup *animation = [CAAnimationGroup animation];
animation.animations = [NSArray arrayWithObjects:
[self opacityAnimation],
[self originAnimation], nil];
animation.duration = 4.0;
return animation;
}
この場合、アニメーションのキーは、実行する契機という意味になってしまう。
self.mover.animations = [NSDictionary dictionaryWithObjectsAndKeys:
[self groupAnimation], @"alphaValue", nil];
なので、個々のアニメーションは何のパスを変化させる物なのかの指定がインスタンスの生成時に必要になる。
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"alphaValue"];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"frameOrigin"];
以前、紹介したコードから色々変わっているので、全体のコードを掲載する。
#import <QuartzCore/QuartzCore.h>
#import "BaseView.h"
@interface BaseView ()
@property (strong, nonatomic) NSImageView *mover;
@property (assign, nonatomic) NSRect leftFramePosiotion;
@property (assign, nonatomic) NSRect rightFramePosiotion;
@property (assign, nonatomic) BOOL isRight;
@property (assign, nonatomic) CGMutablePathRef heartPath;
- (void)initializeFramePositions;
- (void)addImageToSubview;
- (void)move;
- (CAKeyframeAnimation *)opacityAnimation;
@end
@implementation BaseView
@synthesize mover = _mover;
@synthesize leftFramePosiotion = _leftFramePosiotion;
@synthesize rightFramePosiotion = _rightFramePosiotion;
@synthesize isRight = _isRight;
@synthesize heartPath = _heartPath;
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self initializeFramePositions];
[self addImageToSubview];
[self addSubview:self.mover];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
[self initializeFramePositions];
[self addImageToSubview];
[self addSubview:self.mover];
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
}
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (void)mouseDown:(NSEvent*)theEvent
{
[self move];
}
- (void)initializeFramePositions
{
CGFloat frameX = NSWidth(self.frame);
CGFloat frameY = NSHeight(self.frame);
self.leftFramePosiotion = NSMakeRect(0.0, 0.0, frameX / 4.0, frameY / 4.0);
self.rightFramePosiotion = NSMakeRect(7.0 * frameX / 8.0, 7.0 * frameY / 16.0, frameX / 8.0, frameY / 8.0);
/*
self.mover = [[NSImageView alloc] initWithFrame:self.leftFramePosiotion];
*/
CGFloat xInset = 3.0 * (NSWidth(self.frame) / 8.0);
CGFloat yInset = 3.0 * (NSHeight(self.frame) / 8.0);
NSRect moverFrame = NSInsetRect(self.frame, xInset, yInset);
self.mover = [[MyImageView alloc] initWithFrame:moverFrame];
self.isRight = NO;
self.heartPath = NULL;
/*
self.mover.animations = [NSDictionary dictionaryWithObjectsAndKeys:
[self opacityAnimation], @"alphaValue",
[self originAnimation], @"frameOrigin", nil];
*/
self.mover.animations = [NSDictionary dictionaryWithObjectsAndKeys:
[self groupAnimation], @"alphaValue", nil];
}
- (void)addImageToSubview
{
[self.mover setImageScaling:NSScaleToFit];
[self.mover setImage:[NSImage imageNamed:@"snapshot.jpg"]];
}
- (void)move
{
/*
if (self.isRight) {
[[self.mover animator] setFrame:self.leftFramePosiotion];
}
else {
[[self.mover animator] setFrame:self.rightFramePosiotion];
}
self.isRight = !self.isRight;
*/
[[self.mover animator] setAlphaValue:1.0];
/*
NSRect rect = self.mover.frame;
[[self.mover animator] setFrameOrigin:rect.origin];
*/
}
- (CAKeyframeAnimation *)opacityAnimation
{
//CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"alphaValue"];
animation.duration = 4.0;
animation.values = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.75],
[NSNumber numberWithFloat:0.0], nil];
animation.keyTimes = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.25],
[NSNumber numberWithFloat:0.50],
[NSNumber numberWithFloat:0.75], nil];
animation.timingFunctions = [NSArray arrayWithObjects:
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn],
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut], nil];
return animation;
}
- (CABasicAnimation *)opacityAnimationBasic
{
CABasicAnimation *animation = [CABasicAnimation animation];
animation.duration = 4.0;
//animation.repeatCount = 2;
//animation.autoreverses = YES;
animation.fromValue = [NSNumber numberWithFloat:1.0];
animation.toValue = [NSNumber numberWithFloat:0.0];
return animation;
}
- (CAKeyframeAnimation *)originAnimation
{
//CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"frameOrigin"];
animation.duration = 4.0;
animation.path = self.heartPath;
animation.calculationMode = kCAAnimationPaced;
return animation;
}
- (CGMutablePathRef)heartPath
{
if (! _heartPath) {
NSRect frame = self.mover.frame;
_heartPath = CGPathCreateMutable();
CGPathMoveToPoint(_heartPath, NULL, NSMinX(frame), NSMinY(frame));
CGPathAddLineToPoint(_heartPath, NULL,
NSMinX(frame) - NSWidth(frame),
NSMinY(frame) + NSHeight(frame) * 0.85);
CGPathAddLineToPoint(_heartPath, NULL,
NSMinX(frame),
NSMinY(frame) - NSHeight(frame) * 1.5);
CGPathAddLineToPoint(_heartPath, NULL,
NSMinX(frame) + NSWidth(frame),
NSMinY(frame) + NSHeight(frame) * 0.85);
CGPathAddLineToPoint(_heartPath, NULL,
NSMinX(frame),
NSMinY(frame));
CGPathCloseSubpath(_heartPath);
}
return _heartPath;
}
- (CAAnimationGroup *)groupAnimation
{
CAAnimationGroup *animation = [CAAnimationGroup animation];
animation.animations = [NSArray arrayWithObjects:
[self opacityAnimation],
[self originAnimation], nil];
animation.duration = 4.0;
return animation;
}
@end
今回は、色々な失敗をしてしまった。Cocoa toouchで慣れてしまった関係でOS Xとの違いも戸惑った。
TOPのビューに、同じサイズの二つのビューを入れ替えるサンプルだ。
self.pentagonImageView = [[MyImageView alloc] initWithFrame:self.frame];
self.starImageView = [[MyImageView alloc] initWithFrame:self.frame];
[self.pentagonImageView setImageScaling:NSScaleToFit];
[self.pentagonImageView setImage:[NSImage imageNamed:@"pentagon.png"]];
[self.starImageView setImageScaling:NSScaleToFit];
[self.starImageView setImage:[NSImage imageNamed:@"star.png"]];
最初に五角形のビューをサブビュートしてい追加する。
[self addSubview:self.pentagonImageView];
その際、忘れていけないのは、アニメーションを追加する上位ビューのレイヤーを有効にすること。
[self setWantsLayer:YES];
画面遷移のアニメーションを追加する。
self.animations = [NSDictionary dictionaryWithObjectsAndKeys:
[self transitionAnimation], @"subviews",
nil];
アニメーションの定義は以下のとおり。
- (CATransition *)transitionAnimation
{
CATransition *animation = [CATransition animation];
animation.type = kCATransitionMoveIn;
animation.subtype = kCATransitionFromTop;
return animation;
}
マウス押下されると画面を差し替える。
if (nil != [self.pentagonImageView superview]) {
[[self animator] replaceSubview:self.pentagonImageView with:self.starImageView];
}
else if (nil != [self.starImageView superview]) {
[[self animator] replaceSubview:self.starImageView with:self.pentagonImageView];
}
Core Animationでいうtransitionとは、サブビューの追加と削除時の表示と非表示の入れ替わりを想定しているようだ。
既にサンプル・コードでは利用されているが、アニメーションのtimingFunctionプロパティに設定する、タイミング関数について整理してみる。
タイミング関数は独自に定義できる。
CAMediaTimingFunction # + (id)functionWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y
フリーハンドの部分がきたなくて申し訳ないが、グラフにすると以下の感じ。
ベジエ曲線となるのだが、残念ながら著者にはベジエ曲線を説明できる知識がない。ただ、眺めてみて何となく分かるのは、c1点を手前にすると、最初、急激に変化する。c2点を手前にすると、最後、減速するのだと思う。
独自のタイミング関数の例だ。
- (CATransition *)transitionAnimation
{
CATransition *animation = [CATransition animation];
animation.type = kCATransitionMoveIn;
animation.subtype = kCATransitionFromTop;
animation.timingFunction = [self getTimingFunction];
return animation;
}
- (CAMediaTimingFunction *)getTimingFunction
{
CGFloat c1x = 0.5;
CGFloat c1y = 1.0;
CGFloat c2x = 0.5;
CGFloat c2y = 0.0;
return [[CAMediaTimingFunction alloc] initWithControlPoints:c1x :c1y :c2x :c2y];
}
c1点とc2点の値をどうやって決めればいいのか?先ほどの図を頭に描いてc1点とc2点の値を決めて、動作確認をして期待どおりかを確認していくしかない!?
本日は、Cocoa勉強会が開催された。
発表内容をあげると、RSA公開鍵の作り方、iOS向けGIFライブラリ、Baseによるローカライズ、以前発表されたcontrol enabler開発の近況、ネストしたScroll View、CocoaSQLMapperの紹介、Playフレームワークなどなど。
modulusとpublicExponentの二つのパラメータからRSA公開鍵のバイナリ値を生成する必要があった際に調べた事のメモだ。
RSA公開鍵のASN.1での定義は以下のとおり。
RSAPublicKey ::= SEQUENCE {
modulus INTEGER, -- n
publicExponent INTEGER -- e
}
Apple Developer サイトの資料によると、公開鍵のバイナリ形式で、ASN.1の符号化の一つのDERということなので調べてみた。ただし、ASN.1について全ての情報は膨大なので、RSA公開鍵に関係がある部分のみの抜粋となっている。
SEQUENCEやINTEGER等のオブジェクトの形式は以下のとおり。
タグの番号は以下のとおり。
オブジェクトのタグ欄には、タグ番号がそのまま格納されるのではなくて以下の形式となっている。
7〜8bitのクラスは、汎用型のSEQUENCEとINTEGERのみなので、RSA公開鍵では00となる。
6bitの構造化は、INTEGERは単一型(0)、SEQUENCEは構造型(1)となる。なので、タグ欄にはINTEGERにはタグ番号そのものが設定され、SEQUENCEは、00+1+10000=0x30となる。
長さ欄は、以下のとおり。
NSShadowを使った影をつける例だ。
- (void)applyShadow:(NSView *)view
{
NSShadow *shadow = [[NSShadow alloc] init];
[shadow setShadowOffset:NSMakeSize(10.0, -10.0)];
[shadow setShadowBlurRadius:10.0];
[shadow setShadowColor:[NSColor blackColor]];
[view setShadow:shadow];
}
前回の例は、ウィンド一杯に画像を描画していたので、これに影をつけても分からないので、少し縮めて影をつけてみる。
self.pentagonImageView = [[MyImageView alloc] initWithFrame:NSMakeRect(self.frame.origin.x + 20.0,
self.frame.origin.y + 20.0,
self.frame.size.width - 40.0,
self.frame.size.height - 40.0)];
self.starImageView = [[MyImageView alloc] initWithFrame:NSMakeRect(self.frame.origin.x + 20.0,
self.frame.origin.y + 20.0,
self.frame.size.width - 40.0,
self.frame.size.height - 40.0)];
[self applyShadow:self.pentagonImageView];
[self applyShadow:self.starImageView];
影がついた。