文章转载自 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)那些事儿的更多相关文章

  1. iOS:基于CoreText的排版引擎

    一.CoreText的简介 CoreText是用于处理文字和字体的底层技术.它直接和Core Graphics(又被称为Quartz)打交道.Quartz是一个2D图形渲染引擎,能够处理OSX和iOS ...

  2. iOS开发-UITextView文字排版

    UITextView文本排版 1.配置NSMutableParagraphStyle NSMutableParagraphStyle *MParaStyle = [[NSMutableParagrap ...

  3. (转)iOS7界面设计规范(10) - UI基础 - 文字排版与配色

    明天就是周四了.貌似前几天还在恨周一呢.话说今天几乎开了一整天的会,正经事情没做多少:这种感觉比一整天从早到晚12个小时的忙碌于一件事情还要让人感到疲惫的对叭?那今天的iOS7设计规范更新又是一篇很简 ...

  4. amazeui学习笔记--css(基本样式3)--文字排版Typography

    amazeui学习笔记--css(基本样式3)--文字排版Typography 一.总结 1.字体:amaze默认非 衬线字体(sans-serif) 2.引用块blockquote和定义列表:引用块 ...

  5. OpenJudge计算概论-文字排版

    /*====================================================================== 文字排版 总时间限制: 1000ms 内存限制: 65 ...

  6. div介绍 盒子模型边框属性 CSS初始化 文字排版 边框调整 溢出

    今天学习的div,了解了div是干什么用的掌握了什么是盒子模型,以及div的外边距内边距以及边框,运用div和CSS给文字排版,利用边框的来做图像,div溢出的处理 CSS初始化: 精确排版的时候用这 ...

  7. 【html】文字排版

    Web开发过程中文字排版,默认的情况下,行末的长单词会撑开容器. 我们想要的是(像word一样.能够自动换行.既不撑大容器.也不强制拆开行末单词.并且不会隐藏行末单词的多余字母) ①不能撑开容器 ②完 ...

  8. bootstrap世界探索1——山川河流(文字排版)

    世界到底是什么?其实世界很简单,正所谓一花一世界,一树一菩提,世界就在我们身边.造物神是伟大的,在我看来无论是HTML,css,js都可以看作是一个世界,但是他们是构成宏观世界不可或缺的,正如IU框架 ...

  9. iOS App开发的那些事儿2:如何搭建合适的框架

    <iOS App开发的那些事儿>系列文章从更宏观的角度出发,不仅仅局限于具体某个功能.界面的实现,而是结合网易云信iOS端研发负责人多年的经验,从如何优化现有代码的角度出发,深度分析如何创 ...

随机推荐

  1. dynamic解析Http xml格式响应数据

    继续上一篇 构建RESTful风格的WCF服务 ,咱已经把服务端的数据和服务准备好了,客户端调用 wcf rest接口后如何解析xml?下面使用dynamic关键字解析来至于WCF REST XML响 ...

  2. RabbitMQ 上手记录-part 1-基础概念

    RabbitMQ是什么,不用多介绍了,毕竟名声在那,江湖地位摆着,搜索引擎收录着.为什么突然去学习这个框架了,毕竟工作中没有用得上(说来也惭愧,工作中开发的项目没有使用这个框架).但是作为互联网分布式 ...

  3. npm install 后缀

    npm 全局安装与本地安装 npm install express # 本地安装 npm install express -g # 全局安装 本地安装 将安装包放在 ./node_modules 下( ...

  4. BATJ面试必会之并发篇

    一.线程状态转换 新建(New) 可运行(Runnable) 阻塞(Blocking) 无限期等待(Waiting) 限期等待(Timed Waiting) 死亡(Terminated) 二.使用线程 ...

  5. visual studio 不能进入调试状态

    解决Windows操作系统在处理回环地址 1. 第一种解决方案是禁用环回检查. 步骤如下 a) 依次单击“开始”和“运行”,键入 regedit,然后单击“确定” b) 在注册表编辑器中,找到并单击下 ...

  6. mongodb int型id 自增

    mongo的c#客户端提供了接口IIdGenerator,有guid和objectid等几种实现,但没有int型id的实现 接口主要2个方法,一个IsEmpty返回bool,判断当前id值是否是空(估 ...

  7. CLR 中 线程的 ThreadState 解释

    ThreadState   Aborted 线程已停止 AbortedRequested 线程的 Thread.Abort() 方法已被调用,但线程还未停止. Background 线程在后台执行,与 ...

  8. Win8操作系统下IIS如何配置asp.net的运行环境(win7同样)

    一.把鼠标放在电脑屏幕的左下角然后右击,弹出如下图菜单,选择“程序和功能”(快捷键win+X).(win7点击电脑左下角的“开始”,然后点击“控制面板”打开程序与功能界面): 二.进入程序与功能界面后 ...

  9. 用struct模块解决tcp的粘包问题

    服务器端程序 import struct import socket sk = socket.socket() sk.bind(('127.0.0.1',9000)) sk.listen() conn ...

  10. 搭建本地svn

      1. 下载并安装TortoiseSVN,下载地址为:http://tortoisesvn.net/downloads.html.        2. 在本地创建一个文件夹,作为SVN服务的文件夹. ...