オーバーレイ・スクローラーは、MacOSX10.7で大きく変化したユーザーインターフェースの一つです。
今までNSScrollViewのdocumentViewに隣接して配置されていたNSScrollerが、documentViewの上に半透明で重ね置きされるようになりました。
このNSScrollerは、複合ビューであるNSScrollViewに含まれており、特別な苦労もなく簡単に利用することができます。しかし、特殊なスクロール環境を作るために単体でNSScrollerを使おうとすると、単純にNSScrollerをView上に配置するだけで済んでいた以前と比べて、数倍もの手間が掛かことが分かりました。
まず、スクローラーのスタイルとして「オーバーレイ」と「レガシー」が選択できるようになっています。そして、この2つのスタイルを任意のタイミングで切り替えられるようにしなければなりません。
Lionのスクローラーは必ず「オーバーレイ」スタイルになるわけではなく、入力デバイスによってスタイルが変化します。また、システム環境設定によって手動で切り替えることもできます。その際は、アプリケーションの実行中であっても、リアルタイムに設定が反映されなければなりません。例えば、MagicMouseを使っていて突然電池が切れ、普通のUSBマウスに切り替えたとき、スクローラーが隠れたままだとスクロール不能になってしまいます。
この設定変更通知を受け取るために、NSPreferredScrollerStyleDidChangeNotificationが用意されました。現在のスタイル情報は[NSScroller preferredScrollerStyle]で取得できます。
@implementation MyScrollView
@synthesize documentView, scroller;
- (id)initWithFrame:(NSRect)frame
{
...
[[NSNotificationCenter defaultCenter]
addObserver:self selector:@selector(styleChange:)
name:NSPreferredScrollerStyleDidChangeNotification object:nil];
...
}
- (void)styleChange:(NSNotification*)notify
{
NSScrollerStyle style = [NSScroller preferredScrollerStyle];
scroller.scrollerStyle = style;
scroller.frame = scrollerRectForStyle(style);
documentView.frame = documentRectForStyle(style);
[self setNeedsDisplay:YES];
}
スタイルの変更は、スクローラーのプロパティ変更だけにとどまらず、ビューの配置変更を伴います。
このようなスタイル変更だけならまだ楽だったのですが、少し大変なのが「スクローラーを隠す」処理です。
オーバーレイなスクローラーはドキュメントの一部を覆い隠しており、そのためにドキュメントの一部は画面に表示されていても見えず、しかもマウスクリックにも反応しなくなっているのです。オーバーレイなスクローラーは、必要の無い時は隠れていなくてはならないのです。
しかし、スクローラーを隠す処理は自動的には行われません。手動で実装する必要があります。最も簡単な実装方法は-[NSView setHidden:]を使う事です。でも、もう少し本格的に実装しようとしたとき問題となるのが、スクローラーの「ノブ」と「スロット」を独立して透過処理する方法です。
オーバーレイなスクローラーは、常時隠れていて、ビューがスクロールするときのみ「ノブ」が表示され、そこでさらにマウスをスクローラーにフォーカスさせると「スロット」が表示される仕組みです。しかし、以前よりスクローラーのノブとスロットは一体もので、ノブのみを表示させるようなAPIはMacOSX10.7のリファレンスにも載っていません。
このノブとスロットの独立透過処理をプライベートな領域に片足を入れて実装すると以下のようになります。
- (void)hideSlot
{
[NSAnimationContext beginGrouping];
NSAnimationContext* animeContext = [NSAnimationContext currentContext];
[animeContext setDuration:0.25];
id animeProxy = [scroller animator];
NSNumber* alphaValue = [NSNumber numberWithDouble:0.0];
[animeProxy setValue:alphaValue forKey:@"overlayScrollerTrackAlpha"];
[NSAnimationContext endGrouping];
}
これを実行すると、スクローラーのスロットのみが0.25秒かけてなめらかに透明になっていきます。"overlayScrollerTrackAlpha"の代わりに"overlayScrollerKnobAlpha"というキーを使うと、ノブとスロットを合わせた透明度をアニメーション付きで制御できます。
上記のコードは一応プライベートなAPIを一切使用していませんが、"overlayScrollerTrackAlpha"などのキーの使用を許容するかどうかで議論が分かれるかもしれません。
プライベートな領域に一切触れずに実装するなら、以下のようにNSScrollerのサブクラスを作って、drawRect:をオーバーライドしなければなりません。
@interface MyScroller : NSScroller
@property(readwrite) CGFloat knobAlphaValue;
@property(readwrite) CGFloat trackAlphaValue;
@end
@implementation MyScroller
@synthesize knobAlphaValue, trackAlphaValue;
+ (BOOL)isCompatibleWithOverlayScrollers
{
return self == [MyScroller class];
}
- (void)drawRect:(NSRect)dirtyRect
{
[[NSColor clearColor] set];
NSRectFill(NSInsetRect([self bounds], -1.0, -1.0));
CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];
CGContextSaveGState(context);
CGContextSetAlpha(context, trackAlphaValue);
NSRect knobSlotRect = [self rectForPart:NSScrollerKnobSlot];
[self drawKnobSlotInRect:knobSlotRect highlight:NO];
CGContextSetAlpha(context, knobAlphaValue);
[self drawKnob];
CGContextRestoreGState(context);
}
@end
上記のコードでは、knobAlphaValueとtrackAlphaValueの2つのプロパティによって、ノブとスロットの透明度を独立して設定できるようになっています。あとは、NSAnimationを使って透明度を制御すれば、先ほどと同じようなものが実現します。
なお、NSScrollerをオーバーレイ対応でサブクラス化する場合は、上記のような形でisCompatibleWithOverlayScrollersを実装する必要があります。ここでYESを返さないと、オーバーレイは無効です。