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), &centerValue};
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), &centerValue};
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;
        } 
    }
}
posted @ 2014-03-13 11:42  剑尖  阅读(663)  评论(0编辑  收藏  举报