• 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;
            } 
        }
    }
    
  • 相关阅读:
    注意身体
    用生命去战斗
    来到华师,一切清零
    linux fork()函数 转载~~~~
    小端大端
    位域
    内存泄漏(memory leak)和内存溢出
    stack,heap的区别
    内存管理简便复习总结
    虚函数&&虚继承
  • 原文地址:https://www.cnblogs.com/YungMing/p/4346744.html
Copyright © 2020-2023  润新知