美文网首页高性能iOS应用开发CoreText
Core Text 编程指南02:常见文本布局操作

Core Text 编程指南02:常见文本布局操作

作者: 张芳涛 | 来源:发表于2019-04-15 17:12 被阅读3次

    本章介绍了一些常见的文本布局操作,并说明了如何使用Core Text执行它们。本章包含以下代码清单操作:

    • 列出一个段落
    • 简单的文字标签
    • 列式布局
    • 手动换行
    • 应用段落样式
    • 在非矩形区域中显示文本

    列出一个段落

    排版中最常见的操作之一是在任意大小的矩形区域内布置多线段。 Core Text使此操作变得简单,只需要几行特定于Core Text的代码。要布置段落,您需要绘制图形上下文,矩形路径以提供布置文本的区域以及属性字符串。此示例中的大多数代码都需要创建和初始化上下文,路径和字符串。完成此操作后,Core Text只需要三行代码即可完成布局。

    清单2-1中的代码显示了段落的布局方式。此代码可以驻留在UIView子类的drawRect:方法中(OS X中的NSView子类)。

    清单2-1排版一个简单的段落

    // 在iOS中初始化图形上下文。
    CGContextRef context = UIGraphicsGetCurrentContext();
    // 翻转上下文坐标,仅限iOS。
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    
    // 在OS X中初始化图形上下文是不同的:
    // CGContextRef context =
    //     (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
    
    // 设置文本矩阵。
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    
    // 创建一个限定要绘制文本的区域的路径。
    // 路径不需要一定是矩形。
    CGMutablePathRef path = CGPathCreateMutable();
    
    // 在这个简单的例子中,初始化一个矩形路径。
    CGRect bounds = CGRectMake(10.0, 10.0, 200.0, 200.0);
    CGPathAddRect(path, NULL, bounds );
    
    // 初始化一个字符串。
    CFStringRef textString = CFSTR("Hello, World! I know nothing in the world that has as much power as a word. Sometimes I write one, and I look at it, until it begins to shine.");
    
    // 创建一个最大长度为0的可变属性字符串。
    // 最大长度是关于要保留多少内部存储空间的提示。
    // 0表示没有提示。
    CFMutableAttributedStringRef attrString =
         
    CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
    
    // 将textString复制到新创建的attrString中
    CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0),
         textString);
    
    // 创建一个将作为属性添加到attrString的颜色。
    CGColorSpaceRef rgbColorSpace = 
    CGColorSpaceCreateDeviceRGB();
    CGFloat components[] = { 1.0, 0.0, 0.0, 0.8 };
    CGColorRef red = CGColorCreate(rgbColorSpace, components);
    CGColorSpaceRelease(rgbColorSpace);
    
     // 将前12个字符的颜色设置为红色。
    CFAttributedStringSetAttribute(attrString, CFRangeMake(0, 12),
         kCTForegroundColorAttributeName, red);
    
    // 使用属性字符串创建框架集。
    CTFramesetterRef framesetter =
         
    CTFramesetterCreateWithAttributedString(attrString);
    CFRelease(attrString);
    
    // 创建一个框架。
    CTFrameRef frame = 
    CTFramesetterCreateFrame(framesetter,
          CFRangeMake(0, 0), path, NULL);
    
    // 在给定的上下文中绘制指定的帧。
    CTFrameDraw(frame, context);
    
    // 释放我们使用的对象。
    CFRelease(frame);
    CFRelease(path);
    CFRelease(framesetter);
    

    简单的文字标签

    另一种常见的排版操作是绘制单行文本以用作用户界面元素的标签。在Core Text中,这只需要两行代码:一行用于创建具有CFAttributedString的行对象,另一行用于将行绘制到图形上下文中。

    清单2-2显示了如何在UIViewNSView子类的drawRect: 方法中完成此操作。该列表省略了纯文本字符串,字体和图形上下文的初始化,本文档中其他列表中显示的操作。它显示了如何创建属性字典并使用它来创建属性字符串。 (字体创建显示在Creating Font DescriptorsCreating a Font from a Font Descriptor 。)

    清单2-2排版一个简单的文本标签

    CFStringRef string; CTFontRef font; CGContextRef context;
    // 初始化字符串,字体和上下文
    
    CFStringRef keys[] = { kCTFontAttributeName };
    CFTypeRef values[] = { font };
    
    CFDictionaryRef attributes =
    CFDictionaryCreate(kCFAllocatorDefault, (const void**)&keys,
        (const void**)&values, sizeof(keys) / sizeof(keys[0]),
        &kCFTypeDictionaryKeyCallBacks,
        &kCFTypeDictionaryValueCallBacks);
    
    CFAttributedStringRef attrString =
    CFAttributedStringCreate(kCFAllocatorDefault, string, attributes);
    
    CFRelease(string);
    CFRelease(attributes);
    
    CTLineRef line = CTLineCreateWithAttributedString(attrString);
    
    // 设置文本位置并将线条绘制到图形上下文中
    
    CGContextSetTextPosition(context, 10.0, 10.0);
    CTLineDraw(line, context);
    CFRelease(line);
    

    列式布局

    在多列中布置文本是另一种常见的排版操作。严格来说,Core Text本身一次只列出一列,不计算列大小或位置。您在调用Core Text之前执行这些操作,以在您计算的路径区域内布置文本。在此示例中,Core Text除了在每列中布置文本外,还为每列提供了文本字符串中的子范围。

    清单2-3中的createColumnsWithColumnCount:方法接受要绘制的列数作为参数,并返回路径数组,每列一个路径。

    清单2-4包含drawRect: 方法的实现,该方法调用本清单中首先定义的本地createColumnsWithColumnCount方法。此代码驻留在UIView子类(OS X中的NSView子类)中。子类包含一个attributesString属性,此属性未在此处显示,但在此列表中调用其访问者以返回要布局的属性字符串。

    清单2-3将视图划分为列

    - (CFArrayRef)createColumnsWithColumnCount:(int)columnCount
    {
    
    int column;
    
     CGRect* columnRects = (CGRect*)calloc(columnCount, sizeof(*columnRects));
    // 设置第一列以覆盖整个视图。
    columnRects[0] = self.bounds;
    
    // 在框架的宽度上平均分配列。
    CGFloat columnWidth = CGRectGetWidth(self.bounds) / columnCount;
    for (column = 0; column < columnCount - 1; column++) {
        CGRectDivide(columnRects[column], &columnRects[column],
                     &columnRects[column + 1], columnWidth, CGRectMinXEdge);
    }
    
    // 将所有列插入几个像素的边距。
    for (column = 0; column < columnCount; column++) {
        columnRects[column] = CGRectInset(columnRects[column], 8.0, 15.0);
    }
    
    // 创建一个布局路径数组,每列一个。
    CFMutableArrayRef array =
                     CFArrayCreateMutable(kCFAllocatorDefault,
                                  columnCount, &kCFTypeArrayCallBacks);
    
     for (column = 0; column < columnCount; column++) {
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, columnRects[column]);
        CFArrayInsertValueAtIndex(array, column, path);
        CFRelease(path);
     }
    free(columnRects);
    return array;
    }
    

    清单2-4执行列式文本布局

    // Override drawRect: 将属性字符串绘制到列中。
    // (在OS X中,NSView的drawRect:方法采用NSRect参数,
    //  但该列表中未使用该参数。)
    - (void)drawRect:(CGRect)rect{
     // 在iOS中初始化图形上下文。
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 仅在iOS中翻转上下文坐标。
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    
    // 在OS X中初始化图形上下文是不同的:
    // CGContextRef context =
    //     (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
    
    // 设置文本矩阵。
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    
    // 使用属性字符串创建框架集。
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(
                                      (CFAttributedStringRef)self.attributedString);
    
    // 调用createColumnsWithColumnCount函数来创建一个数组
    // 三条路径(列)。
    CFArrayRef columnPaths = [self createColumnsWithColumnCount:3];
    
    CFIndex pathCount = CFArrayGetCount(columnPaths);
    CFIndex startIndex = 0;
    int column;
    
    // 为每列(路径)创建一个框架。
    for (column = 0; column < pathCount; column++) {
        //获取此列的路径。
        CGPathRef path = (CGPathRef)CFArrayGetValueAtIndex(columnPaths, column);
    
        //为此列创建一个框架并绘制它。
        CTFrameRef frame = CTFramesetterCreateFrame(
                             framesetter, CFRangeMake(startIndex, 0), path, NULL);
        CTFrameDraw(frame, context);
    
        //在此框架中不可见的第一个字符处开始下一帧。
        CFRange frameRange = CTFrameGetVisibleStringRange(frame);
        startIndex += frameRange.length;
        CFRelease(frame);
    
    }
    CFRelease(columnPaths);
    CFRelease(framesetter);
    }
    

    手动换行

    Core Text中,除非您有特殊的连字过程或类似要求,否则通常不需要手动换行。framesetter自动执行换行。或者,Core Text使你可以准确指定每行文本的中断位置。清单2-5显示了如何创建排版工具,框架集使用的对象,以及直接使用排版工具查找适当的换行符并手动创建排版行。此示例还显示了在绘制之前如何使线居中。

    此代码可以位于UIView子类的drawRect: 方法中(OS X中的NSView子类)。该列表未显示代码中使用的变量的初始化。

    清单2-5执行手动换行

    double width; CGContextRef context; CGPoint textPosition; CFAttributedStringRef attrString;
    // 初始化这些变量。
    
    // 使用属性字符串创建排字机。
    CTTypesetterRef typesetter = 
    CTTypesetterCreateWithAttributedString(attrString);
    
    // 找到从字符串开头到给定宽度的行的中断。
    CFIndex start = 0;
    CFIndex count = CTTypesetterSuggestLineBreak(typesetter, start, width);
    
    
    // 使用返回的字符数(到中断)来创建该行。
    CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(start, count));
    
    // 获得使线条居中所需的偏移量。
    float flush = 0.5; // centered
    double penOffset = CTLineGetPenOffsetForFlush(line, flush, width);
    
    // 按计算的偏移量移动给定的文本绘图位置并绘制线条。
    CGContextSetTextPosition(context, textPosition.x + penOffset, textPosition.y);
    CTLineDraw(line, context);
    
    // 将索引移到换行符之外。
    start += count;
    

    应用段落样式

    清单2-6实现了一个将段落样式应用于属性字符串的函数。该函数接受字体名称,磅值和行间距作为参数,这增加或减少文本行之间的空间量。此函数由清单2-7中的代码调用,该代码创建纯文本字符串,使用applyParaStyle函数创建具有给定段落属性的属性字符串,然后创建框架集和框架,并绘制框架。

    清单2-6应用段落样式

    NSAttributedString* applyParaStyle(
                CFStringRef fontName , CGFloat pointSize,
                NSString *plainText, CGFloat lineSpaceInc){
    // 创建字体,以便我们确定其高度。
    CTFontRef font = CTFontCreateWithName(fontName, pointSize, NULL);
    
    // 设置行间距。
    CGFloat lineSpacing = (CTFontGetLeading(font) + lineSpaceInc) * 2;
    
    // 创建段落样式设置。
    CTParagraphStyleSetting setting;
    
    setting.spec = kCTParagraphStyleSpecifierLineSpacing;
    setting.valueSize = sizeof(CGFloat);
    setting.value = &lineSpacing;
    
    CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(&setting, 1);
    
    // 将段落样式添加到字典中。
    NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
                               (__bridge id)font, (id)kCTFontNameAttribute,
                               (__bridge id)paragraphStyle,
                               (id)kCTParagraphStyleAttributeName, nil];
    CFRelease(font);
    CFRelease(paragraphStyle);
    
    // 将段落样式应用于字符串以创建属性字符串。
    NSAttributedString* attrString = [[NSAttributedString alloc]
                               initWithString:(NSString*)plainText
                               attributes:attributes];
    
    return attrString;
    }
    

    在清单2-7中,样式化字符串用于创建框架集。代码使用框架集创建框架并绘制框架。

    清单2-7绘制样式段落

    - (void)drawRect:(CGRect)rect {
    // 在iOS中初始化图形上下文。
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 仅在iOS中翻转上下文坐标。
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    
    // 设置文本矩阵。
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    
    CFStringRef fontName = CFSTR("Didot Italic");
    CGFloat pointSize = 24.0;
    
    CFStringRef string = CFSTR("Hello, World! I know nothing in the world that has
                                   as much power as a word. Sometimes I write one,
                                   and I look at it, until it begins to shine.");
    
    // 应用段落样式。
    NSAttributedString* attrString = applyParaStyle(fontName, pointSize, string, 50.0);
    
    // 将带有应用段落样式的属性字符串放入框架集中。
    CTFramesetterRef framesetter =
             CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
    
    // 创建一个填充视图的路径。
    CGPathRef path = CGPathCreateWithRect(rect, NULL);
    
    // 创建一个绘制框架。
    CTFrameRef frame = CTFramesetterCreateFrame(
                                    framesetter, CFRangeMake(0, 0), path, NULL);
    
    // 绘制frame。
    CTFrameDraw(frame, context);
    CFRelease(frame);
    CGPathRelease(path);
    CFRelease(framesetter);
    }
    

    OS X中,NSView drawRect:方法接收NSRect参数,但CGPathCreateWithRect 函数需要CGRect参数。因此,您必须使用以下函数调用将NSRect对象转换为CGRect对象:

    CGRect myRect = NSRectToCGRect([self bounds]);
    

    此外,在OS X中,您可以获得不同的图形上下文,并且不会翻转其坐标,如清单2-7中的注释所示。

    在非矩形区域中显示文本

    在非矩形区域中显示文本的困难部分是描述非矩形路径。清单2-8中的AddSquashedDonutPath函数返回一个环形路径。获得路径后,只需调用常用的Core Text函数即可应用属性并绘制。

    清单2-8在非矩形路径中显示文本

    // 创建一个甜甜圈形状的路径。
    static void AddSquashedDonutPath(CGMutablePathRef path,
              const CGAffineTransform *m, CGRect rect)
    {
    CGFloat width = CGRectGetWidth(rect);
    CGFloat height = CGRectGetHeight(rect);
    
    CGFloat radiusH = width / 3.0;
    CGFloat radiusV = height / 3.0;
    
    CGPathMoveToPoint( path, m, rect.origin.x, rect.origin.y + height - radiusV);
    CGPathAddQuadCurveToPoint( path, m, rect.origin.x, rect.origin.y + height,
                               rect.origin.x + radiusH, rect.origin.y + height);
    CGPathAddLineToPoint( path, m, rect.origin.x + width - radiusH,
                               rect.origin.y + height);
    CGPathAddQuadCurveToPoint( path, m, rect.origin.x + width,
                               rect.origin.y + height,
                               rect.origin.x + width,
                               rect.origin.y + height - radiusV);
    CGPathAddLineToPoint( path, m, rect.origin.x + width,
                               rect.origin.y + radiusV);
    CGPathAddQuadCurveToPoint( path, m, rect.origin.x + width, rect.origin.y,
                               rect.origin.x + width - radiusH, rect.origin.y);
    CGPathAddLineToPoint( path, m, rect.origin.x + radiusH, rect.origin.y);
    CGPathAddQuadCurveToPoint( path, m, rect.origin.x, rect.origin.y,
                               rect.origin.x, rect.origin.y + radiusV);
    CGPathCloseSubpath( path);
    
    CGPathAddEllipseInRect( path, m,
                            CGRectMake( rect.origin.x + width / 2.0 - width / 5.0,
                            rect.origin.y + height / 2.0 - height / 5.0,
                            width / 5.0 * 2.0, height / 5.0 * 2.0));
    }
    
    // 生成drawRect调用之外的路径,以便仅计算路径一次。
    
    - (NSArray *)paths{
     CGMutablePathRef path = CGPathCreateMutable();
     CGRect bounds = self.bounds;
     bounds = CGRectInset(bounds, 10.0, 10.0);
     AddSquashedDonutPath(path, NULL, bounds);
    
     NSMutableArray *result =
              [NSMutableArray 
     arrayWithObject:CFBridgingRelease(path)];
     return result;
    }
    
    - (void)drawRect:(CGRect)rect{
    [super drawRect:rect];
    
    // 在iOS中初始化图形上下文。
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // 仅在iOS中翻转上下文坐标。
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    
    // 设置文本矩阵。
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    
    // 初始化属性字符串。
    CFStringRef textString = CFSTR("Hello, World! I know nothing in the world that
    has as much power as a word. Sometimes I write one, and I look at it,
    until it begins to shine.");
    
    // 创建一个可变的属性字符串。
     CFMutableAttributedStringRef attrString =
                CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
    
    // 将textString复制到新创建的attrString中。
    CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0), textString);
    
    // 创建一个将作为属性添加到attrString的颜色。
    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    CGFloat components[] = { 1.0, 0.0, 0.0, 0.8 };
    CGColorRef red = CGColorCreate(rgbColorSpace, components);
    CGColorSpaceRelease(rgbColorSpace);
    
    // 将前13个字符的颜色设置为红色。
    CFAttributedStringSetAttribute(attrString, CFRangeMake(0, 13),
                                     kCTForegroundColorAttributeName, red);
    
    // 使用属性字符串创建框架集。
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);
    
    // 创建用于绘制文本的路径数组。
    NSArray *paths = [self paths];
    
    CFIndex startIndex = 0;
    
    // 在OS X中,使用NSColor而不是UIColor。
    #define GREEN_COLOR [UIColor greenColor]
    #define YELLOW_COLOR [UIColor yellowColor]
    #define BLACK_COLOR [UIColor blackColor]
    
    // 对于路径数组中的每个路径......
    for (id object in paths) {
        CGPathRef path = (__bridge CGPathRef)object;
    
        // 将路径的背景设置为黄色。
        CGContextSetFillColorWithColor(context, [YELLOW_COLOR CGColor]);
    
        CGContextAddPath(context, path);
        CGContextFillPath(context);
    
        CGContextDrawPath(context, kCGPathStroke);
    
        // 为此路径创建frame并绘制文本。
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter,
                                         CFRangeMake(startIndex, 0), path, NULL);
        CTFrameDraw(frame, context);
    
        // 在此frame中不可见的第一个字符处开始下一帧。
        CFRange frameRange = CTFrameGetVisibleStringRange(frame);
        startIndex += frameRange.length;
        CFRelease(frame);
    }
    CFRelease(attrString);
    CFRelease(framesetter);
    }
    
    上一章 目录 下一章

    相关文章

      网友评论

        本文标题:Core Text 编程指南02:常见文本布局操作

        本文链接:https://www.haomeiwen.com/subject/xkguwqtx.html