CoreText编程指南(布局操作)

Demo地址

通用文本布局操作

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

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

布局一个段落

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

<a name="listingone"></a>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 DescriptorsCreating a Font from a Font Descriptor中展示。)

<a name="listing two"></a>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);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • CoreText是用来文字排版和处理字体的一个高级的底层技术。CoreText直接和CoreGraphics(CG...
    癫癫的恋了阅读 2,022评论 0 6
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 整理中... 文本布局 TextLayout = Glyphs + Locations参考:活字印刷 Glyphs...
    DBreak阅读 15,544评论 17 71
  • 清晨 路过黄帝故里景区 这是民族朝拜的圣地 这是凝聚华人的精神家园 在这里, 来过了多少虔诚的拜祖者 在这里 来过...
    白丰阁阅读 307评论 0 1
  • 我想赶紧把这篇文章写完,因为害怕很快就会忘记。因为这些人才是我们熟悉的人啊。毕业季,回忆就像记者,在我们的脑海里面...
    九型之旅阅读 287评论 0 2