CoreText编程指南(布局操作)

Demo地址

通用文本布局操作

这一章描述一些通用的文本布局操作,显示怎么用CoreText来实现它们。下面是本章包含的操作代码的列表:

  • 布局一个段落
  • 简单的文本标签
  • 多列布局
  • 手动换行
  • 提供段落样式
  • 在非矩形区域显示文本

布局一个段落

一种排版中最常见的操作是在在一个变化无常的矩形区域布局一个多行的段落。CoreText是这种操作变得非常简单,只需要几行CoreText代码。为了布局一个段落,你需要一个graphics context来绘制,一个矩形路径来提供布局的区域,一个attributed string。例子中的大部分代码是用来创建和初始化contextpathstring的。在这些完成之后,CoreText只需要仅仅3行代码来布局。
2-1中列出的代码展示了段落式怎么布局的。这些代码可以放在UIView子类的drawRect:方法中。

Listing 2-1 Typesetting a simple paragraph
 // Initialize a graphics context in iOS.
 CGContextRef context = UIGraphicsGetCurrentContext();

 // Flip the context coordinates, in iOS only.
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

// Initializing a graphic context in OS X is different:
// CGContextRef context =
//     (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];

// Set the text matrix.
CGContextSetTextMatrix(context, CGAffineTransformIdentity);

// Create a path which bounds the area where you will be drawing text.
// The path need not be rectangular.
CGMutablePathRef path = CGPathCreateMutable();

// In this simple example, initialize a rectangular path.
CGRect bounds = CGRectMake(10.0, 10.0, 200.0, 200.0);
CGPathAddRect(path, NULL, bounds );

// Initialize a string.
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.");

// Create a mutable attributed string with a max length of 0.
// The max length is a hint as to how much internal storage to reserve.
// 0 means no hint.
CFMutableAttributedStringRef attrString =
CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);

// Copy the textString into the newly created attrString
CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0),
                                 textString);

// Create a color that will be added as an attribute to the attrString.
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat components[] = { 1.0, 0.0, 0.0, 0.8 };
CGColorRef red = CGColorCreate(rgbColorSpace, components);
CGColorSpaceRelease(rgbColorSpace);

// Set the color of the first 12 chars to red.
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, 12),
                               kCTForegroundColorAttributeName, red);

// Create the framesetter with the attributed string.
CTFramesetterRef framesetter =
CTFramesetterCreateWithAttributedString(attrString);
CFRelease(attrString);

// Create a frame.
CTFrameRef frame = CTFramesetterCreateFrame(framesetter,
                                            CFRangeMake(0, 0), path, NULL);

// Draw the specified frame in the given context.
CTFrameDraw(frame, context);

// Release the objects we used.
CFRelease(frame);
CFRelease(path);
CFRelease(frame setter);

简单的文本标签

另一个常见的排版操作绘制一行文本,想label一样用作用户交互元素。在CoreText中这只需要两行代码:一行来用CFAttributedString生成一个line object,另一行来把line绘制到graphic context上。
表2-2展示了怎样在UIView或者NSView的子类里的drawRect:方式里完成这个操作。这个列表省略了plain text stringfontgraphics context的初始化等已经在本文档的其它代码中展示了的操作。它展示了怎么创建一个attributes dictionary并用它来创建attributed string。(字体创建在Creating Font Descriptors和Creating a Font from a Font Descriptor中展示。)

Listing 2-2 Typesetting a simple text label
CFStringRef string; CTFontRef font; CGContextRef context;
// Initialize the string, font, and 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);

// Set text position and draw the line into the graphics context
CGContextSetTextPosition(context, 10.0, 10.0);
CTLineDraw(line, context);
CFRelease(line);

分列布局

在多个列布局文本是另外一种常见的排版操作。严格地说来,CoreText在同一时间只布局一列,并且不计算列的尺寸和位置。在调用CoreText排版之前你需要计算它们的路径区域。在这个例子中,CoreText,不光在每列中布局文本,还为每一列提供了字符串中的子范围。
表2-3中createColumnsWithColumnCount:方法接受一个参数,这个参数表示需要绘制的列数,并返回一个路径的数组,每个路径代表一列。
表2-4包含drawRect:方法的定义,在里面调用上面列出的createColumnsWithColumnCount方法。这些代码写在一个UIView的子类里(在OS X中是NSView的子类)。这个子类包含一个attributedString的property,这个属性没有展示在这里,但是需要调用方法的setter来为这段代码提供一个attributed string

Listing 2-3 Dividing a view into columns
- (CFArrayRef)createColumnsWithColumnCount:(int)columnCount
{
  int column;

  CGRect* columnRects = (CGRect*)calloc(columnCount, sizeof(*columnRects));
  // Set the first column to cover the entire view.
  columnRects[0] = self.bounds;

  // Divide the columns equally across the frame's width.
  CGFloat columnWidth = CGRectGetWidth(self.bounds) / columnCount;
  for (column = 0; column < columnCount - 1; column++) {
      CGRectDivide(columnRects[column], &columnRects[column],
                 &columnRects[column + 1], columnWidth, CGRectMinXEdge);
  }

  // Inset all columns by a few pixels of margin.
  for (column = 0; column < columnCount; column++) {
      columnRects[column] = CGRectInset(columnRects[column], 8.0, 15.0);
  }

  // Create an array of layout paths, one for each column.
  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;
}
Listing 2-4 Performing columnar text layout
// Override drawRect: to draw the attributed string into columns.
// (In OS X, the drawRect: method of NSView takes an NSRect parameter,
//  but that parameter is not used in this listing.)
- (void)drawRect:(CGRect)rect
{
  // Initialize a graphics context in iOS.
  CGContextRef context = UIGraphicsGetCurrentContext();

  // Flip the context coordinates in iOS only.
  CGContextTranslateCTM(context, 0, self.bounds.size.height);
  CGContextScaleCTM(context, 1.0, -1.0);

  // Initializing a graphic context in OS X is different:
  // CGContextRef context =
  //     (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];

  // Set the text matrix.
  CGContextSetTextMatrix(context, CGAffineTransformIdentity);

  // Create the framesetter with the attributed string.
  CTFramesetterRef frame setter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)self.attributedString);

  // Call createColumnsWithColumnCount function to create an array of
  // three paths (columns).
  CFArrayRef columnPaths = [self createColumnsWithColumnCount:3];

  CFIndex pathCount = CFArrayGetCount(columnPaths);
  CFIndex startIndex = 0;
  int column;

  // Create a frame for each column (path).
  for (column = 0; column < pathCount; column++) {
      // Get the path for this column.
      CGPathRef path = (CGPathRef)CFArrayGetValueAtIndex(columnPaths, column);
    
      // Create a frame for this column and draw it.
      CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(startIndex, 0), path, NULL);
      CTFrameDraw(frame, context);
    
      // Start the next frame at the first character not visible in this frame.
      CFRange frameRange = CTFrameGetVisibleStringRange(frame);
      startIndex += frameRange.length;
      CFRelease(frame);
    
  }
  CFRelease(columnPaths);
  CFRelease(framesetter);

}

手动换行

在CoreText,你通常不需要手动处理换行,除非你有一个特别的断字需求或类似的需求。Framesetter可以自动处理换行。作为一种选择,CoreText可以让你精确地决定在哪里换行。表2-5展示了怎么创建一个typesetter(一个被framesetter使用的对象),并用typesetter直接找到合适的换行,并手动创建一个typeset line。这个例子也显示了怎么在绘制之前让行居中。
这些代码可以放在UIView子类的drawRect:方法里(OS X中NSView的子类中)。列表中没有展示代码中用到的变量的初始化。

Listing 2-5 Performing manual line breaking

 double width; CGContextRef context; CGPoint textPosition;         CFAttributedStringRef attrString;
// Initialize those variables.

// Create a typesetter using the attributed string.
CTTypesetterRef typesetter =     CTTypesetterCreateWithAttributedString(attrString);

// Find a break for line from the beginning of the string to the given width.
CFIndex start = 0;
CFIndex count = CTTypesetterSuggestLineBreak(typesetter, start, width);

// Use the returned character count (to the break) to create the line.
CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(start, count));

// Get the offset needed to center the line.
float flush = 0.5; // centered
double penOffset = CTLineGetPenOffsetForFlush(line, flush, width);

// Move the given text drawing position by the calculated offset and draw the line.
CGContextSetTextPosition(context, textPosition.x + penOffset, textPosition.y);
CTLineDraw(line, context);

// Move the index beyond the line break.
start += count;

提供段落样式

表2-6定义了一个函数为attributed string提供一个段落样式。这个函数接受字体名字、字号、行间距作为参数。这个函数在表2-7的代码中调用,创建一个plain text string,用applyParaStyle函数使用给定的段落属性生成一个attributed string,然后创建一个framesetterframe,然后绘制frame

Listing 2-6 Applying a paragraph style

NSAttributedString* applyParaStyle(
            CFStringRef fontName , CGFloat pointSize,
            NSString *plainText, CGFloat lineSpaceInc){

  // Create the font so we can determine its height.
  CTFontRef font = CTFontCreateWithName(fontName, pointSize, NULL);

  // Set the lineSpacing.
  CGFloat lineSpacing = (CTFontGetLeading(font) + lineSpaceInc) * 2;

  // Create the paragraph style settings.
  CTParagraphStyleSetting setting;

  setting.spec = kCTParagraphStyleSpecifierLineSpacing;
  setting.valueSize = sizeof(CGFloat);
  setting.value = &lineSpacing;

  CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(&setting, 1);

  // Add the paragraph style to the dictionary.
  NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
                           (__bridge id)font, (id)kCTFontNameAttribute,
                           (__bridge id)paragraphStyle,
                           (id)kCTParagraphStyleAttributeName, nil];
  CFRelease(font);
  CFRelease(paragraphStyle);

  // Apply the paragraph style to the string to created the attributed string.
  NSAttributedString* attrString = [[NSAttributedString alloc]
                           initWithString:(NSString*)plainText
                           attributes:attributes];

  return attrString;
}

在表2-7中,这个格式化的string用来创建一个frame setter。这段代码用这个frame setter来创建一个frame并绘制这个frame。

Listing 2-7 Drawing the styled paragraph

- (void)drawRect:(CGRect)rect {
  // Initialize a graphics context in iOS.
  CGContextRef context = UIGraphicsGetCurrentContext();

  // Flip the context coordinates in iOS only.
  CGContextTranslateCTM(context, 0, self.bounds.size.height);
  CGContextScaleCTM(context, 1.0, -1.0);

  // Set the text matrix.
  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.");

  // Apply the paragraph style.
  NSAttributedString* attrString = applyParaStyle(fontName, pointSize, string, 50.0);

  // Put the attributed string with applied paragraph style into a framesetter.
  CTFramesetterRef framesetter =
           CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);

  // Create a path to fill the View.
  CGPathRef path = CGPathCreateWithRect(rect, NULL);

  // Create a frame in which to draw.
  CTFrameRef frame = CTFramesetterCreateFrame(
                                framesetter, CFRangeMake(0, 0), path, NULL);

  // Draw the frame.
  CTFrameDraw(frame, context);
  CFRelease(frame);
  CGPathRelease(path);
  CFRelease(framesetter);
}

在OS X中,NSViewdrawRect:接受一个NSRect参数,但是CGPathCreateWithRect函数需要一个CGRect参数。因此,你必须用下面的方法把一个NSRect对象转化成一个CGRect对象:

CGRect myRect = NSRectToCGRect([self bounds]);

另外,在OS X中,你用不同的方法获取graphics context,而且你不需要翻转它的坐标,如表2-7注释中所示。

在非矩形区域显示文本

在非矩形区域中显示文本中最难的部分是描述非矩形路径。表2-8中的AddSquashedDonutPath方法返回一个环形的路径。当你有了路径之后,只需要简单地调用通常的CoreText函数来提供属性并绘制。

Listing 2-8 Displaying text in a nonrectangular path

 // Create a path in the shape of a donut.
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));
}

// Generate the path outside of the drawRect call so the path is calculated only once.
- (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];

  // Initialize a graphics context in iOS.
  CGContextRef context = UIGraphicsGetCurrentContext();

  // Flip the context coordinates in iOS only.
  CGContextTranslateCTM(context, 0, self.bounds.size.height);
  CGContextScaleCTM(context, 1.0, -1.0);

  // Set the text matrix.
  CGContextSetTextMatrix(context, CGAffineTransformIdentity);

  // Initialize an attributed string.
  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.");

  // Create a mutable attributed string.
  CFMutableAttributedStringRef attrString =
            CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);

  // Copy the textString into the newly created attrString.
  CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0), textString);

  // Create a color that will be added as an attribute to the attrString.
  CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
  CGFloat components[] = { 1.0, 0.0, 0.0, 0.8 };
  CGColorRef red = CGColorCreate(rgbColorSpace, components);
  CGColorSpaceRelease(rgbColorSpace);

  // Set the color of the first 13 chars to red.
  CFAttributedStringSetAttribute(attrString, CFRangeMake(0, 13),
                                 kCTForegroundColorAttributeName, red);

  // Create the framesetter with the attributed string.
  CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);

  // Create the array of paths in which to draw the text.
  NSArray *paths = [self paths];

  CFIndex startIndex = 0;

  // In OS X, use NSColor instead of UIColor.
  #define GREEN_COLOR [UIColor greenColor]
  #define YELLOW_COLOR [UIColor yellowColor]
  #define BLACK_COLOR [UIColor blackColor]

  // For each path in the array of paths...
  for (id object in paths) {
      CGPathRef path = (__bridge CGPathRef)object;

      // Set the background of the path to yellow.
      CGContextSetFillColorWithColor(context, [YELLOW_COLOR CGColor]);

      CGContextAddPath(context, path);
      CGContextFillPath(context);

      CGContextDrawPath(context, kCGPathStroke);

      // Create a frame for this path and draw the text.
      CTFrameRef frame = CTFramesetterCreateFrame(framesetter,
                                     CFRangeMake(startIndex, 0), path, NULL);
      CTFrameDraw(frame, context);

      // Start the next frame at the first character not visible in this frame.
      CFRange frameRange = CTFrameGetVisibleStringRange(frame);
      startIndex += frameRange.length;
      CFRelease(frame);
  }

  CFRelease(attrString);
  CFRelease(framesetter);
}

你可能感兴趣的:(CoreText编程指南(布局操作))