YYKit 之 YYText

本文的目的是希望能帮助到我们更快的熟悉和学习 YYText 的结构和实现的思路,如有不正确或者不准确的地方请指正,谢谢。  

一、特点与用法

     关于 YYText 的特点和用法请看 @ibireme 大神的 github:

          https://github.com/ibireme/YYText

二、使用到的组件

     1、介绍 YYLabel 之前先说一下 YYTextAttribute,因为后面会大量的使用到它。

     YYTextAttribute:

     1)定义的一些 Enum,YYTextAttributeType:attribute 的类型,有 None、UIKit、CoreText 和 YYText 四种类型;YYTextLineStyle:line 的样式;YYTextVerticalAlignment:垂直方向 text 的位置;YYTextDirection:text 的位置;YYTextTruncationType:text 截断的位置。

     2)YYText 中定义的 Attribute Name。主要是独有的一些类型。

     3)YYTextBackedString:可以将一些表情图片映射成纯文本。

     4)YYTextBinding:使一些特定的字符串绑定在一起,YYTextView 在选择和编辑他们的时候把他们当成一个单独的字符。

     5)YYTextShadow:用处和 NSShadow 一样,只是比 NSShadow 多了一些功能,比如说可以使用 blendMode(图形混合模式)、可以在 shadow 上再加一层 shadow。关于 blendMode 的学习,可以参见喵神的博客:https://onevcat.com/2013/04/using-blending-in-ios/

     6)YYTextDecoration:实现下划线(underline)和中间截线(strikethrough)时使用,线条的形式给出了几种样式,可以通过 YYTextLineStyle 枚举查看。具体是 underline 还是 strikethrough 是在 NSAttributedString+YYText 中 NSMutableAttributeString(YYText)中实现的方法。

     7)YYTextBorder: 实现在文本周围画一个 border,也可以是填充一个背景色。

     8)YYTextAttachment:封装需要放入 text 中的对象。在说明文档中提到,如果 attachment 是 UIImage,就绘制到 CGContext,如果是 UIView 或者 CALayer 就加入到 text container 的 view 或者 layer 中。

     9)YYTextHightlight:当 YYLabel 或者 YYTextView 中的 text 可以被用户按下时,被按下的 text 会有一个 highlighted 状态,这时候就需要是用 YYTextHighlight 来修改原来的 text。所以这个对象和 YYText 一样,只是是在 highlight 状态下的 YYText,而且添加了点击和长按事件。

     2、再来说一下 NSAttributedString+YYText 文件

       在这个分类中主要是实现了几类的操作:

          1)一些操作当前 attributed string 的方法

               比如说归档和反归档当前字符串、获得某个位置的 attributes、字间距、色值、背景色、shadow 等等。具体的参见文件,基本上是作者封装的方便获得各种数据的方法。(和 Foundation 中比强大太多了)。

          2)为 YYText 创建 attachment 的方法

          3)为 YYText 添加 YYText 特有的 attribute 的方法

          4)添加像设置属性一样的设置 character attribute 的 font、color、backgroundColor 等等的方法。

          5)添加像设置属性一样的设置 paragraph attribute 的方法。

          6)添加像设置属性一样的设置 YYText attribute 的方法。 

          7)使用 range 设置不连续的 attribute 的方法

          8)设置 text highlight 的便捷方法

          9)和其他的工具型的方法。

             

     3、NSParagraphStyle+YYText 文件

          提供了 CoreText 中的 CTParagraphStyleRef 和 NSParagraphStyle 之间的转化。

     4、YYTextParser

          这是一个 protocol,声明了一个 -(BOOL)parseText:(NSMutableAttributedString *)string selectedRange:(NSRangePointer)selectedRange; 方法。这个方式是遵守这个协议必须实现的方法,当 YYTextView 或者 YYLabel 中的 text 改变时被调用。返回 YES 说明修改了这个 text。

          作者简单的实现了 MarkdownParser 和 EmotionParser,两个原理都差不多一样,在这里只对 EmotionParser 做一下简单的介绍,希望能有所启发:

     这段代码就是生成正则表达式 _regex 和映射的字典 _mapper, 第一层 for 是获得你要匹配的 key,第二层是如果有这些特殊的字符需要转译一下,然后将这些需要比配的 key 用“|”连接起来。

这个就是修改 text 之后会被调用的方法,在这个方法里对输入的 text 进行匹配,如果匹配到之前 _mapper 中需要替换的字符,就将这个字符串替换为需要替换的表情符。

替换成表情符之后就需要重新计算这个表情符所占的 range 了,这个方法就是拿到替换之后的的 range。

          5、YYTextLayout

               先看一下文档中的说明,如下图:

是不是很眼熟?好像在哪见过?是的,就是 NSLayoutManager 和 NSTextContainer。他们的作用都是相似的。

          1)YYTextContainer

               支持矩形(CGSize)和图形(UIBezierPath)来初始化 YYTextContainer;

               在这里重点说一下 YYTextLinePositionModifier,它是 一个协议,定义了一个必须实现的方法,这个方法将会在 layout 完成的时候被调用,三个参数分别是存放 YYTextLine 的数组、完整的 text 和 layout container。

               YYTextLine:它是封装了 CTLineRef 的对象,封装了每一行 text 的具体展示位置、range、这一行拥有的 attachments 等等,只有一个类方法的初始化方法。如果不了解一些自行描述集的内容,对 textLine 中的一些属性和操作会不是很清晰,看下图:

     

     

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

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

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

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

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

上行高度 (Ascent) 和下行高度(Decent):一个字形最高点和最低点到基线的距离,所以行高就是 ascent + decent。

     看完上面的简单介绍你就能明白,在 YYTextLine 的 setCTLine 中的代码逻辑是从 CTLineRef 中取出对应的行宽、上行高度、下行高度、行间距、rangge 和第一个字型符的位置(这个在垂直布局会用到)。之后调用 reloadBounds 方法,重新计算当前行的 bounds、attachments 所在的 range 和 rect。

          2)YYTextLayout

               这个真的是核心内容了,这个文件一共 3300 多行的代码,从代码量上就能看出它的地位。这个类中存储着 text 的 layout 结果,所有的 property 都是 readonly 的。实现的接口有:

               1、通过一些类方法初始化的方法(YYTextContainer、CGSize 和 text)

               2、layout 之后的 attributes,都是只读的

               3、从 layout 中读取信息(位置、range 等等)

               4、绘制 text layout

               这个类主要是使用上面讲过的所有的数据来绘制 text,这部分的代码还是需要一点一点的去读的,如果是新手每一行都会有收获(比如说我),如果是老司机就没有必要一行行的读了,了解他的解题思路和解决这个问题的办法就可以。下面说一下生成 layout 的那个 500 行代码的情况,就按照代码的顺序从上往下大概的说明一下干了什么。

                         1)、初始化一系列使用到的变量

                         2)、安全判断,text 和 container

                         3)、判断是否需要修复 emoji 的 bug(iOS8.3 中)

                         4)、判断是否设置了 path 属性和 exclusionPaths 数组,做相应的计算拿到 cgpath,如果 cgpath 为空则 goto fail 返回 nil(如果设置了 path,size 和 insets 就没有用了)

                         5)、判断是不是奇偶填充,判断 pathWidth 是否为 0,判断是否是垂直展示

                         6)、使用 text 创建 CTFramesetterRef,创建失败 goto fail

                         7)、使用上一步中创建的 frameSetter 创建 CTFrameRef

                         8)、从 CTFrameRef 的对象中获得每一行、ctRun 数组,计算每一行的 frame,判断是否实现了 linePositionModifier 这个协议,有的话调用协议方法。

                         9)、计算 bounding size

                         10)、判断是否需要 truncation,和按那种方式处理

                         11)、判断是否垂直布局 text,需要的话,旋转布局

                         12)、判断得到的 visibleRange 长度,有效的话遍历 text 中的 attributes,配置对应的 layout 属性

                         13)、配置 layout 中的 attachments

                         14)、配置结束,返回 layout

                    绘制时就是根据 layout 中的配置情况绘制相应的特征,这段代码在此就不做分析了。

          6、YYAsyncLayer 文件

               YYAsyncLayerDispalyTask 是在 YYAsyncLayer 去 background queue 渲染是调用的对象,它有三个回调,一个 willDisplay 在渲染之前、一个 didDisplay 在渲染之后和渲染时被调用的 display。

               YYAsyncLayer 是 CAlayer 的子类,当这个 layer 更新 contents 时就会调用 delegate 方法去调用 async display task 去 background queue 渲染。这个 delegate 方法是 YYAsyncLayerDelegate 的方法。

               YYAsyncLayer 在刷新时调用 _displayAsync: 方法,然后调用遵守 YYAsyncLayerDelegate 的对象实现的 newAsyncDisplayTask 方法,获取到需要绘制的前后和绘制时的 task,根据是够需要异步来判断直接在主线程执行绘制代码还是异步执行绘制代码。

               在异步绘制过程中用到了一个异步队列,获取方法是 YYAsyncLayerGetDisplayQueue,在这个方法中有一个关于 QOS 的概念,NSQualityOfService(QOS)ios8 之后提供的新功能,这个枚举值是要告诉操作系统我们在进行什么样的工作,让系统能通过合理的资源控制来最高效的执行任务代码,主要涉及 CPU 调度、IO 优先级、任务运行在哪个线程以及运行的顺序等等。

      枚举值的含义如下:

            NSQualityOfServiceUserInteractive

            与用户交互的任务,这些任务通常跟 UI 级别的刷新相关,比如动画,这些任务需要在一瞬间完成

            NSQualityOfServiceUserInitiated

            由用户发起的并且需要立即得到结果的任务,比如滑动 scroll view 时去加载数据用于后续 cell 的显示,这些任务通常跟后续的用户交互相关,在几秒或者更短的时间内完成

            NSQualityOfServiceUtility

            一些可能需要花点时间的任务,这些任务不需要马上返回结果,比如下载的任务,这些任务可能花费几秒或者几分钟的时间

            NSQualityOfServiceBackground
            这些任务对用户不可见,比如后台进行备份的操作,这些任务可能需要较长的时间,几分钟甚至几个小时

            NSQualityOfServiceDefault

_            优先级介于 user-initiated 和 utility,当没有 QoS 信息时默认使用,开发者不应该使用这个值来设置自己的任务 _

     

 Qos 和 GCD queue 的对照图:

 

     看详细的分析请到这里:http://www.jianshu.com/p/f9e01c69a46f

     收获到的小知识点:

     1、iOS7 and later,UIFont 和 CTFontRef 是 toll-free bridged 的,在 iOS6,UIFont 是对 CTFontRref 的封装,所以在 CoreText 中是可以使用 UIFont 的,但是在 UILabel 和 UITextView 中不能使用 CTFontRef。

     2、NSParagraphStype 不是 toll-free bridged 到 CTParagraphStypeRef,CoreText 可以同时使用两者,但 UILabel 和 UITextView 只能使用 NSParagraphStyle。

     3、查看.a 静态文件支持哪种 iOS 处理器