サンプルのCore Animation Menuは、Core Animationレイヤを使ってユーザインターフェイスを生成、アニメーション化する簡単なサンプルの選択項目を表示します。100行足らずのコードで、次の機能とデザインパターンを示します。
ビューでのレイヤ階層のルートレイヤのホスティング。
レイヤの作成とレイヤ階層への挿入。
CQCompostiionLayerを使ってQuartz Composerコンポジションをレイヤコンテンツとして表示。
連続的に実行する明示的なアニメーションの使用。
Core Imageフィルタ入力のアニメーション化。
選択項目の位置の暗黙的なアニメーション化。
ビューをホストするMenuViewインスタンスを通じたキーイベントの処理。
Core Animation Menuアプリケーションは、ごく基本的なユーザインターフェイスを提供します。ユーザはメニューから1つだけ項目を選択できます。ユーザはキーボードの上下の矢印でメニューをナビゲートします。選択項目が変わると、選択インジケータ(白い角丸矩形)が新しい位置にスムーズに移動します。選択インジケータには、連続的にアニメーション化されるブルームフィルタが設定されています。これにより、ユーザの注意を引き付けることができます。背景は連続的に実行するQuartz Composerアニメーションです。図 1に、このアプリケーションのインターフェイスを示します。
Menu.nibは非常に単純です。CustomViewのインスタンスは、Interface Builderパレットからドラッグされてウインドウに配置されます。これは、ウインドウ全体を塗りつぶすようにサイズ変更されます。MenuView.hファイルは、Menu.nibウインドウにドラッグすることによってInterface Builderにインポートされます。次にCustomViewが選択され、オブジェクトタイプがMenuViewに変更されます。

ほかのコネクションを作成する必要はありません。nibファイルがロードされると、ウインドウが展開されます。MenuViewも同様です。MenuViewクラスはawakeFromNibメッセージを受け取り、レイヤはそこで構成されます。
Menuアプリケーションのレイヤ階層(レイヤツリーとも呼ばれます)を次に示します。

rootLayerは、QCComposerLayerのインスタンスです。ルートレイヤであるこのレイヤは、MenuViewインスタンスと同じサイズであり、ウインドウがサイズ変更されても変わりません。
menusLayerは、rootLayerのサブレイヤです。これは空のレイヤです。このレイヤには、contentsプロパティとして設定されているものがなく、スタイルプロパティも設定されていません。menusLayerは、単にメニュー項目レイヤのコンテナとして使われます。このアプローチにより、アプリケーションはmenusLayers.sublayers配列内の位置によってメニュー項目のサブレイヤに簡単にアクセスできます。menusLayerはrootLayerと同じサイズで、rootLayerに重なります。これは、現在のメニュー項目を基準にして相対的にselectionLayerを位置決めする場合に、座標系間で変換を行う必要をなくすために意図的になされました。
アプリケーションのnibファイルと全体的なデザインを確認したので、次にMenuViewクラスの実装を詳しく見ていきます。
MenuViewクラスはNSViewのサブクラスで、次の4つのObjective C 2.0プロパティを宣言します。
@property NSIndex selectedIndex — 現在選択されているインデックスを追跡します。
@property(retained) CALayer *menusLayer — メニュー項目をサブレイヤとして含むCore Animationレイヤ。
@property(retained) CALayer *selectionLayer — 選択インジケータを表示するCore Animationレイヤ。
@property(retained) name — メニュー項目として表示される名前の配列。
注: QuartzCore/CoreAnimation.hがインポートされることに注意してください。QuartzCore.frameworkは、Core Animationを使用するすべてのプロジェクトに追加する必要があります。
リスト 1 MenuView.hのリスト
#import <Cocoa/Cocoa.h> |
#import <QuartzCore/CoreAnimation.h> |
// MenuViewクラスは、ウインドウに挿入されるビューの |
// サブクラスであり、rootLayerをホストしイベントに応答する |
@interface MenuView : NSView { |
// 選択されたメニュー項目インデックスを含む |
NSInteger selectedIndex; |
// メニュー項目レイヤを含むレイヤ |
CALayer *menusLayer; |
// 選択項目の表示に使用されるレイヤ |
CALayer *selectionLayer; |
// メニュー項目名の配列 |
NSArray *names; |
} |
-(void)awakeFromNib; |
-(void)setupLayers; |
-(void)changeSelectedIndex:(NSInteger)theSelectedIndex; |
-(void)moveUp:(id)sender; |
-(void)moveDown:(id)sender; |
-(void)dealloc; |
MenuViewクラスはこのアプリケーションの中心的な役割を果たします。MenuViewクラスは、ビューがnibによってロードされると応答して、表示すべきレイヤのセットアップ、アニメーションの作成、選択項目を変更するキーの処理を行います。
MenuView.mの詳細を、次のように分けて見ていきます。
MenuViewのセットアップ
レイヤのセットアップ
選択レイヤの動きのアニメーション化
キーイベントへの応答
クリーンアップ
awakeFromNibメソッドは、Menu.nibがロードされて展開されると呼び出されます。ビューはawakeFromNibでセットアップを完了することを期待されています。
awakeFromNibのMenuViewの実装は、メニュー項目の表示に使う文字列の配列であるnamesを作成します。次にsetupLayersメソッドを呼び出し、ビューのレイヤをセットアップします。
- (void)awakeFromNib |
{ |
names=[[NSArray arrayWithObjects:@"Item 1",@"Item 2", |
@"Item 3",@"Item 4",@"Item 5", |
nil] retain]; |
[self setupLayers]; |
} |
Menuのコードの大半は、setupLayersメソッドに置かれています。このメソッドは次の役割を担います。
rootLayerの作成と初期化
rootLayerをビューのホスト対象のレイヤとして設定
menusLayerの作成と初期化
メニュー項目レイヤの作成と初期化
メニュー項目の配置制約の追加
menusLayerのレイアウト
selectionLayerの作成
selectionLayerの連続的なアニメーションの設定
rootLayerのレイヤツリーへの追加
selectedIndexの初期値の設定
まず、レイヤの位置と間隔に使用する定数が定義されます。
-(void)setupLayers; |
{ |
CGFloat width=400.0; |
CGFloat height=50.0; |
CGFloat spacing=20.0; |
CGFloat fontSize=32.0; |
CGFloat initialOffset=100.0; |
ビューは、最初に上下の矢印イベントを処理できるよう、最初のレスポンダとして設定されなければなりません。
[[self window] makeFirstResponder:self]; |
rootLayerを作成します。これは、アプリケーションのバンドル内に含まれるBackground.qtzファイルを表示するQCCompositionLayerのインスタンスです。
QCCompositionLayer* rootLayer; |
rootLayer=[QCCompositionLayer compositionLayerWithFile: |
[[NSBundle mainBundle] pathForResource:@"Background" |
ofType:@"qtz"]]; |
MenuViewのインスタンスは、rootLayerのレイヤをホストするビューとして設定されます。これら2つの呼び出しの順番は重要です。最初にrootLayerへのレイヤの設定を行い、次にsetWantsLayer:をYESに設定することによって、ビューが作成するレイヤではなく、この例で作成するレイヤが使用されます。これは、レイヤをホストするビューと、レイヤに支えられるビューとの大きな違いです。
[self setLayer:rootLayer]; |
[self setWantsLayer:YES]; |
menusLayerを作成し、この境界矩形をrootLayerの境界矩形に設定します。ここでも、この設定を行うのは、menusLayerのサブレイヤとselectedLayerの両方に同じ座標系を使用できるようにするためです。menusLayerも保持されます。MenuViewはselectedLayerの位置決めの際にこれを必要とします。
menusLayer=[[CALayer layer] retain]; |
menusLayer.frame=rootLayer.frame; |
menusLayerのサブレイヤがCAConstraintLayoutManagerを使って配置されるよう指定します。制約レイアウトにより、レイヤの位置とサイズをその兄弟レイヤとスーパーレイヤを基準にして相対的に指定できます。スーパーレイヤは制約マネージャを使用して設定され、個々のCAContraintインスタンスが作成されて各サブレイヤにアタッチされます。
menusLayer.layoutManager=[CAConstraintLayoutManager layoutManager]; |
menusLayerをrootLayerのサブレイヤとして追加します。
[rootLayer addSublayer:menusLayer]; |
次のコードは、names配列内の項目に対して反復処理を実行し、各名前に新しいCATextLayerを作成し、制約を使ってその位置を定義します。
NSInteger i; |
for (i=0;i<[names count];i++) { |
現在の反復のインデックス位置にある名前を取得します。
NSString *name=[names objectAtIndex:i]; |
menuItemLayerと呼ばれる新しいCATextLayerインスタンスを作成します。メニュー項目の名前にその文字列を設定し、白の32ポイントのLucida-Grandeで表示されるよう指定します。
CATextLayer *menuItemLayer=[CATextLayer layer]; |
menuItemLayer.string=name; |
menuItemLayer.font=@"Lucida-Grande"; |
menuItemLayer.fontSize=fontSize; |
menuItemLayer.foregroundColor=CGColorCreateGenericRGB(1.0,1.0,1.0,1.0); |
menuItemLayerの境界矩形の指定はできません。CATextLayerインスタンスを使用すると、制約マネージャがレイヤの境界矩形と高さを設定する役割を引き受けます。
次に、レイアウトの制約を指定します。まず垂直方向の制約は、スーパーレイヤの上端を基準にして相対的に設定されます。menuItemLayerの上端は、initialOffset(以前に定義済み)と項目間の間隔(同様に以前に定義済み)によってオフセットされ、高さ(同様に以前に定義済み)には名前のインデックスを乗じます。最後の値は、レイヤの座標系がその原点として左下を使用するために反転されます。
[menuItemLayer addConstraint:[CAConstraint |
constraintWithAttribute:kCAConstraintMaxY |
relativeTo:@"superlayer" |
attribute:kCAConstraintMaxY |
offset:-(i*height+spacing+initialOffset)]]; |
2番目の制約では、単にmenuItemLayerオブジェクトをそのスーパーレイヤの中心点を基準にして相対的に水平方向ににセンタリングしています。
[menuItemLayer addConstraint:[CAConstraint |
constraintWithAttribute:kCAConstraintMidX |
relativeTo:@"superlayer" |
constraintWithAttribute:kCAConstraintMidX]]; |
各menuItemLayerは、サブレイヤとしてmenusLayerレイヤに追加されます。
[menusLayer addSublayer:menuItemLayer]; |
} // ループの最後 |
メニュー項目レイヤをすべて設定したので、今度はこれらを配置します。最初のselectionLayerの位置が正しいことを確認する必要があります。
[menusLayer layoutIfNeeded]; |
selectionlayerとして使われるCALayerが作成され設定されました。境界矩形は、以前に定義した幅と高さに設定されます。これは、レイヤがレイヤツリーに追加された後にMenuViewから利用できることを前提としているので保持されています。
selectionLayer=[[CALayer layer] retain]; |
selectionLayer.bounds=CGRectMake(0.0,0.0,width,height); |
selectionLayerは、視覚的なコンポーネントの指定にスタイルプロパティのborderWidth、borderColor、cornerRadiusを使用します。これらのスタイルプロパティは、幅2ポイント、白、そしてselectionLayerの端が完全に丸められるよう角の丸みが設定されています。
selectionLayer.borderWidth=2.0; |
selectionLayer.borderColor=CGColorCreateGenericRGB(1.0f,1.0f,1.0f,1.0f); |
selectionLayer.cornerRadius=height/2; |
selectionLayerは表示されると、1秒ごとに静かに振動します。これはCIBloomフィルタを使用し、0(輝度値なし)から1.5(若干の輝度あり)の間でinputIntensityをアニメーション化して行われます。
フィルタを作成し、デフォルト値を設定して、inputRadiusを5.0と指定します。
CIFilter *filter = [CIFilter filterWithName:@"CIBloom"]; |
[filter setDefaults]; |
[filter setValue:[NSNumber numberWithFloat:5.0] forKey:@"inputRadius"]; |
Core Animationは、CIFilterクラスを拡張してnameプロパティを追加しています。nameプロパティにより、レイヤのフィルタ配列内のフィルタの入力を、キーパスを使ってアニメーション化できます。
[filter setName:@"pulseFilter"]; |
selectionLayerフィルタ配列を、フィルタが含まれるように設定します。
[selectionLayer setFilters:[NSArray arrayWithObject:filter]]; |
振動のアニメーションは、連続的に実行する明示的なアニメーションです。これはCABasicAnimationのサブクラスであり、キーパス、toValue、およびfromValueを指定しなければなりません。
CABasicAnimation* pulseAnimation = [CABasicAnimation animation]; |
キーパスがアニメーション化されるようfilters.pulseFilter.inputIntensityを設定します。これは、フィルタのnameプロパティが使用される場所です。
pulseAnimation.keyPath = @"filters.pulseFilter.inputIntensity"; |
fromValueとtoValueをそれぞれ0と1.5に設定します。.これにより優れた振動効果が得られます。
pulseAnimation.fromValue = [NSNumber numberWithFloat: 0.0]; |
pulseAnimation.toValue = [NSNumber numberWithFloat:1.5]; |
アニメーションは1秒の長さで、無限に反復します。アニメーションが1.5に到達すると、また0に戻り、繰り返されます。次のコードでこれを設定します。
pulseAnimation.duration = 1.0; |
pulseAnimation.repeatCount = 1e100f; |
pulseAnimation.autoreverses = YES; |
アニメーションのtimingFunctionは、アニメーションの値がアニメーションの再生時間中にどのように配分されるかを制御します。ここでは、イーズイン/イーズアウトアニメーションを使用します。これにより、アニメーションが最初は遅く、だんだん速くなり、終了する前には再び遅くなります。
pulseAnimation.timingFunction = [CAMediaTimingFunction functionWithName: |
kCAMediaTimingFunctionEaseInEaseOut]; |
明示的なアニメーションを開始するには、アニメーションをレイヤのアニメーションコレクションに追加する必要があります。これはaddAnimation:forKey:を使用して行われます。キーそのものは、必要に応じてアニメーションを後から削除するための識別子として使用されます。
[selectionLayer addAnimation:pulseAnimation forKey:@"pulseAnimation"]; |
最後にセットアップが完了したらselectionLayerをrootLayerに追加します。
[rootLayer addSublayer:selectionLayer]; |
selectionLayerの最初の位置を設定し、次に最初のselectedIndexを0に設定します。
[self changeSelectedIndex:0]; |
? // setupLayersの最後 |
setupLayersメソッドは、このアプリケーションで群を抜いて長く、複雑です。ただし、各レイヤのセットアップにブレークダウンすることによって、かなり分かりやすくなります。
changeSelectedIndex:メソッドは、新しい値へのselectedIndexの設定、メニュー項目の項目数の範囲内であることの保証、選択レイヤをmenusLayerサブレイヤのselectedIndexの位置を基準にして相対的に配置する責任を持ちます。これにより、選択レイヤは新しい項目が選択されたことを示すためにアニメーション化されます。
-(void)changeSelectedIndex:(NSInteger)theSelectedIndex |
{ |
selectedIndex=theSelectedIndex; |
if (selectedIndex == [names count]) selectedIndex=[names count]-1; |
if (selectedIndex < 0) selectedIndex=0; |
selectionLayer.position=[[[menusLayer sublayers] |
objectAtIndex:selectedIndex] position]; |
}; |
selectionLayerをアニメーション化するのに必要な最後の行では、単にpositionプロパティに新しい値を割り当てているだけであることに注目してください。これは暗黙的なアニメーションの例です。
レイヤはレスポンダの変化に関与しない(つまりイベントを受け付けない)ため、レイヤツリーのレイヤホストとして機能するMenuViewがその役割を引き継ぐ必要があります。moveUp:およびmoveDown:メッセージは、NSResponderによって提供されます。MenuViewはNSResponderの子孫です。moveUp:およびmoveDown:メッセージは、それぞれ上矢印と下矢印が押されると呼び出されます。これらのメソッドを使用すると、アプリケーションは、ユーザによって指定され、再マップされた機能上の任意の矢印キーに従います(keyDown:を実装するよりも簡単です)。
上矢印が使われると、selectedIndexは1つ減らさなければならず、changeSelectedIndex:を呼び出すことによって更新されます。
-(void)moveUp:(id)sender |
{ |
[self changeSelectedIndex:selectedIndex-1]; |
} |
下矢印が使われると、selectedIndexは1つ増やさなければならず、changeSelectedIndex:を呼び出すことによって更新されます。
-(void)moveDown:(id)sender |
{ |
[self changeSelectedIndex:selectedIndex+1]; |
} |
MenuViewが解放されたら、インスタンス変数をクリーンアップする必要があります。menusLayer、selectionLayer、およびnamesは、deallocの実装では自動解放されます。
-(void)dealloc |
{ |
[menusLayer autorelease]; |
[selectionLayer autorelease]; |
[names autorelease]; |
[super dealloc]; |
} |
Last updated: 2007-12-06