Core Text
iOS中的文本绘制底层就是用Core Text来实现的。在iOS6 以前,使用Core Text来绘制不同样式的文本是唯一的选择,直到iOS6 添加了NSAttributedString类,封装了一些有用的方法,我们才能直接、方面地绘制自己想要的样式文本。但是仍然有些特殊的需求只有Core Text才能做到,Core Text是C语言写的框架,虽然有点难懂,但是并不复杂。
其中一个很好的例子就是使用Core Text可以在字体家族中进行字体的转换,NSAttributedString无法做到。在Core Text中,一个字体就是一个CTFont(CTFontRef),这个不能桥接(转换)到UIFont。但是,Core Foundation中的CFAttributedString和 NSAttributedString是可以互相转换的。
我先创建一个可变的属性字符串:
NSString* s = @"Yo ho ho and a bottle of rum!";
NSMutableAttributedString* mas =
[[NSMutableAttributedString alloc] initWithString:s];
接下来我给这字符串添加一些属性,同样的字体,但是每个字符有不同的大小。注意,创建一个CTFont时提供的名称必须是PostScript名称。有一个免费的app,叫Typefaces,列举了一个设备中的字体对应的PostScript名称:
__block CGFloat f = 18.0;
CTFontRef basefont = CTFontCreateWithName((CFStringRef)@"Baskerville", f, nil);
[s enumerateSubstringsInRange:NSMakeRange(0, [s length])
options:NSStringEnumerationByWords
usingBlock:
^(NSString *substring, NSRange substringRange, NSRange encRange, BOOL *stop) {
f += 3.5;
CTFontRef font2 = CTFontCreateCopyWithAttributes(basefont, f, nil, nil);
NSDictionary* d2 =
@{(NSString*)kCTFontAttributeName: CFBridgingRelease(font2)};
[mas addAttributes:d2 range:encRange];
}];
最后,我要让最后一个单词变成粗体。获取最后一个单词的范围的最简单的方法就是从后面遍历,然后在一个单词后停止遍历:
[s enumerateSubstringsInRange:NSMakeRange(0, [s length])
options: (NSStringEnumerationByWords |
NSStringEnumerationReverse)
usingBlock:
^(NSString *substring, NSRange substringRange, NSRange encRange, BOOL *stop) {
CTFontRef font2 =
CTFontCreateCopyWithSymbolicTraits (
basefont, f, nil, kCTFontBoldTrait, kCTFontBoldTrait);
NSDictionary* d2 =
@{(NSString*)kCTFontAttributeName: CFBridgingRelease(font2)};
[mas addAttributes:d2 range:encRange];
*stop = YES; // do just once, last word
}];
最后的最后,别忘了释放内存:
CFRelease(basefont);
注意到上面,ARC开启的情况下,使用了CFBridgingRelease,这是一种把 CFTypeRef 转换成 Objective-C 的方法,同时这个方法也会负责帮我们管理这个内存。
Core Text 也可以绘制在图形上下文中,只是如果你不翻转图形上下文的坐标系统,绘制出的文本会上下颠倒。
如果字符串就是一行而已,我们可以通过CTLineRef直接绘制在图形上下文中。下面的代码是一个自定义的UIView子类,效果跟上面绘制的效果一样:
- (void)drawRect:(CGRect)rect {
if (!self.text)
return;
CGContextRef ctx = UIGraphicsGetCurrentContext();
// flip context
CGContextSaveGState(ctx);
CGContextTranslateCTM(ctx, 0, self.bounds.size.height);
CGContextScaleCTM(ctx, 1.0, -1.0);
CTLineRef line =
CTLineCreateWithAttributedString(
(__bridge CFAttributedStringRef)self.text);
CGContextSetTextPosition(ctx, 1, 3);
CTLineDraw(line, ctx);
CFRelease(line);
CGContextRestoreGState(ctx);
}
如果我们想绘制的文本有多行,我们必须使用CTFramesetter。这个frame-setter要求提供一个范围来进行绘制,这个范围用CGPath表示。但是不要以为这样可以绘制文本到任意形状内,这个CGPath,只能是矩形:
- (void)drawRect:(CGRect)rect {
if (!self.text)
return;
CGContextRef ctx = UIGraphicsGetCurrentContext();
// flip context
CGContextSaveGState(ctx);
CGContextTranslateCTM(ctx, 0, self.bounds.size.height);
CGContextScaleCTM(ctx, 1.0, -1.0);
CTFramesetterRef fs =
CTFramesetterCreateWithAttributedString(
(__bridge CFAttributedStringRef)self.text);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, nil, rect);
// range (0,0) means "the whole string"
CTFrameRef f = CTFramesetterCreateFrame(fs, CFRangeMake(0, 0), path, nil);
CTFrameDraw(f, ctx);
CGPathRelease(path);
CFRelease(f);
CFRelease(fs);
CGContextRestoreGState(ctx);
}
如果我们不像绘制出来的文本是左对齐的,而是希望是中间对齐的,我们可以提供一个CTParagraphStyle给这个属性字符串。段落类型可以包括首行缩进,tab代表的空格数,行高度,行间距,以及断行的模式等等。为了让我们的文本中间对齐,我们可以:
NSMutableAttributedString* mas = [self.text mutableCopy];
NSString* s = [mas string];
CTTextAlignment centerValue = kCTCenterTextAlignment;
CTParagraphStyleSetting center =
{kCTParagraphStyleSpecifierAlignment, sizeof(centerValue), ¢erValue};
CTParagraphStyleSetting pss[1] = {center};
CTParagraphStyleRef ps = CTParagraphStyleCreate(pss, 1);
[mas addAttribute:(NSString*)kCTParagraphStyleAttributeName
value:CFBridgingRelease(ps)
range:NSMakeRange(0, [s length])];
self.text = mas;
效果如下图:
使用Core Text,我们甚至可以改变字体的印刷效果,例如绘制Hoefler Text(http://zh.wikipedia.org/wiki/Hoefler_Text)
下面我们再绘制如下的文本:
当我们创建一个属性字符串时,我们通常使用CTFontDescriptorCreateCopyWithFeature函数,同时也可以访问Didot 字体的缩进变量:
NSString* path =
[[NSBundle mainBundle] pathForResource:@"states" ofType:@"txt"];
NSString* s =
[NSString stringWithContentsOfFile:path
encoding:NSUTF8StringEncoding error:nil];
CTFontRef font = CTFontCreateWithName((CFStringRef)@"Didot", 18, nil);
CTFontDescriptorRef fontdesc1 = CTFontCopyFontDescriptor(font);
// names come from SFNTLayoutTypes.h (iOS 6 new feature)
CTFontDescriptorRef fontdesc2 =
CTFontDescriptorCreateCopyWithFeature(fontdesc1,
(__bridge CFNumberRef)@(kLetterCaseType),
(__bridge CFNumberRef)@(kSmallCapsSelector));
CTFontRef basefont = CTFontCreateWithFontDescriptor(fontdesc2, 0, nil);
NSDictionary* d =
@{(NSString*)kCTFontAttributeName: CFBridgingRelease(basefont)};
NSMutableAttributedString* mas =
[[NSMutableAttributedString alloc] initWithString:s attributes:d];
CTTextAlignment centerValue = kCTCenterTextAlignment;
CTParagraphStyleSetting center =
{kCTParagraphStyleSpecifierAlignment, sizeof(centerValue), ¢erValue};
CTParagraphStyleSetting pss[1] = {center};
CTParagraphStyleRef ps = CTParagraphStyleCreate(pss, 1);
[mas addAttribute:(NSString*)kCTParagraphStyleAttributeName
value:CFBridgingRelease(ps)
range:NSMakeRange(0, [s length])];
CFRelease(font); CFRelease(fontdesc1); CFRelease(fontdesc2);
两列的文本是分别绘制在两个frame中的。在我们的drawRect:方法中,我们首先翻转了图形上下文的坐标系统,然后把所有的文本绘制在第一个frame中,然后使用CTFrameGetVisibleStringRange方法获取文本实际占用的空间大小,这个结果可以告诉我们在哪里开始绘制文本到第二个frame中:
CGRect r1 = rect;
r1.size.width /= 2.0; // column 1
CGRect r2 = r1;
r2.origin.x += r2.size.width; // column 2
CTFramesetterRef fs =
CTFramesetterCreateWithAttributedString(
(__bridge CFAttributedStringRef)self.text);
// draw column 1
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, nil, r1);
CTFrameRef f = CTFramesetterCreateFrame(fs, CFRangeMake(0, 0), path, nil);
CTFrameDraw(f, ctx);
CGPathRelease(path);
CFRange drawnRange = CTFrameGetVisibleStringRange(f);
CFRelease(f);
// draw column 2
path = CGPathCreateMutable();
CGPathAddRect(path, nil, r2);
f = CTFramesetterCreateFrame(fs,
CFRangeMake(drawnRange.location + drawnRange.length, 0), path, nil);
CTFrameDraw(f, ctx);
CGPathRelease(path);
CFRelease(f);
CFRelease(fs);
下面我们会这两个文本添加用户操作,当用户点击某个州的名称时,会绘制一个黑色的矩形框:
我们有两个可变数组的属性,theLines和 theBounds,我们会在drawRect:方法的开始初始化这两个数组,每一次我们调用 CTFrameDraw,我们同时也会调用一个帮助方法:
[self appendLinesAndBoundsOfFrame:f context:ctx];
这个帮助方法中,我们保存这个frame的CTLines到theLines中,同时计算每一行的绘制范围,然后保存到theBounds中:
- (void) appendLinesAndBoundsOfFrame:(CTFrameRef)f context:(CGContextRef)ctx{
CGAffineTransform t1 =
CGAffineTransformMakeTranslation(0, self.bounds.size.height);
CGAffineTransform t2 = CGAffineTransformMakeScale(1, -1);
CGAffineTransform t = CGAffineTransformConcat(t2, t1);
CGPathRef p = CTFrameGetPath(f);
CGRect r = CGPathGetBoundingBox(p); // this is the frame bounds
NSArray* lines = (__bridge NSArray*)CTFrameGetLines(f);
[self.theLines addObjectsFromArray:lines];
CGPoint origins[[lines count]];
CTFrameGetLineOrigins(f, CFRangeMake(0,0), origins);
for (int i = 0; i < [lines count]; i++) {
CTLineRef aLine = (__bridge CTLineRef)lines[i];
CGRect b = CTLineGetImageBounds((CTLineRef)aLine, ctx);
// the line origin plus the image bounds size is the bounds we want
CGRect b2 = { origins[i], b.size };
// but it is expressed in terms of the frame, so we must compensate
b2.origin.x += r.origin.x;
b2.origin.y += r.origin.y;
// we must also compensate for the flippedness of the graphics context
b2 = CGRectApplyAffineTransform(b2, t);
[self.theBounds addObject: [NSValue valueWithCGRect:b2]];
}
}
接着我们创建一个UITapGestureRecognizer手势识别器;当用户点击时,我们遍历保存了的bounds数组,查看是否包含点击的点。如果有,我们获取这个点击的州的名称,然后绘制黑色矩形:
- (void) tapped: (UITapGestureRecognizer*) tap {
CGPoint loc = [tap locationInView:self];
for (int i = 0; i < [self.theBounds count]; i++) {
CGRect rect = [self.theBounds[i] CGRectValue];
if (CGRectContainsPoint(rect, loc)) {
// draw rectangle for feedback
CALayer* lay = [CALayer layer];
lay.frame = CGRectInset(rect, -5, -5);
lay.borderWidth = 2;
[self.layer addSublayer: lay];
dispatch_time_t popTime =
dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[lay removeFromSuperlayer];
});
// fetch the drawn string tapped on
CTLineRef theLine =
(__bridge CTLineRef)[self.theLines[i];
CFRange range = CTLineGetStringRange(theLine);
CFStringRef s = CFStringCreateWithSubstring(
nil, (__bridge CFStringRef)[self.text string], range);
// ... could do something useful with string here ...
NSLog(@"tapped %@", s);
CFRelease(s);
break;
}
}
}