本文转载至 http://blog.cnbang.net/tech/2729/

上一篇介绍了DTCoreText怎样把HTML+CSS解析转换成NSAttributeString,本篇接着看看怎样把NSAttributeString渲染出来。

CoreText

先简单介绍下CoreText,CoreText是iOS/OSX里的文字渲染引擎,在iOS/OSX上看到的所有文字在底层都是由CoreText去渲染。

CoreText会把一行里连在一起相同属性的文字合在一起作为一个CTRun,每一行是一个CTLine,多行合在一起组成CTFrame。如上图,第一行的文字有两种样式,第一部分是加粗,第二部分是斜体,因为样式不同所以分成了两个CTRun,CTLine包含了这两个CTRun,CTFrame包含了所有CTLine。

一个NSAttributeString可以通过CoreText提供的方法生成CTFramesetter,CTFramesetter是用于创建CTFrame的工厂,给CTFramesetter一个CGPath,或者简单理解为给他一个框框,它就会通过它持有的CTTypesetter生成CTFrame,CTFrame生成时里面包含的CTLine和CTRun就全部生成好了,可以直接绘制到画布上。CTFrame/CTLine/CTRun都提供了渲染接口,但前两者是封装,最后实际都是调用到CTRun的渲染接口去绘制。

如果要用CoreText渲染NSAttributeString,可以简单生成CTFramesetter,再生成CTFrame,在UIView的drawRect方法里直接把CTFrame绘制到当前画布上:

1
2
3
4
5
6
7
8
- (void) drawRect:(CGRect)rect
{
     UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 320, 400)];
     CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)content);
     CTFrame frame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0, 0), [path CGPath] , NULL);
     CGContextRef ctx = UIGraphicsGetCurrentContext();
     CTFrameDraw(frame, ctx);
}

CoreText会按NSAttributeString里的样式属性把文字渲染出来。这种是最简单的粗粒度的渲染方式,但如果需要对文字渲染再做进一步处理,例如添加背景色等这些CoreText没有支持的属性,或者要在文字中间插入图片,就不能简单绘制CTFrame,需要逐行或逐个CTRun处理。

概览

DTCoreText需要处理穿插在文字里的各类Attachment,并支持文字背景色,段缩进等CoreText不支持的属性,不能简单把NSAttributeString扔给CoreText渲染了事,需要做更细致的处理。DTCoreText分了几层,整体结构图:

最上层是使用者,可以是Controller,例如项目里示例的DemoTextViewController,也可以是某视图类。接着是DTCoreText封装好的各个控件,自带的有Label,TextView和Cell,这些控件的文字渲染都由DTAttributedTextContentView负责,非文字部分例如图片/视频等元素会在上层使用者那里通过delegate传给DTAttributedTextContentView。DTCoreTextLayouter / DTCoreTextLayoutFrame / DTCoreTextLayoutLine / DTCoreTextGlyphRun这四个类分别对应CoreText里的CTFramesetter / CTFrame / CTLine / CTRun,模仿了CoreText的模式,功能和作用一样,只是在它们基础上添加了功能。接下来看看每一个类具体做了什么事情。

DTAttributedTextContentView

DTAttributedTextContentView继承自UIView,作为DTCoreTextLayoutFrame和上层控件的中间层,负责按需求绘制内容,大致做了以下几件事:

1.支持CATiledLayer分段渲染

把UIView的layerClass设为CATiledLayer就能实现分区域渲染,即只渲染显示在屏幕上的区域,类似那些地图APP的效果,主要用于像TextView这样可能内容很长的控件,避免一次性把全部内容渲染出来,只渲染能看到的部分,提高性能。使用CATiledLayer后,在-drawLayer:inContext:方法里用CGContextGetClipBoundingBox通过context取得当前显示的区域,DTCoreTextLayoutFrame只渲染这个区域的内容就行了。

2.生成DTCoreTextLayoutFrame并绘制

通过上层传进来的NSAttributeString生成DTCoreTextLayouter和DTCoreTextLayoutFrame,进行各种配置后用DTCoreTextLayoutFrame渲染文字到当前layer上,这些配置包括 是否显示图片链接/限定行数/断行规则等。

3.处理Attachment和Link

-layoutSubviewsInRect:方法里遍历DTCoreTextLayoutFrame里的每一个DTCoreTextGlyphRun,找出有附件和链接的Run进行处理,附件包括图片/视频等,创建这些附件对应的view,把这些view按DTCoreTextGlyphRun计算好的位置添加到专门存放附件和链接的customViews上完事。

实际上这些附件view的创建是在上层使用者那里,DTAttributedTextContentView通过delegate把每个附件的内容和对应的frame传到上层生成相应的view再给回来,这样做估计是因为对附件的处理每个使用者的需求都不一样,不应该直接写死在底层,例如有些使用者要求图片需要点击后放大,视频需要用自己的控件等。

DTCoreTextLayouter

DTCoreTextLayouter负责生成和缓存DTCoreTextLayoutFrame,相当于CTFramesetter和CTFrame的关系,做的事很简单,就是通过NSAttributeString生成CTFramesetter,再根据不同的rect生成DTCoreTextLayoutFrame,并缓存这些frame。

DTCoreTextLayoutFrame

DTCoreTextLayoutFrame是最重要的一个类,负责渲染文字,主要做了两件事:生成行和渲染每一行。

生成DTCoreTextLayoutLine

-_buildLinesWithTypesetter:会创建出当前frame范围内可见的每一行DTCoreTextLayoutLine,创建过程中做的处理包括:

1.支持整段缩进

从NSAttributeString里取出当前行是否有表示缩进的DTTextBlock,如果需要缩进,要计算出当前行缩进后的宽度和位置。

2.支持截断加省略号

上层像Label/TextView这样的控件是限制了宽高的,如果内容超出了宽高,就需要对最后一行进行处理,在合适的位置加”…”。

这里有个问题,就是必须在渲染到超出宽高的那一行时,才知道要处理的最后一行是什么。例如一个TextView高40,文字每行高15,在渲染第三行时高已经到45,发现已经超出了TextView的高度,这时知道只能渲染到第二行,但当前已经处理到第三行了,需要把第二行拿出来截断加”…”。

另外除了超出高度,在超出外部传进来的numberOfLines时也要截断,为了统一流程,这里的做法是在渲染超出高度时记录总共可以渲染多少行(_numberLinesFitInFrame),然后全部重新来,从头到尾再生成每一行,这时已经知道总共有多少行,在生成最后一行时处理就行了。这样做优点是简单粗暴避免重复代码,缺点是浪费性能,前面所有行都要重新排一遍。

3.支持hyphen

hyphen是连字符号,就是让英文单词在合适的位置换行并加上破折号”-”。CoreText原生不支持hyphen,断行方式只有按单词断行和安字母断行。这里hyphen的实现方式是:在所有英文单词里可以加破折号的位置全部加上占位符0x00AD,例如location->lo-ca-tion->lo0x00ADca0x00ADtion。0x00AD是不可见字符,CoreText不会渲染这个字符,但在这个字符的位置是可以断行的,CoreText不再认为location是一个单词,会在占位符处换行。DTCoreText做的处理就是如果发现换行处是占位符0x00AD,就替换成破折号”-”,所以要支持hyphen,传进来的内容就必须是所有单词都写好占位符的,否则无效。

4.计算每一行在当前frame的位置

在生成每一行时是不知道这一行在当前frame的位置的,需要自己手动计算。每一行的x坐标容易确定,但y坐标的计算就要费一番功夫。要考虑的因素有当前行高,上一行位置,行距,段间距,padding,baseline等。

如图,每一行以baseline为基准,需要计算出这一行的baseline在当前frame的Y坐标值,asent与descent是CoreText给出的值,asent+descent就是行高。推算当前行baseline位置的流程是:

  • A.计算上一行的行末位置,即baseline+descent
  • B.计算上一行行间距的一半,例如1.5倍行间距,就是 ((1.5 – 1)*asent+descent)/2
  • C.计算当前行行间距的一半,算法同上,只是这一行的行间距不一定与上一行一致。这里两行各算一半也是为了不同行间距的中和。
  • D.上述计算结果相加,再加上当前行asent值,就得到当前行的baseline Y坐标值。

除了上述主流程,还针对首行,段首段尾,DTTextBlock的留白和附件Attachment做了处理,计算的逻辑在-_algorithmWebKit_BaselineOriginToPositionLine

5.处理对齐

要对每一种对齐方式进行处理,右对齐和居中对齐需要计算出行的x坐标值,两端对齐需要通过CTLineCreateJustifiedLine方法重新创建出一个两端对齐的行,针对两端对齐这里还要了两件事,一是段末不做两端对齐,二是若内容长度不够(默认是不足行宽的60%)也不做两端对齐,避免文字间距拉伸得太厉害效果差。

6.封装成DTCoreTextLayoutLine

经过上述处理,每一行的CTLine对象以及这一行的位置信息都有了,把这些封装成DTCoreTextLayoutLine保存起来,任务就完成了。

渲染

DTCoreTextLayoutFrame对外提供了-drawInContext:options:方法,用于把上述生成的每一行都渲染到传进来的context画布上。做的处理包括:

1.绘制DTTextBlock样式

DTCoreText支持段落加背景色,在这里会先找出所有DTTextBlock,通过一系列麻烦的方法取到这些block的坐标和大小,把它们对应的背景色画出来。

2.绘制附件

实现了DTTextAttachmentDrawing接口的附件可以在这里跟文字一起绘制出来,在DTCoreText里图片附件就是实现了DTTextAttachmentDrawing接口,可以直接把图片在这里绘制出来。实际上图片附件的渲染DTCoreText提供了两种方式,上面介绍DTAttributedTextContentView时说图片附件也可以在上层让用户自行添加,若要在上层自行添加,可以传参数告诉DTCoreTextLayoutFrame绘制时不要处理图片附件。

3.绘制文字和阴影

最后就是再遍历每一行DTCoreTextLayoutLine以及行里的每一个DTCoreTextGlyphRun,调用它的-drawInContext:方法逐个run绘制到画布上。绘制时需要算好每个Run的位置,调用CGContextSetTextPosition定位到指定位置绘制文字。绘制文字同时还处理了阴影效果,CoreText不直接支持文字阴影效果,但可以用CoreGraphic的接口在绘制时加上阴影,这里还支持同时存在多个shadow -_-!

DTCoreTextLayoutLine

DTCoreTextLayoutLine封装了CTLine,做的事包括:

1.生成GlyphRun

通过CTLine可以取出所这一行里的CTRun,计算每个CTRun的位置,封装生成DTCoreTextGlyphRun。

2.计算属性和提供辅助方法

计算并保存了这一行asent/descent/lineHeight等属性,提供各种辅助方法方便获取这一行里的信息,包括通过stringIndex获取对应文字的坐标等,CTLine相关的几个方法例如CTLineGetOffsetForStringIndex() / CTLineGetStringIndexForPosition()也有相应的封装。

DTCoreTextGlyphRun

DTCoreTextGlyphRun里做的事跟DTCoreTextLayoutLine差不多,只是在渲染方法里额外做了一些事,首先支持文字背景色,这是CoreText原生不支持的,如果Attribute里有背景色的属性,这里会绘制出来。然后支持iOS6以下文字的下划线和删除线,iOS6以前CoreText是不支持下划线和删除线的,这里自己做了处理把它画上去。

总结

整个流程最核心的就是DTCoreTextLayoutFrame生成行和渲染的实现,相当于把CoreText原生的CTFramesetterCreateFrame / CTFrameDraw再自己实现了一遍,在实现的过程加上自己特殊的需求,从中我们也可以大致了解到CTFrame/CTLine内部大致实现是怎样的。CoreText已经提供了足够细粒度的接口让使用者可以按自己意愿去随意排版,DTCoreText这一系列的处理给出了很好的示例可供参考。

iOS富文本组件的实现—DTCoreText源码解析 渲染篇的更多相关文章

  1. iOS富文本组件的实现—DTCoreText源码解析 数据篇

    本文转载 http://blog.cnbang.net/tech/2630/ DTCoreText是个开源的iOS富文本组件,它可以解析HTML与CSS最终用CoreText绘制出来,通常用于在一些需 ...

  2. iOS Socket 整理以及CocoaAsyncSocket、SRWebSocket源码解析(一)

    写在准备动手的时候: Socket通讯在iOS中也是很常见,自己最近也一直在学习Telegram这个开源项目,Telegram就是在Socket的基础上做的即时通讯,这个相信了解这个开源项目的也都知道 ...

  3. jQuery2.x源码解析(缓存篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...

  4. jQuery2.x源码解析(构建篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 笔者阅读了园友艾伦 Aaron的系列博客< ...

  5. jQuery2.x源码解析(设计篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 这一篇笔者主要以设计的角度探索jQuery的源代 ...

  6. jQuery2.x源码解析(回调篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 通过艾伦的博客,我们能看出,jQuery的pro ...

  7. Shiro源码解析-Session篇

    上一篇Shiro源码解析-登录篇中提到了在登录验证成功后有对session的处理,但未详细分析,本文对此部分源码详细分析下. 1. 分析切入点:DefaultSecurityManger的login方 ...

  8. abp vnext2.0核心组件之.Net Core默认DI组件切换到AutoFac源码解析

    老版Abp对Castle的严重依赖在vnext中已经得到了解决,vnext中DI容器可以任意更换,为了实现这个功能,底层架构相较于老版abp,可以说是进行了高度重构.当然这得益于.Net Core的D ...

  9. myBatis源码解析-数据源篇(3)

    前言:我们使用mybatis时,关于数据源的配置多使用如c3p0,druid等第三方的数据源.其实mybatis内置了数据源的实现,提供了连接数据库,池的功能.在分析了缓存和日志包的源码后,接下来分析 ...

随机推荐

  1. SSH远程启动tomcat后,退出SSH,tomcat也退出

    在原本的启动命令 ./catalina.sh start前面加上nohup,即nohup ./catalina.sh start其中,nohup 是linux命令,不挂断运行,具体如下:语法:nohu ...

  2. [转]java加密算法简要介绍

    本篇内容简要介绍BASE64.MD5.SHA.HMAC几种加密算法.     BASE64编码算法不算是真正的加密算法.     MD5.SHA.HMAC这三种加密算法,可谓是非可逆加密,就是不可解密 ...

  3. Android 8 AudioPolicy 分析

    AudioTrack最终会调用AudioPolicyManager::getOutput(); frameworks\av\services\audiopolicy\managerdefault\Au ...

  4. C语言中重要的小知识汇总

    用于整理记录一些C语言下的小知识点: 1. 在C语言中,怎么查看一个数据类型占用了多少个字节大小呢? 可以使用sizeof(int)/sizeof(double)等来查看某数据类型到底用了几个字节: ...

  5. Mac eclipse 快捷键 f6、f8 失效

    解决方法:

  6. Spring系列(一):Spring的基本概念及其核心

    一.Spring是什么 Spring是一种多层的J2EE应用程序框架,其核心就是提供一种新的机制管理业务对象及其依赖关系. 二.为什么要使用Spring 1. 降低组件之间的耦合度,实现软件各层之间的 ...

  7. svn -- svn简介

    一.为什么需要SVN 你们在做中级项目中,都是采用小组合作开发的?那么说说你们在后期整合中遇到问题? 主要应用于: 1.协作开发 2.远程协作 3.版本回退 二.什么是SVN l svn全称SubVe ...

  8. 每天一个linux命令:cd命令

    Linux cd 命令可以说是Linux中最基本的命令语句,其他的命令语句要进行操作,都是建立在使用 cd 命令上的. 所以,学习Linux 常用命令,首先就要学好 cd 命令的使用方法技巧. 1. ...

  9. mysql 中 时间和日期函数

    From: http://www.cnblogs.com/redfox241/archive/2009/07/23/1529092.html 一.MySQL 获得当前日期时间 函数 1.1 获得当前日 ...

  10. PHP替换回车换行的三种方法

    一个小小的换行,其实在不同的平台有着不同的实现,为什么要这样,世界是多样的! 本来在Unix世界换行用/n来代替换行, Windows为了体现不同,就用/r/n, 更有意思的是,Mac中又用了/r. ...