アニメーションするビューを作る【実践的Macintoshプログラミング解説】

印刷用表示 |テキストサイズ 小 |中 |大 |

CoreData版 Repeating Motif Generator の開発 Repeating Motif Wonderland CoreData 実践的 Macintosh プログラミング解説

LinkIconホーム

更新日 2009-05-24

波形をドラッグして発振器のパラメータを設定できる様にする

アニメーションするビューを作る

 波形ドラッグの機能を実装するのはOscillatorViewですが、他のクラスにも変更が波及していきます。

 振幅と位相のずれをスライダーで設定する必要がなくなったので、それらを削除します。したがってEnclosureViewの変更が発生します。こうなると周波数だけスライダーを使う事もないので、一緒に削除してしまいます。

 スライダーを削除すると発振器のパラメータを設定するためのビューが大幅に小さくなります。そこで画面レイアウトを見直す事にしました。発振器のパラメータを設定するビューを小さくして、ミキサーを表示するビューと発振器を表示するビューの間に押し込んでしまう事にします。

oscParam.png ただし、こうなるとパラメータを説明するテキストが邪魔です。このテキストがあると、そこまで小さくなりません。テキストをやめて画像で説明すれば小さくなると思ってこんな画像を作ってみましたが、これでわかってもらえるかどうか、ちょっと自信がありません。(ちなみに上から振幅、周波数、位相のずれです)
 そこで振幅なら振幅が変化するアニメーションをするビューを作る事にしました。これなら何をするパラメータなのか一目瞭然です。

OscillatorParameterViewを作る

 発振器のパラメータをアニメーションで表すビューOscillatorParameterViewを作ります。アニメーションを実行するためにMacOS X 10.4で追加されたNSAnimationクラスを使います。10.4をサポートしたいのでCore Animationは使いません。

OscillatorParameterViewのインターフェイスファイル

 OscillatorParameterViewはNSViewのサブクラスとします。NSAnimationはサブクラスを作らずにそのまま使い、自分をデリゲートに指定する事で定期的にメッセージを受ける様にします。

increasing
行ったり来たりするアニメーションを実行したいので、値が増えているのか減っているのかを表すパラメータが必要です。increasingがYESなら値が増えている事を表します。
kind
振幅、周波数、位相のずれのどのアニメーションを表示するのかを表します。
theAnimation
NSAnimationクラスのインスタンスです。
path
表示に使うベジエパスです。

//
//  OscillatorParameterView.h
//  RepeatingMotifGenerator
//
//  Copyright NovemberKou 2008. All rights reserved.
//

#import <Cocoa/Cocoa.h>


@interface OscillatorParameterView : NSView
{
    BOOL            increasing;
    int             kind;
    NSAnimation     *theAnimation;
    NSBezierPath    *path;
}

- (void)setKind:(int)aKind;

@end

初期化メソッド

 OscillatorViewをドラッグする事ができますよ、という事をアピールするため、マウスポインタがOscillatorViewの中に入ってきたらマウスカーソルの形を変える事にします。マウスカーソルの形を変えるにはNSViewのresetCursorRectsメソッドを使います。デベロッパドキュメントには以下の様に書いてあります。

//
//  OscillatorParameterView.m
//  RepeatingMotifGenerator
//
//  Copyright NovemberKou 2008. All rights reserved.
//

#import "OscillatorParameterView.h"


#define     RESOLUTION  20
#define     PI          acos(-1)

@implementation OscillatorParameterView

#pragma mark -
#pragma mark Initializer and Deallocator

- (id)initWithFrame:(NSRect)frame
{
    int i;

    self = [super initWithFrame:frame];
    if (self)
    {
        increasing = YES;
        theAnimation = [[NSAnimation alloc] initWithDuration:2
                                              animationCurve:NSAnimationLinear];
        for(i=0;i<=10;i++)
            [theAnimation addProgressMark:i*0.1];
        [theAnimation setDelegate:self];
        [theAnimation setAnimationBlockingMode:NSAnimationNonblocking];
    }
    return self;
}

- (void)dealloc
{
    [theAnimation stopAnimation];
    [theAnimation release];
    [super dealloc];
}

振幅パラメータを表すパス

 振幅を変化させるパラメータである事を表すために、正弦波の振幅が変化するアニメーションを表示します。そのために引数のaProgressに応じた大きさの振幅で描かれたベジエパスを作成し、返します。

 2を引いているのは線の太さ分縮めている意味で、最後に1を足しているのはセンタリングの意味です。

//
//  OscillatorParameterView.m
//

#pragma mark -
#pragma mark Animation Path

- (NSBezierPath *)newAmplitudePath:(double)aProgress
{
    int     i;
    NSBezierPath    *newPath = [NSBezierPath bezierPath];
    
    [newPath moveToPoint:NSMakePoint(0, (NSHeight([self bounds])-2)/2+1)];
    for(i=1;i<=RESOLUTION;i++)
    {
        [newPath lineToPoint:
         NSMakePoint(i/(double)RESOLUTION*(NSWidth([self bounds])-2)+1,
                     (NSHeight([self bounds])-2)/2*(1+sin(2*PI*i/(double)RESOLUTION)*aProgress)+1)];
    }
    [newPath setLineWidth:2];
    return  newPath;
}

周波数パラメータを表すパス

 周波数を変化させるパラメータである事を表すために、正弦波の周波数が変化するアニメーションを表示します。そのために引数のaProgressに応じた大きさの周波数で描かれたベジエパスを作成し、返します。

 2を引いているのは線の太さ分縮めている意味で、最後に1を足しているのはセンタリングの意味です。

//
//  OscillatorParameterView.m
//

- (NSBezierPath *)newFrequencyPath:(double)aProgress
{
    int     i;
    NSBezierPath    *newPath = [NSBezierPath bezierPath];
    
    [newPath moveToPoint:NSMakePoint(0, (NSHeight([self bounds])-2)/2+1)];
    for(i=1;i<=RESOLUTION;i++)
    {
        [newPath lineToPoint:
         NSMakePoint(i/(double)RESOLUTION*(NSWidth([self bounds])-2)+1,
                     (NSHeight([self bounds])-2)/2*(1+sin(2*PI*i/(double)RESOLUTION*(1+aProgress)))+1)];
    }
    [newPath setLineWidth:2];
    return  newPath;
}

位相ずれパラメータを表すパス

 位相のずれを変化させるパラメータである事を表すために、正弦波の位相が変化するアニメーションを表示します。そのために引数のaProgressに応じた大きさの初期位相で描かれたベジエパスを作成し、返します。

 2を引いているのは線の太さ分縮めている意味で、最後に1を足しているのはセンタリングの意味です。

//
//  OscillatorParameterView.m
//

- (NSBezierPath *)newPhaseLagPath:(double)aProgress
{
    int     i;
    double  x,y;
    NSBezierPath    *newPath = [NSBezierPath bezierPath];
    
    for(i=0;i<=RESOLUTION;i++)
    {
        x = i/(double)RESOLUTION*(NSWidth([self bounds])-2)+1;
        y = (NSHeight([self bounds])-2)/2*(1+sin(2*PI*i/(double)RESOLUTION+aProgress*1.2*PI))+1;
        if(i==0)
            [newPath moveToPoint:NSMakePoint(x,y)];
        else
            [newPath lineToPoint:NSMakePoint(x,y)];
    }
    [newPath setLineWidth:2];
    return  newPath;
}

デリゲートメソッドを実装する

 プログレスマークが指定した値に達すると、animation:didReachProgressMark:メッセージが送られてきます。increasingフラグがYESなら引数のprogressをそのまま使い、NOなら逆に減っていく様に1-progressをプログレスの値として使ってアニメーション用のパスを作成します。パスはkindによって作り分けます。作成したパスをセットして、再描画して終了です。

 アニメーションが終了するとanimationDidEnd:メッセージが送られてきます。ここで何もしないとアニメーションを一回実行しただけで終わってしまうので、再度スタートをかけます。再スタートをかける前にincreasingフラグを反転して、アニメーションが行ったり来たりする様にします。これでムービーでいうパリンドローム再生(再生、逆再生を繰り返す)の様になります。

//
//  OscillatorParameterView.m
//

#pragma mark -
#pragma mark Animation Delegate methods

- (void)animation:(NSAnimation *)animation didReachProgressMark:(NSAnimationProgress)progress
{
    double  aProgress;
    NSBezierPath    *newPath;

    aProgress = increasing ? progress : 1 - progress;
    switch(kind)
    {
        case 0:newPath = [self newAmplitudePath:aProgress];  break;
        case 1:newPath = [self newFrequencyPath:aProgress];  break;
        case 2:newPath = [self newPhaseLagPath:aProgress];
    }

    [path release];
    path = [newPath retain];
    [self setNeedsDisplay:YES];
}

- (void)animationDidEnd:(NSAnimation *)animation
{
    increasing = !increasing;
    [animation startAnimation];
}

描画、アクセサメソッド

 後はたいした事はないので、特に説明する事はありません。

//
//  OscillatorParameterView.m
//

#pragma mark -
#pragma mark Event Handler

- (void)drawRect:(NSRect)rect
{
    [path stroke];
}

#pragma mark -
#pragma mark Accessor methods

- (void)setKind:(int)aKind
{
    kind = aKind;
    [theAnimation startAnimation];
}

EnclosureViewを変更する

 発振器のパラメータを設定するスライダーを削除したので、EnclosureViewが変更になります。また発振器のパラメータを設定するビューをミキサーのビューと発振器のビューの隙間に押し込む事にしたので、ビューがリサイズされた時の並べ替えをしているコードに追加が発生します。

 OscillatorParameterViewのkindを設定する作業も必要です。

EnclosureViewのインターフェイスファイル

 発振器のパラメータを設定するビューが入ったNSBoxのアウトレットを追加します。これはビューがリサイズされた時の並べ替えをしているコードで使います。

 OscillatorParameterViewのアウトレットを追加します。kindを設定するのに使います。

 振幅、周波数、位相ずれを設定するスライダーを削除するので、そのアウトレットも削除します。

//
//  EnclosureView.h
//  RepeatingMotifGenerator
//
//  Copyright NovemberKou 2008. All rights reserved.
//

#import <Cocoa/Cocoa.h>


@class  OscillatorView, BezierPathView, RMGDocument, OscillatorParameterView;

@interface EnclosureView : NSView
{
    IBOutlet    OscillatorView          *x1View,*x2View,*x3View;
    IBOutlet    OscillatorView          *y1View,*y2View,*y3View;
    IBOutlet    BezierPathView          *viewX,*viewY,*viewMaster;
    IBOutlet    NSView                  *lineX,*lineY;
    IBOutlet    NSView                  *lineX1,*lineX2,*lineX3;
    IBOutlet    NSView                  *lineY1,*lineY2,*lineY3;

    IBOutlet    NSArrayController       *controller;
    IBOutlet    NSButton                *randomizeButton;
    IBOutlet    NSTextField             *amplitudeTextField, *frequencyTextField, *phaseLagTextField;
    IBOutlet    NSSlider                *amplitudeSlider, *frequencySlider, *phaseLagSlider;
    IBOutlet    NSStepper               *amplitudeStepper, *frequencyStepper, *phaseLagStepper;
    IBOutlet    RMGDocument             *document;
    IBOutlet    NSBox                   *oscParam;
    IBOutlet    OscillatorParameterView *amplitudeParameter, *frequencyParameter, *phaseLagParameter;

    int     whiteSpace,viewMargin,lineLength;

    OscillatorView  *selectedView;
}

- (void)selectOscillator:(OscillatorView *)sender;

@end

OscillatorParamaterViewのkindを設定する

 awakeFromNibでアウトレットが有効になるので、ここでOscillatorParamaterViewのkindを設定します。

//
//  EnclosureView.m
//

- (void)awakeFromNib
{
    NSSize  sizeLineX = NSMakeSize(1,lineLength);
    NSSize  sizeLineY = NSMakeSize(lineLength,1);

    selectedView = nil;
    [self selectOscillator:x1View];
    [lineX1 setFrameSize:sizeLineX];
    [lineX3 setFrameSize:sizeLineX];
    [lineY1 setFrameSize:sizeLineY];
    [lineY2 setFrameSize:sizeLineY];

    [self frameDidChange:nil];

    [viewMaster setKeyPath:@"selection" isXView:NO];
    [viewX setKeyPath:@"selection.x" isXView:YES];
    [viewY setKeyPath:@"selection.y" isXView:NO];
    [x1View setKeyPath:@"selection.x.osc1" isXView:YES];
    [x2View setKeyPath:@"selection.x.osc2" isXView:YES];
    [x3View setKeyPath:@"selection.x.osc3" isXView:YES];
    [y1View setKeyPath:@"selection.y.osc1" isXView:NO];
    [y2View setKeyPath:@"selection.y.osc2" isXView:NO];
    [y3View setKeyPath:@"selection.y.osc3" isXView:NO];

    NSArray *views = [NSArray arrayWithObjects:
                      viewMaster, viewX, viewY, x1View, x2View, x3View, y1View, y2View, y3View, nil];
    NSEnumerator    *enumerator = [views objectEnumerator];
    NSView          *view;

    while(view = [enumerator nextObject])
        [view bind:@"markerPosition" toObject:document withKeyPath:@"markerPosition" options:nil];

    [amplitudeParameter setKind:0];
    [frequencyParameter setKind:1];
    [phaseLagParameter setKind:2];
}

スライダー削除に伴う変更

 発振器を切替える際にスライダーのバインディングを切替えているコードがありますが、そこを削除します。

//
//  EnclosureView.m
//

- (void)selectOscillator:(OscillatorView *)sender
{
    if(sender == selectedView)  return;

    [selectedView setSelected:NO];
    [sender setSelected:YES];
    selectedView = sender;

    [self bind:amplitudeTextField   withKeyPath:[self amplitudeKeyPath]];
    [self bind:amplitudeSlider      withKeyPath:[self amplitudeKeyPath]];
    [self bind:amplitudeStepper     withKeyPath:[self amplitudeKeyPath]];
    [self bind:frequencyTextField   withKeyPath:[self frequencyKeyPath]];
    [self bind:frequencySlider      withKeyPath:[self frequencyKeyPath]];
    [self bind:frequencyStepper     withKeyPath:[self frequencyKeyPath]];
    [self bind:phaseLagTextField    withKeyPath:[self phaseLagKeyPath]];
    [self bind:phaseLagSlider       withKeyPath:[self phaseLagKeyPath]];
    [self bind:phaseLagStepper      withKeyPath:[self phaseLagKeyPath]];
}

リサイズ時にビューを並べ替える

 発振器のパラメータを設定するビューを小型化して、ミキサーのビューと発振器のビューに囲まれる位置に配置します。追加したNSBoxのアウトレットを使って配置するコードを追加します。

//
//  EnclosureView.m
//

- (void)frameDidChange:(NSNotification *)aNotification
{
    int         width,height,viewSize,marginWidth,marginHeight;
    NSSize      sizeOfView;
    NSSize      sizeLineX,sizeLineY,longSizeLineX,longSizeLineY;
    NSPoint     originLineX,originLineY;
    NSPoint     originViewX,originViewY,originViewMaster,originRandomize;    
    NSPoint     originViewX1,originViewX2,originViewX3;
    NSPoint     originViewY1,originViewY2,originViewY3;
    NSPoint     originLineX1,originLineX2,originLineX3;
    NSPoint     originLineY1,originLineY2,originLineY3;
    NSPoint     originOscParam;
    
    //  接続線にアンチエイリアスがかかってぼやけない様に、widthとheightを偶数にする
    width = ([self bounds].size.width - whiteSpace*2 - viewMargin*3 - lineLength*2)/8;
    width *= 2;
    height = ([self bounds].size.height - whiteSpace*2 - viewMargin*2 - lineLength*2)/6;
    height *= 2;
    viewSize = width < height ? width : height;
    marginWidth = ([self bounds].size.width - viewSize*4 - viewMargin*3 - lineLength*2)/2;
    marginHeight = ([self bounds].size.height - viewSize*3 - viewMargin*2 - lineLength*2)/2;

    //  Y軸のOscillatorViewを配置する
    originViewY1.x = originViewY2.x = originViewY3.x = marginWidth;
    originViewY3.y = marginHeight;
    originViewY2.y = originViewY3.y + viewSize + viewMargin;
    originViewY1.y = originViewY2.y + viewSize + viewMargin;
    [y1View setFrameOrigin:originViewY1];
    [y2View setFrameOrigin:originViewY2];
    [y3View setFrameOrigin:originViewY3];
    
    //  Y軸のOscillatorViewにつながる接続線を配置する
    originLineY1.x = originLineY2.x = originLineY3.x = originViewY1.x + viewSize + viewMargin;
    originLineY1.y = originViewY1.y + viewSize*0.5;
    originLineY2.y = originViewY2.y + viewSize*0.5;
    originLineY3.y = originViewY3.y + viewSize*0.5;    
    [lineY1 setFrameOrigin:originLineY1];
    [lineY2 setFrameOrigin:originLineY2];
    [lineY3 setFrameOrigin:originLineY3];
    
    //  一番下の接続線を長くする
    longSizeLineY = NSMakeSize(lineLength*2,1);
    [lineY3 setFrameSize:longSizeLineY];
    
    //  Y軸の縦の接続線を配置する
    originLineY.x = originLineY3.x + lineLength;
    originLineY.y = originLineY3.y;
    sizeLineY = NSMakeSize(1,viewMargin*2+viewSize*2);
    [lineY setFrameOrigin:originLineY];
    [lineY setFrameSize:sizeLineY];
    
    //  Y軸のMixerViewを配置する
    originViewY.x = originLineY3.x + longSizeLineY.width;
    originViewY.y = originViewY3.y;
    [viewY setFrameOrigin:originViewY];
    
    //  MasterMotifViewを配置する
    originViewMaster.x = originViewY.x + viewSize + viewMargin;
    originViewMaster.y = originViewY.y;
    [viewMaster setFrameOrigin:originViewMaster];
    
    //  Buttonを配置する
    originRandomize.x = originViewMaster.x + viewSize + viewMargin;
    originRandomize.y = originViewMaster.y;
    [randomizeButton setFrameOrigin:originRandomize];
    
    //  X軸のMixerViewを配置する
    originViewX.x = originViewMaster.x;
    originViewX.y = originViewMaster.y + viewSize + viewMargin;
    [viewX setFrameOrigin:originViewX];
    
    //  発振器のパラメータを配置する
    originOscParam.x = originViewY.x + (viewSize - NSWidth([oscParam bounds]))/2;
    originOscParam.y = originLineY2.y - NSHeight([oscParam bounds])/2;
    [oscParam setFrameOrigin:originOscParam];
    
    //  X軸の横の接続線を配置する
    originLineX.x = originViewX.x - viewSize*0.5 - viewMargin;
    originLineX.y = originViewX.y + viewSize + lineLength;
    [lineX setFrameOrigin:originLineX];
    sizeLineX = NSMakeSize(viewMargin*2+viewSize*2,1);
    [lineX setFrameSize:sizeLineX];
    
    //  X軸の縦の接続線を配置する
    originLineX1.x = originLineX.x;
    originLineX2.x = originLineX1.x + viewSize + viewMargin;
    originLineX3.x = originLineX2.x + viewSize + viewMargin;
    originLineX1.y = originLineX3.y = originLineX.y;    
    originLineX2.y = originLineX.y - lineLength;
    [lineX1 setFrameOrigin:originLineX1];
    [lineX2 setFrameOrigin:originLineX2];
    [lineX3 setFrameOrigin:originLineX3];
    
    //  真ん中の接続線を長くする
    longSizeLineX = NSMakeSize(1,lineLength*2);
    [lineX2 setFrameSize:longSizeLineX];
    
    //  X軸のOscillatorViewを配置する
    originViewX1.x = originViewX.x - viewSize - viewMargin;
    originViewX2.x = originViewX.x;
    originViewX3.x = originViewX.x + viewSize + viewMargin;
    originViewX1.y = originViewX2.y = originViewX3.y = originLineX.y + lineLength + viewMargin;
    [x1View setFrameOrigin:originViewX1];
    [x2View setFrameOrigin:originViewX2];
    [x3View setFrameOrigin:originViewX3];
    
    //  サイズをセットする
    sizeOfView = NSMakeSize(viewSize,viewSize);
    [viewMaster setFrameSize:sizeOfView];
    [viewX setFrameSize:sizeOfView];
    [viewY setFrameSize:sizeOfView];
    [x1View setFrameSize:sizeOfView];
    [x2View setFrameSize:sizeOfView];
    [x3View setFrameSize:sizeOfView];
    [y1View setFrameSize:sizeOfView];
    [y2View setFrameSize:sizeOfView];
    [y3View setFrameSize:sizeOfView];
    [randomizeButton setFrameSize:sizeOfView];
}