Can CoreText estimate text frame?
おかしい?
CoreTextでテキストをレンダリングするとき,典型的な例として,以下のような流れがあると考える.
- テキストを用意する.
- NSAttributedStringを用意する.
- CTFramesetterを作成する.
- CTFramesetterSuggestFrameSizeWithConstraintsでテキストのレンダリングサイズを計算する.
- (レンダリングサイズに基づいてビューをリサイズする)
- レンダリングサイズに基づいてCTFrameを作成する.
例えば,
NSString *string = @"abcdef \ngh \n ";
という文字列をレンダリングすることを考えよう. 本来ならば,図の一番上のgoodの例のようにレンダリングされるはずです. まずは,どうでもいいメソッドは,以下のように実装する.
- (void)setAttributedString:(NSAttributedString *)attributedString {
_attributedString = attributedString;
[self update];
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// attribute
[[UIColor yellowColor] setFill];
CGContextFillRect(context, _contentRect);
[self drawStringRectForDebug];
// draw text
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0, _contentRect.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CTFrameDraw(_frame, context);
CGContextRestoreGState(context);
}
そして,このupdate
というメソッドが肝となる.
update
メソッドでは,文字列が入力されると,CTFrame
とCTFrameSetter
を作成する.
このupdate
メソッドの中では,CTFramesetterSuggestFrameSizeWithConstraints
が返すサイズに基づき,CTFrame
を作成する..
これが・・・・罠・・・・.
下のコードのようにCTFramesetterSuggestFrameSizeWithConstraints
が返すサイズをそのまま使ってしまうと,図の真ん中のbadのようなレンダリング結果が得られてしまうのだ・・・.
- (void)update {
SAFE_CFRELEASE(_framesetter);
SAFE_CFRELEASE(_frame);
CFAttributedStringRef p
= (__bridge CFAttributedStringRef)_attributedString;
if (p) {
_framesetter = CTFramesetterCreateWithAttributedString(p);
}
else {
p = CFAttributedStringCreate(NULL, CFSTR(""), NULL);
_framesetter = CTFramesetterCreateWithAttributedString(p);
CFRelease(p);
}
CGFloat constrainedWidth = self.frame.size.width;
CGSize frameSize
= CTFramesetterSuggestFrameSizeWithConstraints(
_framesetter,
CFRangeMake(0, _attributedString.length),
NULL,
CGSizeMake(constrainedWidth, CGFLOAT_MAX),
NULL
);
_contentRect = CGRectZero;
_contentRect.size = frameSize;
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, _contentRect);
_frame
= CTFramesetterCreateFrame(_framesetter, CFRangeMake(0, 0), path, NULL);
CGPathRelease(path);
[self setNeedsDisplay];
}
図の一番上のような結果を得るには,CTFramesetterSuggestFrameSizeWithConstraints
に指定した幅をサイズとして入力する必要がある.
CGFloat constrainedWidth = self.frame.size.width;
CGSize frameSize
= CTFramesetterSuggestFrameSizeWithConstraints(
_framesetter,
CFRangeMake(0, _attributedString.length),
NULL,
CGSizeMake(constrainedWidth, CGFLOAT_MAX),
NULL
);
_contentRect = CGRectZero;
_contentRect.size = frameSize;
_contentRect.size.width = constrainedWidth;
これは,実は,CTFramesetterSuggestFrameSizeWithConstraints
が返すサイズが改行の前にある半角,全角スペースをないものとして処理してしまうためである.
CTFrameレンダリングするときには当然空白をレンダリングするためのスペースが必要なのだが,CTFramesetterSuggestFrameSizeWithConstraints
は,横幅として行の最後にある空白を無視したサイズを返すため,空白が入り切らず,図の真ん中のbadのようなレンダリング結果が得られるのである.
本末転倒だが,CTFramesetterSuggestFrameSizeWithConstraints
が返す横幅ですべてのコンテンツをレンダリングしたいなら,以下のように二度サイズを計算すればよい.不毛だが.
CFAttributedStringRef p
= (__bridge CFAttributedStringRef)_attributedString;
if (p) {
_framesetter = CTFramesetterCreateWithAttributedString(p);
}
else {
p = CFAttributedStringCreate(NULL, CFSTR(""), NULL);
_framesetter = CTFramesetterCreateWithAttributedString(p);
CFRelease(p);
}
CGFloat width = self.frame.size.width;
CGSize frameSize = CGRectZero;
frameSize = CTFramesetterSuggestFrameSizeWithConstraints(
_framesetter,
CFRangeMake(0, _attributedString.length),
NULL,
CGSizeMake(width, CGFLOAT_MAX),
NULL
);
_contentRect = CGRectZero;
_contentRect.size = frameSize;
CGFloat temp = frameSize.width;
frameSize = CTFramesetterSuggestFrameSizeWithConstraints(
_framesetter,
CFRangeMake(0, _attributedString.length),
NULL,
CGSizeMake(temp, CGFLOAT_MAX),
NULL
);
_contentRect = CGRectZero;
_contentRect.size = frameSize;
frameSize.width = temp;
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, _contentRect);
_frame
= CTFramesetterCreateFrame(_framesetter, CFRangeMake(0, 0), path, NULL);
CGPathRelease(path);
さて,この動作は多分仕様なんだろうが・・・・・. どう受け止めればいいのやら.