[转] iOS文字排版(CoreText)那些事儿

文章转载自 http://www.cocoachina.com/applenews/devnews/2014/0521/8504.html

iOS文字排版(CoreText)那些事儿

转自阿毛的蛋疼地

第一次比较深入接触iOS文字排版相关内容是在12年底,实现某IM项目聊天内容的图文混排,照着nimbus的AttributedLabel和Raywenderlish上的这篇文章《Core Text Tutorial for iOS: Making a Magazine
App
》改出了一个比较适用于聊天内容展现的图文混排(文字和表情)控件。

选择自己写而不是直接使用现有第三方库的原因有三:

1.
在这之前也做过一个iOS上的IM产品,当时这个模块并不是我负责,图文混排的实现非常诡异(通过二分法计算出文字所占区域大小),效率极低,所以需要重新做一个效率比较高的控件出来。

2.
看过一些开源的实现,包括OHAttribtuedLabel,DTCoreText和Nimbus,总觉得他们实现插入图片的接口有点别扭,对于上层调用者来说CoreText部分不是完全透明的:调用者需要考虑怎么用自己的图片把原来内容替换掉。(当时的印象,现在具体怎么样已经不清楚了)

3. 这是重新造轮子的机会!

直接拿了Nimbus的AttributedLabel作为基础,然后重新整理图文混排那部分的代码,调整接口,一共也就花了一个晚上的时间:拜一下Nimbus的作者们。后来也根据项目的需求做了一些小改动,比如hack
iOS7下不准的问题,支持在Label上添加UIView的特性等等。最新的代码可以在github上找到:M80AttributedLabel。

不过写这篇文章最重要的原因不是为了放个代码出来,而是在闲暇时整理一下iOS/OSX文字排版相关的知识。

文字排版的基础概念

字体(Font):和我们平时说的字体不同,计算机意义上的字体表示的是同一大小,同一样式(Style)字形的集合。从这个意义上来说,当我们为文字设置粗体,斜体时其实是使用了另外一种字体(下划线不算)。而平时我们所说的字体只是具有相同设计属性的字体集合,即Font
Family或typeface。

字符(Character)和字形(Glyphs):排版过程中一个重要的步骤就是从字符到字形的转换,字符表示信息本身,而字形是它的图形表现形式。字符一般就是指某种编码,如Unicode编码,而字形则是这些编码对应的图片。但是他们之间不是一一对应关系,同个字符的不同字体族,不同字体大小,不同字体样式都对应了不同的字形。而由于连写(Ligatures)的存在,多个字符也会存在对应一个字形的情况。

字形描述集(Glyphs
Metris)
:即字形的各个参数。如下面的两张图:

边框(Bounding Box):一个假想的边框,尽可能地容纳整个字形。

基线(Baseline):一条假想的参照线,以此为基础进行字形的渲染。一般来说是一条横线。

基础原点(Origin):基线上最左侧的点。

行间距(Leading):行与行之间的间距。

字间距(Kerning):字与字之间的距离,为了排版的美观,并不是所有的字形之间的距离都是一致的,但是这个基本步影响到我们的文字排版。

上行高度(Ascent)和下行高度(Decent):一个字形最高点和最低点到基线的距离,前者为正数,而后者为负数。当同一行内有不同字体的文字时,就取最大值作为相应的值。如下图:

红框高度既为当前行的行高,绿线为baseline,绿色到红框上部分为当前行的最大Ascent,绿线到黄线为当前行的最大Desent,而黄框的高即为行间距。由此可以得出:lineHeight
= Ascent + |Decent| + Leading。

更加详细的内容可以参考苹果的这篇文档: 《Cocoa Text Architecture
Guide
》。当然如果要做到更完善的排版,还需要掌握段落排版(Paragragh
Style)相关的知识,但是如果只是完成聊天框内的文字排版,以上的基础知识已经够用了。详细的段落样式相关知识可以参考: 《Ruler and Paragraph Style Programming
Topics

CoreText

iOS/OSX中用于描述富文本的类是NSAttributedString,顾名思义,它比NSString多了Attribute的概念。它可以包含很多属性,粗体,斜体,下划线,颜色,背景色等等,每个属性都有其对应的字符区域。在OSX上我们只需解析完毕相应的数据,准备好NSAttributedString即可,底层的绘制完全可以交给相应的控件完成。但是在iOS上就没有这么方便,想要绘制Attributed
String就需要用到CoreText了。(当然iOS6之后已经有AttributedLabel了。)

使用CoreText进行NSAttributedString的绘制,最重要的两个概念就是CTFrameSetter和CTFrame。他们的关系如下:

其中CTFramesetter是由CFAttributedString(NSAttributedString)初始化而来,可以认为它是CTFrame的一个Factory,通过传入CGPath生成相应的CTFrame并使用它进行渲染:直接以CTFrame为参数使用CTFrameDraw绘制或者从CTFrame中获取CTLine进行微调后使用CTLineDraw进行绘制。

一个CTFrame是由一行一行的CLine组成,每个CTLine又会包含若干个CTRun(既字形绘制的最小单元),通过相应的方法可以获取到不同位置的CTRun和CTLine,以实现对不同位置touch事件的响应。

图文混排的实现

CoreText实际上并没有相应API直接将一个图片转换为CTRun并进行绘制,它所能做的只是为图片预留相应的空白区域,而真正的绘制则是交由CoreGraphics完成。(像OSX就方便很多,直接将图片打包进NSTextAttachment即可,根本无须操心绘制的事情,所以基于这个想法,M80AttributedLabel的接口和实现也是使用了attachment这么个概念,图片或者UIView都是被当作文字段中的attachment。)

在CoreText中提供了CTRunDelegate这么个Core
Foundation类,顾名思义它可以对CTRun进行拓展:AttributedString某个段设置kCTRunDelegateAttributeName属性之后,CoreText使用它生成CTRun是通过当前Delegate的回调来获取自己的ascent,descent和width,而不是根据字体信息。这样就给我们留下了可操作的空间:用一个空白字符作为图片的占位符,设好Delegate,占好位置,然后用CoreGraphics进行图片的绘制。以下就是整个图文混排代码描述的过程:

占位:

  1. - (void)appendAttachment: (M80AttributedLabelAttachment *)attachment

  2. {

  3. attachment.fontAscent                   = _fontAscent;

  4. attachment.fontDescent                  = _fontDescent;

  5. unichar objectReplacementChar           = 0xFFFC;

  6. NSString *objectReplacementString       = [NSString stringWithCharacters:&objectReplacementChar length:1];

  7. NSMutableAttributedString *attachText   = [[NSMutableAttributedString alloc]initWithString:objectReplacementString];
  8. CTRunDelegateCallbacks callbacks;

  9. callbacks.version       = kCTRunDelegateVersion1;

  10. callbacks.getAscent     = ascentCallback;

  11. callbacks.getDescent    = descentCallback;

  12. callbacks.getWidth      = widthCallback;

  13. callbacks.dealloc       = deallocCallback;
  14. CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (void *)attachment);

  15. NSDictionary *attr = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)delegate,kCTRunDelegateAttributeName, nil];

  16. [attachText setAttributes:attr range:NSMakeRange(0, 1)];

  17. CFRelease(delegate);
  18. [_attachments addObject:attachment];

  19. [self appendAttributedText:attachText];

  20. }

实现委托回调:

  1. CGFloat ascentCallback(void *ref)

  2. {

  3. M80AttributedLabelAttachment *image = (__bridge M80AttributedLabelAttachment *)ref;

  4. CGFloat ascent = 0;

  5. CGFloat height = [image boxSize].height;

  6. switch (image.alignment)

  7. {

  8. case M80ImageAlignmentTop:

  9. ascent = image.fontAscent;

  10. break;

  11. case M80ImageAlignmentCenter:

  12. {

  13. CGFloat fontAscent  = image.fontAscent;

  14. CGFloat fontDescent = image.fontDescent;

  15. CGFloat baseLine = (fontAscent + fontDescent) / 2 - fontDescent;

  16. ascent = height / 2 + baseLine;

  17. }

  18. break;

  19. case M80ImageAlignmentBottom:

  20. ascent = height - image.fontDescent;

  21. break;

  22. default:

  23. break;

  24. }

  25. return ascent;

  26. }
  27. CGFloat descentCallback(void *ref)

  28. {

  29. M80AttributedLabelAttachment *image = (__bridge M80AttributedLabelAttachment *)ref;

  30. CGFloat descent = 0;

  31. CGFloat height = [image boxSize].height;

  32. switch (image.alignment)

  33. {

  34. case M80ImageAlignmentTop:

  35. {

  36. descent = height - image.fontAscent;

  37. break;

  38. }

  39. case M80ImageAlignmentCenter:

  40. {

  41. CGFloat fontAscent  = image.fontAscent;

  42. CGFloat fontDescent = image.fontDescent;

  43. CGFloat baseLine = (fontAscent + fontDescent) / 2 - fontDescent;

  44. descent = height / 2 - baseLine;

  45. }

  46. break;

  47. case M80ImageAlignmentBottom:

  48. {

  49. descent = image.fontDescent;

  50. break;

  51. }

  52. default:

  53. break;

  54. }
  55. return descent;
  56. }
  57. CGFloat widthCallback(void* ref)

  58. {

  59. M80AttributedLabelAttachment *image  = (__bridge M80AttributedLabelAttachment *)ref;

  60. return [image boxSize].width;

  61. }

真正的绘制:

  1. - (void)drawAttachments

  2. {

  3. if ([_attachments count] == 0)

  4. {

  5. return;

  6. }

  7. CGContextRef ctx = UIGraphicsGetCurrentContext();

  8. if (ctx == nil)

  9. {

  10. return;

  11. }
  12. CFArrayRef lines = CTFrameGetLines(_textFrame);

  13. CFIndex lineCount = CFArrayGetCount(lines);

  14. CGPoint lineOrigins[lineCount];

  15. CTFrameGetLineOrigins(_textFrame, CFRangeMake(0, 0), lineOrigins);

  16. NSInteger numberOfLines = [self numberOfDisplayedLines];

  17. for (CFIndex i = 0; i < numberOfLines; i++)

  18. {

  19. CTLineRef line = CFArrayGetValueAtIndex(lines, i);

  20. CFArrayRef runs = CTLineGetGlyphRuns(line);

  21. CFIndex runCount = CFArrayGetCount(runs);

  22. CGPoint lineOrigin = lineOrigins[i];

  23. CGFloat lineAscent;

  24. CGFloat lineDescent;

  25. CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, NULL);

  26. CGFloat lineHeight = lineAscent + lineDescent;

  27. CGFloat lineBottomY = lineOrigin.y - lineDescent;
  28. // Iterate through each of the "runs" (i.e. a chunk of text) and find the runs that

  29. // intersect with the range.

  30. for (CFIndex k = 0; k < runCount; k++)

  31. {

  32. CTRunRef run = CFArrayGetValueAtIndex(runs, k);

  33. NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);

  34. CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];

  35. if (nil == delegate)

  36. {

  37. continue;

  38. }

  39. M80AttributedLabelAttachment* attributedImage = (M80AttributedLabelAttachment *)CTRunDelegateGetRefCon(delegate);
  40. CGFloat ascent = 0.0f;

  41. CGFloat descent = 0.0f;

  42. CGFloat width = (CGFloat)CTRunGetTypographicBounds(run,

  43. CFRangeMake(0, 0),

  44. &ascent,

  45. &descent,

  46. NULL);
  47. CGFloat imageBoxHeight = [attributedImage boxSize].height;

  48. CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, nil);
  49. CGFloat imageBoxOriginY = 0.0f;

  50. switch (attributedImage.alignment)

  51. {

  52. case M80ImageAlignmentTop:

  53. imageBoxOriginY = lineBottomY + (lineHeight - imageBoxHeight);

  54. break;

  55. case M80ImageAlignmentCenter:

  56. imageBoxOriginY = lineBottomY + (lineHeight - imageBoxHeight) / 2.0;

  57. break;

  58. case M80ImageAlignmentBottom:

  59. imageBoxOriginY = lineBottomY;

  60. break;

  61. }
  62. CGRect rect = CGRectMake(lineOrigin.x + xOffset, imageBoxOriginY, width, imageBoxHeight);

  63. UIEdgeInsets flippedMargins = attributedImage.margin;

  64. CGFloat top = flippedMargins.top;

  65. flippedMargins.top = flippedMargins.bottom;

  66. flippedMargins.bottom = top;
  67. CGRect attatchmentRect = UIEdgeInsetsInsetRect(rect, flippedMargins);
  68. id content = attributedImage.content;

  69. if ([content isKindOfClass:[UIImage class]])

  70. {

  71. CGContextDrawImage(ctx, attatchmentRect, ((UIImage *)content).CGImage);

  72. }

  73. else if ([content isKindOfClass:[UIView class]])

  74. {

  75. UIView *view = (UIView *)content;

  76. if (view.superview == nil)

  77. {

  78. [self addSubview:view];

  79. }

  80. CGRect viewFrame = CGRectMake(attatchmentRect.origin.x,

  81. self.bounds.size.height - attatchmentRect.origin.y - attatchmentRect.size.height,

  82. attatchmentRect.size.width,

  83. attatchmentRect.size.height);

  84. [view setFrame:viewFrame];

  85. }

  86. else

  87. {

  88. NSLog(@"Attachment Content Not Supported %@",content);

  89. }
  90. }

  91. }

  92. }

详细的代码可以直接在github上查看: https://github.com/xiangwangfeng/M80AttributedLabel/

[转] iOS文字排版(CoreText)那些事儿

时间: 2024-10-18 22:48:49

[转] iOS文字排版(CoreText)那些事儿的相关文章

iOS文字排版(CoreText)

和我们平时说的字体不同,计算机意义上的字体表示的是同一大小,同一样式(Style)字形的集合.从这个意义上来说,当我们为文字设置粗体,斜体时其实是使用了另外一种字体(下划线不算). iOSCoreText 转自阿毛的蛋疼地 第一次比较深入接触iOS文字排版相关内容是在12年底,实现某IM项目聊天内容的图文混排,照着nimbus的AttributedLabel和Raywenderlish上的这篇文章<Core Text Tutorial for iOS: Making a Magazine App

ios文字排版

文字排版的基础概念 字体(Font):和我们平时说的字体不同,计算机意义上的字体表示的是同一大小,同一样式(Style)字形的集合.从这个意义上来说,当我们为文字设置粗体,斜体时其实是使用了另外一种字体(下划线不算).而平时我们所说的字体只是具有相同设计属性的字体集合,即Font Family或typeface. 字符(Character)和字形(Glyphs):排版过程中一个重要的步骤就是从字符到字形的转换,字符表示信息本身,而字形是它的图形表现形式.字符一般就是指某种编码,如Unicode编

iOS开发 Coretext(文字排版)的基本用法

oreText 框架中最常用的几个类: CTFont CTFontCollection CTFontDescriptor CTFrame CTFramesetter CTGlyphInfo CTLine CTParagraphStyle CTRun CTTextTab CTTypesetter 先来了解一下该框架的整体视窗组合图: CTFrame 作为一个整体的画布(Canvas),其中由行(CTLine)组成,而每行可以分为一个或多个小方块(CTRun). 注意:你不需要自己创建CTRun,C

文字排版需要遵循的规律

文字排版必须遵循的规律 人类在阅读文字时的基本规则是从上到下,从左到右的.文案的排版应尽量遵从这一个规则,除特殊版式之外,尽可能减少参差不齐或者右对齐排列,大部分情况下,还是使用左对齐或居中对齐最为合适. 在设计中,可能都会遇到使用一些文字的特性,或者带有一点倾斜等,都是遵循左对齐原则. 这样不仅美观而且使阅读不会形成混乱. 当沿着某一版块或素材的左侧排列文字的时候我们使用右对齐会比左对齐和居中对齐要有更好的表达效果. 使用居中对齐让版面整体有自由感. 技巧2:明确 主.副标题与描述 文字的排版

iOS文字大小自适应库(MBFontAdapter)诞生记

背景 什么是iOS文字大小自适应?单纯回答这个问题实在太困难-所以讲一个故事,就很好理解了: 某天做UI设计的妹子弱弱地问我:"我们的app可不可以实现文字的字号在iPhone6上和iPhone6Plus上比iPhone4s和iPhone5大一些?如果要实现难度是不是比较大?工作量会不会增加很多?"其实当我听到这个问题的时候,我整个人都不好了,因为目前主流的app貌似没有这么干的.然而,这个时候能回答不可以么?能回答很难么?能回答工作量会增加很多么?能告诉UI主流app都不这么干么?显

Android - 实现两端对齐的文字排版

要实现Android两端对齐的文字排版效果,我们当然可以继承原有的TextView来实现,但一个更简单的方式就是使用WebView,利用HTML样式来实现. 首先定义一个String常量,我们可以将它视为一个HTML模板: private static final String WEBVIEW_CONTENT = "<html><head></head><body style=\"text-align:justify;margin:0;\&qu

文字排版--字体、字号、颜色、粗体、斜体、下划线、删除线

我们可以使用css样式为网页中的文字设置字体.字号.颜色等样式属性.下面我们来看一个例子,下面代码实现:为网页中的文字设置字体为宋体. body{font-family:"宋体";} 这里注意不要设置不常用的字体,因为如果用户本地电脑上如果没有安装你设置的字体,就会显示浏览器默认的字体.(因为用户是否可以看到你设置的字体样式取决于用户本地电脑上是否安装你设置的字体.)现在一般网页喜欢设置"微软雅黑",如下代码: body{font-family:"Micr

iOS文字滚动效果 之纵向滚动

原文链接: iOS文字滚动效果 之纵向滚动 简书主页:http://www.jianshu.com/users/37f2920f6848 Github主页:https://github.com/MajorLMJ iOS开发者公会-技术1群 QQ群号:87440292 iOS开发者公会-技术2群 QQ群号:232702419 iOS开发者公会-议事区   QQ群号:413102158

iOS文字滚动效果 之横向滚动

原文链接: iOS文字滚动效果 之横向滚动 简书主页:http://www.jianshu.com/users/37f2920f6848 Github主页:https://github.com/MajorLMJ iOS开发者公会-技术1群 QQ群号:87440292 iOS开发者公会-技术2群 QQ群号:232702419 iOS开发者公会-议事区   QQ群号:413102158