CoreText基本知识

来源:互联网 发布:如何提高记忆力 知乎 编辑:程序博客网 时间:2024/06/07 06:15

第一次比较深入接触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.     } 
原文地址:
0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 画眼线老是晕妆怎么办 闷青色染的太绿怎么办 血氧饱和度80多怎么办 染发前洗了头发怎么办 剪了短发后悔了怎么办 短发被剪的太短怎么办 短发剪得太短怎么办 烫头发后洗头了怎么办 头发染得太黄了怎么办 烫发后一直掉发怎么办 头发染的太黄了怎么办 头发染色太浅了怎么办 怀孕60天没有胎心怎么办 染了深褐色很黑怎么办 路边停车费没交怎么办 3岁宝宝难入睡怎么办 一上火眼睛就肿怎么办 孩子上火眼睛红有眼屎怎么办 孩子眼屎多又黄怎么办 眼睛皮周围红痒怎么办 新买的拖鞋有味怎么办 毛巾变得滑滑的怎么办 买的挂钩粘不住怎么办 吸墙挂钩吸不住怎么办 沾挂钩不粘了怎么办 粘钩掉了不粘了怎么办 贴墙挂钩粘不住怎么办 月经量大血块多怎么办 23岁乳房小扁平怎么办 十六岁基本没胸怎么办 肚子上的肉松弛怎么办 17岁乳房外扩该怎么办 胸下垂严重怎么办 17岁 棉条超过8小时了怎么办 在学校来了月经怎么办 如果在学校来月经怎么办 来月经流血量大怎么办 非经期出血量多怎么办 在学校来月经了怎么办 月经好久不来了怎么办 例假推迟了20天怎么办