iOS文字排版(CoreText)

简介: <p><span style="color:rgb(51,51,51); font-family:'Helvetica Neue',Helvetica,STheiti,微软雅黑,黑体,Arial,Tahoma,sans-serif,serif; font-size:14px; line-height:25px; background-color:rgb(240,240,240)">和我们平

和我们平时说的字体不同,计算机意义上的字体表示的是同一大小,同一样式(Style)字形的集合。从这个意义上来说,当我们为文字设置粗体,斜体时其实是使用了另外一种字体(下划线不算)。


转自阿毛的蛋疼地

 

第一次比较深入接触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.     attachment.fontAscent                   = _fontAscent; 
  3.     attachment.fontDescent                  = _fontDescent; 
  4.     unichar objectReplacementChar           = 0xFFFC; 
  5.     NSString *objectReplacementString       = [NSString stringWithCharacters:&objectReplacementChar length:1]; 
  6.     NSMutableAttributedString *attachText   = [[NSMutableAttributedString alloc]initWithString:objectReplacementString]; 
  7.  
  8.     CTRunDelegateCallbacks callbacks; 
  9.     callbacks.version       = kCTRunDelegateVersion1; 
  10.     callbacks.getAscent     = ascentCallback; 
  11.     callbacks.getDescent    = descentCallback; 
  12.     callbacks.getWidth      = widthCallback; 
  13.     callbacks.dealloc       = deallocCallback; 
  14.  
  15.     CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (void *)attachment); 
  16.     NSDictionary *attr = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)delegate,kCTRunDelegateAttributeName, nil]; 
  17.     [attachText setAttributes:attr range:NSMakeRange(0, 1)]; 
  18.     CFRelease(delegate); 
  19.  
  20.     [_attachments addObject:attachment]; 
  21.     [self appendAttributedText:attachText]; 
 
实现委托回调:
    
    
  1. CGFloat ascentCallback(void *ref) 
  2.     M80AttributedLabelAttachment *image = (__bridge M80AttributedLabelAttachment *)ref; 
  3.     CGFloat ascent = 0; 
  4.     CGFloat height = [image boxSize].height; 
  5.     switch (image.alignment) 
  6.     { 
  7.         case M80ImageAlignmentTop: 
  8.             ascent = image.fontAscent; 
  9.             break
  10.         case M80ImageAlignmentCenter: 
  11.         { 
  12.             CGFloat fontAscent  = image.fontAscent; 
  13.             CGFloat fontDescent = image.fontDescent; 
  14.             CGFloat baseLine = (fontAscent + fontDescent) / 2 - fontDescent; 
  15.             ascent = height / 2 + baseLine; 
  16.         } 
  17.             break
  18.         case M80ImageAlignmentBottom: 
  19.             ascent = height - image.fontDescent; 
  20.             break
  21.         default
  22.             break
  23.     } 
  24.     return ascent; 
  25.  
  26. CGFloat descentCallback(void *ref) 
  27.     M80AttributedLabelAttachment *image = (__bridge M80AttributedLabelAttachment *)ref; 
  28.     CGFloat descent = 0; 
  29.     CGFloat height = [image boxSize].height; 
  30.     switch (image.alignment) 
  31.     { 
  32.         case M80ImageAlignmentTop: 
  33.         { 
  34.             descent = height - image.fontAscent; 
  35.             break
  36.         } 
  37.         case M80ImageAlignmentCenter: 
  38.         { 
  39.             CGFloat fontAscent  = image.fontAscent; 
  40.             CGFloat fontDescent = image.fontDescent; 
  41.             CGFloat baseLine = (fontAscent + fontDescent) / 2 - fontDescent; 
  42.             descent = height / 2 - baseLine; 
  43.         } 
  44.             break
  45.         case M80ImageAlignmentBottom: 
  46.         { 
  47.             descent = image.fontDescent; 
  48.             break
  49.         } 
  50.         default
  51.             break
  52.     } 
  53.  
  54.     return descent; 
  55.  
  56.  
  57. CGFloat widthCallback(void* ref) 
  58.     M80AttributedLabelAttachment *image  = (__bridge M80AttributedLabelAttachment *)ref; 
  59.     return [image boxSize].width; 
 
真正的绘制:
    
    
  1. - (void)drawAttachments 
  2.     if ([_attachments count] == 0) 
  3.     { 
  4.         return
  5.     } 
  6.     CGContextRef ctx = UIGraphicsGetCurrentContext(); 
  7.     if (ctx == nil) 
  8.     { 
  9.         return
  10.     } 
  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.  
  29.         // Iterate through each of the "runs" (i.e. a chunk of text) and find the runs that 
  30.         // intersect with the range. 
  31.         for (CFIndex k = 0; k < runCount; k++) 
  32.         { 
  33.             CTRunRef run = CFArrayGetValueAtIndex(runs, k); 
  34.             NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run); 
  35.             CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName]; 
  36.             if (nil == delegate) 
  37.             { 
  38.                 continue
  39.             } 
  40.             M80AttributedLabelAttachment* attributedImage = (M80AttributedLabelAttachment *)CTRunDelegateGetRefCon(delegate); 
  41.  
  42.             CGFloat ascent = 0.0f; 
  43.             CGFloat descent = 0.0f; 
  44.             CGFloat width = (CGFloat)CTRunGetTypographicBounds(run, 
  45.                                                                CFRangeMake(0, 0), 
  46.                                                                &ascent, 
  47.                                                                &descent, 
  48.                                                                NULL); 
  49.  
  50.             CGFloat imageBoxHeight = [attributedImage boxSize].height; 
  51.             CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, nil); 
  52.  
  53.             CGFloat imageBoxOriginY = 0.0f; 
  54.             switch (attributedImage.alignment) 
  55.             { 
  56.                 case M80ImageAlignmentTop: 
  57.                     imageBoxOriginY = lineBottomY + (lineHeight - imageBoxHeight); 
  58.                     break
  59.                 case M80ImageAlignmentCenter: 
  60.                     imageBoxOriginY = lineBottomY + (lineHeight - imageBoxHeight) / 2.0; 
  61.                     break
  62.                 case M80ImageAlignmentBottom: 
  63.                     imageBoxOriginY = lineBottomY; 
  64.                     break
  65.             } 
  66.  
  67.             CGRect rect = CGRectMake(lineOrigin.x + xOffset, imageBoxOriginY, width, imageBoxHeight); 
  68.             UIEdgeInsets flippedMargins = attributedImage.margin; 
  69.             CGFloat top = flippedMargins.top; 
  70.             flippedMargins.top = flippedMargins.bottom; 
  71.             flippedMargins.bottom = top; 
  72.  
  73.             CGRect attatchmentRect = UIEdgeInsetsInsetRect(rect, flippedMargins); 
  74.  
  75.             id content = attributedImage.content; 
  76.             if ([content isKindOfClass:[UIImage class]]) 
  77.             { 
  78.                 CGContextDrawImage(ctx, attatchmentRect, ((UIImage *)content).CGImage); 
  79.             } 
  80.             else if ([content isKindOfClass:[UIView class]]) 
  81.             { 
  82.                 UIView *view = (UIView *)content; 
  83.                 if (view.superview == nil) 
  84.                 { 
  85.                     [self addSubview:view]; 
  86.                 } 
  87.                 CGRect viewFrame = CGRectMake(attatchmentRect.origin.x, 
  88.                                               self.bounds.size.height - attatchmentRect.origin.y - attatchmentRect.size.height, 
  89.                                               attatchmentRect.size.width, 
  90.                                               attatchmentRect.size.height); 
  91.                 [view setFrame:viewFrame]; 
  92.             } 
  93.             else 
  94.             { 
  95.                 NSLog(@"Attachment Content Not Supported %@",content); 
  96.             } 
  97.  
  98.         } 
  99.     } 
详细的代码可以直接在github上查看:  https://github.com/xiangwangfeng/M80AttributedLabel/

目录
相关文章
|
文字识别 算法 Swift
毕业设计在iOS上使用OpenCV实现图片中的文字框选文字识别
毕业设计在iOS上使用OpenCV实现图片中的文字框选文字识别
558 0
毕业设计在iOS上使用OpenCV实现图片中的文字框选文字识别
|
API iOS开发 Perl
iOS UIImageView文字头像,首字母缩略头像
iOS UIImageView文字头像,首字母缩略头像
iOS UIImageView文字头像,首字母缩略头像
|
iOS开发
iOS开发-使用ShareSDK做分享如何让新浪分享后是可点击蓝色文字
iOS开发-使用ShareSDK做分享如何让新浪分享后是可点击蓝色文字
115 0
iOS开发-使用ShareSDK做分享如何让新浪分享后是可点击蓝色文字
|
iOS开发
iOS开发 - 同一段文字显示不同颜色和字体
iOS开发 - 同一段文字显示不同颜色和字体
110 0
|
iOS开发
iOS开发-调整文字之间间距
iOS开发-调整文字之间间距
292 0
|
iOS开发
iOS开发-同一段文字显示不同颜色
iOS开发-同一段文字显示不同颜色
101 0
|
程序员 API iOS开发
iOS开发:字符串设置指定内容的文字颜色、文字大小、文字字体类型
在iOS开发过程中,会有一些为了提高APP的视觉效果而设置的特别一点的效果,比如一行文字需要自定义不同的颜色和文字大小,这就用到通过富文本来设置字符串的颜色、大小和文字类型。这篇博文我打算只介绍怎么设置指定内容的一些文字属性设置,如果之前看过我写的博文,就会发现有一篇类似介绍通过富文本来设置字符串内容的博文,但是那篇是综合性的,包括介绍button的,以及UItextfield的设置,所以在这里我只介绍怎么设置字符串指定位置的一些自定义设置的方法,如有不妥之处,欢迎指正。
603 0
|
iOS开发
iOS tabbar文字不显示原因
iOS tabbar文字不显示原因
938 0
|
iOS开发
设计iOS中随系统键盘弹收和内容文字长度自适应高度的文本框
设计iOS中随系统键盘弹收和内容文字长度自适应高度的文本框
209 0
设计iOS中随系统键盘弹收和内容文字长度自适应高度的文本框