目的
CALayerのcontentsの使い方を理解する。

主要クラス
UIView,CALayer

使用テンプレートプロジェクト
Window-based Application

プロジェクトの名称
Aqua

事前に体験しておくべきドリル

サンプル実装説明


 このドリルではCALayerのcontentsを直接変更する事で、画面上にAquaライクなボタンを描く。
 まずはプロジェクトAquaを作成し、UIViewを継承したAquaButtonViewクラスを作成する。

$テン*シー*シー-5

 まず、よく観察すると、この画像はグラデーションで塗られた領域をラウンドレクトで切り取っている事がわかる。

$テン*シー*シー-1

 このうち、ラウンドレクトで切り取る事はCALayerのcornerRadius、masksToBoundsプロパティを設定してやればいい
@implementation AquaButtonView

- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
CALayer* internalButton = [CALayer layer];
internalButton.frame = self.bounds;
internalButton.cornerRadius = 8;
internalButton.masksToBounds = YES;
[self.layer addSublayer:internalButton];
}
return self;
}
@end

 次に、内容部のグラデーション画像を作ってcontentsプロパティを設定する。contentsに設定できる画像はCGImageRefであるが、これはUIImageのCGImageプロパティで取り出せる。
 そして、このUIImageを作るには以下のようにUIGraphicsBeginImageContextで画像の大きさを指定して、UIGraphicsEndImageContextを呼ぶ前にUIGraphicsGetImageFromCurrentImageContextを呼ぶだけ。
- (UIImage*)createLayerGradImage:(CGSize)contentsSize
{
UIGraphicsBeginImageContext(contentsSize);
UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}

 そして、このUIGraphicsBeginImageContext~UIGraphicsEndImageContextの間におこなうUIKitの描画やUIGraphicsGetCurrentContextによって取り出したCGContextRefによる描画は、すべて作り出すUIImageの画像となる。
- (UIImage*)createLayerGradImage:(CGSize)contentsSize
{
UIGraphicsBeginImageContext(contentsSize);
CGContextRef context = UIGraphicsGetCurrentContext();
ここにグラデーション描画処理を記述

UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}

 グラデーション描画にはCGGradientCreateWithColorsで作成したCGGradientRefを利用する。

$テン*シー*シー-2

 まず、2番目の引数で渡すCFArrayRefにはNSArrayをそのまま渡せる。
 次にNSArrayに登録するCGColorRefはUIColorのCGColorプロパティで取り出せる。
 そして、最初の引数で渡すCGColorSpaceRefはCGColorRefをCGColorGetColorSpaceに渡す事で取り出せる。
 このCGColorSpaceRefとは、作成するグラデーションをどの色空間(色をどのような数値の構成で指定するか)で用意するかを指定するもので、RGB表色系かCMYK表色系か、それともGray系かなどを指定する。

Wiki:色空間

 ここではUIGraphicsBeginImageContextで作ったオフスクリーンへのグラデーションなので、UIColorから取り出したCGColorRef、そこから取り出したCGColorSpaceRefをそのまま渡せばよいことになる。
 以下のようにHSBでグラデーションの各ポイントの色を決めてCGGradientRefを作成した。
float hue = 0.0;
float saturation = 1.0;
float brightness = 1.0;
CGColorRef halationTop = [UIColor colorWithHue:hue
saturation:saturation * 0.2
brightness:brightness
alpha:1].CGColor;
CGColorRef halationBottom = [UIColor colorWithHue:hue
saturation:saturation * 0.8
brightness:brightness * 0.8
alpha:1].CGColor;
CGColorRef normalTop = [UIColor colorWithHue:hue
saturation:saturation
brightness:brightness * 0.8
alpha:1].CGColor;
CGColorRef normalBottom = [UIColor colorWithHue:hue
saturation:saturation
brightness:brightness
alpha:1].CGColor;
NSArray* colors = [NSArray arrayWithObjects:
(id)halationTop,
(id)halationBottom,
(id)normalTop,
(id)normalBottom, nil];
CGFloat locations[] = {0.0, 0.5, 0.5, 1.0};
CGGradientRef gradient = CGGradientCreateWithColors(CGColorGetColorSpace(halationTop),
(CFArrayRef)colors, locations);

 あとはCGContextDrawLinearGradientに上記のCGGradientRefを渡し、locationで指定した0.0~1.0の直線にベクトルを与えてやればグラデーション描画が完成する。

$テン*シー*シー-3

CGPoint gradientStartPoint = CGPointZero;
CGPoint gradientEndPoint = CGPointMake(0, contentsSize.height);
CGContextDrawLinearGradient(context,
gradient,
gradientStartPoint,
gradientEndPoint,
kCGGradientDrawsAfterEndLocation);
// グラデーション情報は用が無くなったので解放。
CGGradientRelease(gradient);
// 画像バッファをCGImageRefに変換。
UIImage* image = UIGraphicsGetImageFromCurrentImageContext();

 各自でgradientStartPoint、gradientEndPointの位置を色々変えて実験して欲しい。

 最後に、このcreateLayerGradImage:メソッドで返されるUIImageからCGImageを取り出しinternalButton.contentsに設定してやればAqua風ボタンの背景が描かれる。
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
CALayer* internalButton = [CALayer layer];
internalButton.frame = self.bounds;
internalButton.cornerRadius = 8;
internalButton.masksToBounds = YES;
internalButton.contents = (id)[self createLayerGradImage:frame.size].CGImage;
internalButton.borderWidth = 1;
[self.layer addSublayer:internalButton];
}
return self;
}

 AquaAppDelegate.mのapplication:didFinishLaunchingWithOptions:で
AquaButtonView* view = [[[AquaButtonView alloc] initWithFrame:
CGRectMake(10, 100, 300, 44)] autorelease];
[self.window addSubview:view];

 とすれば以下の画像が現れる。

$テン*シー*シー-4

 しかしcreateLayerGradImageでframe.sizeを渡す必要はないのではないか、横はすべて同じなのだからCGSizeMake(1, frame.size.height)としてもいいのでは?
 と思う人も多いと思う。実際そのとおりで横幅は1ピクセルで十分。
 そもそも画像のサイズがframe.sizeであるならば、UIImageなど作らずデリゲートパターンやCALayer継承を使えばいい。

 そして、もっと言うならば、このようなグラデーションを描画する専用のCALayer継承クラスはiOS 3.0からすでに存在する。
CAGradientLayer* internalButton = [CAGradientLayer layer];
internalButton.frame = self.bounds;
float hue = 0.0;
float saturation = 1.0;
float brightness = 1.0;
CGColorRef halationTop = [UIColor colorWithHue:hue
saturation:saturation * 0.2
brightness:brightness
alpha:1].CGColor;
CGColorRef halationBottom = [UIColor colorWithHue:hue
saturation:saturation * 0.8
brightness:brightness * 0.8
alpha:1].CGColor;
CGColorRef normalTop = [UIColor colorWithHue:hue
saturation:saturation
brightness:brightness * 0.8
alpha:1].CGColor;
CGColorRef normalBottom = [UIColor colorWithHue:hue
saturation:saturation
brightness:brightness
alpha:1].CGColor;
NSMutableArray *colors = [NSArray arrayWithObjects:
(id)halationTop,
(id)halationBottom,
(id)normalTop,
(id)normalBottom, nil];
NSMutableArray *locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:1.0],nil];
[internalButton setColors:colors];
[internalButton setLocations:locations];
internalButton.cornerRadius = 8;
internalButton.masksToBounds = YES;
internalButton.borderWidth = 1;

 contentsに画像を提供する例としてグラデーションを使ったが、グラデーションに関してはこちらのCAGradientLayerを利用するのが適切。
 self.layerにaddSublayerするのではなくself.layerをCAGradientLayerにしたいならAquaButtonViewのlayerClassをオーバーライドする。
+ (Class)layerClass {
return [CAGradientLayer class];
}

 最後にボタンらしくタイトルを指定できるようにする。iOS 3.2からはCATextLayerも使えるが、そちらを使うより、外部から調整しやすいようにUILabelとしプロパティで公開する。
@interface AquaButtonView : UIView {
}
@property (readonly) UILabel* textLabel;
@end

@implementation AquaButtonView
@synthesize textLabel;

- (UIImage*)createLayerGradImage:(CGSize)contentsSize


[self.layer addSublayer:internalButton];

textLabel = [[[UILabel alloc] initWithFrame:self.bounds] autorelease];
textLabel.font = [UIFont boldSystemFontOfSize:18];
textLabel.textColor = [UIColor whiteColor];
textLabel.textAlignment = UITextAlignmentCenter;
textLabel.shadowColor = [UIColor grayColor];
textLabel.backgroundColor = [UIColor clearColor];
[self addSubview:textLabel];
}
return self;
}

 これでAquaAppDelegate.m側で
view.textLabel.text = @"Aqua風ボタン";

 とすれば以下のようになる。

$テン*シー*シー-5

プロジェクト


検討

 UIButtonのように利用したいなら、UIControlを継承したクラスにこのCAGradientLayerを利用する実装をおこなえばよい。

 興味深い事にCAGradientLayerではcontentsはnilのままで表示がおこなわれる。直接OpenGLのテクスチャを触っているのかもしれない。

 NSArrayをCFArrayRefとして直接渡せる根拠やCGColorRefをNSArrayに渡せる根拠はCocoa Fundation Design ConceptsのToll-Free Bridged Typesを参照。

Note from the example that the memory management functions and methods are
also interchangeable—you can use CFRelease with a Cocoa object and release
and autorelease with a Core Foundation object.