CoreText|CoreText 学习笔记(上)

唐巧原博客地址:
基于 CoreText 的排版引擎
CoreText|CoreText 学习笔记(上)
文章图片
CoreText是相对来说非常底层的框架,在日常的iOS开发过程中遇到诸如大量文本排版、图文混合排版或者文本链接点击等情况,选择用CoreText去做框架底层还是相当优选的。
这些内容在唐巧的博客中都详细的给出了,有兴趣的朋友可以去唐巧的博客里好好学习一下。我这里要写的是,在学习唐巧关于CoreText的文章时遇到的几个问题,结合原作者的文章,做个自我学习总结。
唐巧关于CoreText的介绍是循序渐进的,先介绍的是纯文本的排版,我也从这开始,从不一样的角度去看 CoreText 纯文本排版。
CoreText 纯文本排版 坐标系 在使用CoreText时需要注意坐标系的不同,在CoreText下坐标系的原点为视图的左下角,x轴向右为正方向,y轴向上为正方向。而我们平时的UIKit坐标系原点则是视图的左上角,x轴向右为正方向,y轴向下为正方向。如图所示:

CoreText|CoreText 学习笔记(上)
文章图片


所以在确定绘制位置时,要注意坐标系的转换,比如下面这个黑色圆点的位置,在两个坐标系中是不一样的 CoreText|CoreText 学习笔记(上)
文章图片
CoreText使用的整体流程 首先,使用CoreText绘制纯文本是在UIView中,整个调用流程的入口是UIView的 drawRect 方法,每次创建一个新的UIView系统都会给你预先写好的那部分代码
/* // Only override drawRect: if you perform custom drawing. // An empty implementation adversely affects performance during animation. - (void)drawRect:(CGRect)rect { // Drawing code } */

接下来就是在 drawRect 方法中实现绘制的代码了,总体流程结构如图:

CoreText|CoreText 学习笔记(上)
文章图片

图里面总结了基于 CoreText 的排版引擎原文中的架构,下面描述一下具体思路:
  • CoreText排版的入口是 drawRect 方法,所有绘制的代码都要从这里开始
  • 首先第一步要在 drawRect 方法中获取绘制上下文
    CGContextRef context = UIGraphicsGetCurrentContext();

  • 第二步要反转坐标系
    CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0);

    • CGContextSetTextMatrix(context, CGAffineTransformIdentity); 是初始化文本矩阵 Text Matrix,在绘制之前一定记得初始化文本矩阵 Text Matrix,否则,结果将是不可预测的,就像使用非初始化内存一样
    • CGContextTranslateCTM(context, 0, self.bounds.size.height); 向上平移一个View高度
    • CGContextScaleCTM(context, 1.0, -1.0); 将CoreText坐标系的 y轴 反转
  • 第三步要在绘制之前要计算出绘制区域的总高度,计算高度可以在下一步创建CTFrame时根据其参数 CTFramesetter 获得
  • 最后第四步要调用 CTFrameDraw() 函数进行绘制,完整的函数描述为 CTFrameDraw(CTFrameRef _Nonnull frame, CGContextRef _Nonnull context),共需要两个参数:CTFrameCGContextCGContext 是前面第一步获取过的参数,下一步重点要说的就是最重要的参数 CTFrame


创建CTFrame 创建 CTFrame 需要两个参数:CTFramesetterCGMutablePath
创建 CTFramesetter 需要富文本字符串(NSAttributedString),这个富文本字符串可以根据我们的需求自行创建所需的文本(NSString)和样式(attributes字典)。
NSDictionary *attributes = @{属性字典}; NSAttributedString *content = [[NSAttributedString alloc] initWithString:@"要显示的文本" attributes:attributes]; CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);

这样 CTFramesetter 就创建好了,接下来要用 CTFramesetter 计算出整个绘制区域的高度:
// 获得要绘制的区域的高度 CGSize restrictSize = CGSizeMake(自定义的宽度, CGFLOAT_MAX); CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), nil, restrictSize, nil); CGFloat textHeight = coreTextSize.height;

绘制区域的总高度就是 textHeight
接下来创建 CGMutablePath,创建 CGMutablePath需要两个参数:自定的宽度和计算好的高度
CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, CGRectMake(0, 0, 自定宽度, textHeight));

CGMutablePath也有了,现在可以回头创建 CTFrame
CTFrameRef ctFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);



有了 CTFrame 后即可以进行 CoreText使用的整体流程 中的第四步:调用 CTFrameDraw() 函数进行绘制。至此绘制纯文本的架构思路全部介绍完。
CTFrameDraw(ctFrame, context);



问题:反转坐标系为什么要向上平移一个View高度? 我画了几张示意图,来说说为什么要平移。
  • 黄色坐标表示CoreText坐标系
  • 红色坐标表示UIKit坐标系
  • 灰色区域是手机屏幕
  • 蓝色区域是自定义的View,就是我们用来绘制的View
  • 文本 Hello World! 所在的白色区域正是绘制区域


首先不反转坐标系的时候,绘制出来的图像是倒转的。 CoreText|CoreText 学习笔记(上)
文章图片
然后调用 CGContextSetTextMatrix(context, CGAffineTransformIdentity); 初始化文本矩阵,并且调用CGContextScaleCTM(context, 1.0, -1.0); 将CoreText坐标系的 y轴 反转,会得到下面的图像
CoreText|CoreText 学习笔记(上)
文章图片
【CoreText|CoreText 学习笔记(上)】
可以看到,其实在反转CoreText坐标系的 y轴 后,图像刚刚好被弄到View外面了,也就是说黑色虚线位置就是View,蓝色区域的实际图像我们是看不到的,所以我们一定要把蓝色区域向上平移一整个View的高度,才会回到原位,如下图
CoreText|CoreText 学习笔记(上)
文章图片



牛刀小试
接下来根据上面的思路写一个小 demo,算是练练手。写这个demo暂不考虑代码结构的优化,优化的代码结构在基于 CoreText 的排版引擎可以找到。完全是为了快速记忆刚刚提到的那些逻辑,用最简单的方式全部回顾一遍。
创建一个继承自UIView的类,用于绘制,取名 GCDisplayView,源代码如下:
头文件
// //GCDisplayView.h // //Created by 崇 on 2018. //Copyright ? 2018 崇. All rights reserved. //#import @interface GCDisplayView : UIView@property (nonatomic, assign) CGFloat textHeight; @end

实现文件
// //GCDisplayView.m // //Created by 崇 on 2018. //Copyright ? 2018 崇. All rights reserved. //#import "GCDisplayView.h" #import @interface GCDisplayView()@property (nonatomic, assign) CTFramesetterRef framesetter; @property (nonatomic, assign) CTFrameRef ctFrame; @end@implementation GCDisplayView- (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { // 创建 CTFrame [self createCTFrame]; } return self; }- (void)drawRect:(CGRect)rect {// 获取绘制上下文 CGContextRef context = UIGraphicsGetCurrentContext(); // 初始化文本矩阵 CGContextSetTextMatrix(context, CGAffineTransformIdentity); // 平移一个View高度 CGContextTranslateCTM(context, 0, self.bounds.size.height); // 反转 y 轴 CGContextScaleCTM(context, 1.0, -1.0); // 绘制 CTFrameDraw(self.ctFrame, context); // 释放 CFRelease(self.ctFrame); CFRelease(self.framesetter); }- (void)createCTFrame {/* 创建 CTFrame 需要两个参数:CTFramesetter 和 CGMutablePath。先创建 CTFramesetter,利用 CTFramesetter 计算出绘制区域高度后再创建 CGMutablePath。创建 CTFramesetter 需要先创建 NSAttributedString。 */NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init]; NSMutableAttributedString *aStr1 = [[NSMutableAttributedString alloc] initWithString:@"创建 CTFrame 需要两个参数:CTFramesetter 和 CGMutablePath。\n" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor redColor]}]; NSMutableAttributedString *aStr2 = [[NSMutableAttributedString alloc] initWithString:@"先创建 CTFramesetter,利用 CTFramesetter 计算出绘制区域高度后再创建 CGMutablePath。\n" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor blueColor]}]; NSMutableAttributedString *aStr3 = [[NSMutableAttributedString alloc] initWithString:@"创建 CTFramesetter 需要先创建NSAttributedString." attributes:@{(id)kCTForegroundColorAttributeName : [UIColor blackColor]}]; [attributedString appendAttributedString:aStr1]; [attributedString appendAttributedString:aStr2]; [attributedString appendAttributedString:aStr3]; // 用创建好的 attString 创建 framesetter self.framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString); // 获得要绘制的区域的高度 CGSize restrictSize = CGSizeMake(self.bounds.size.width, CGFLOAT_MAX); CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(self.framesetter, CFRangeMake(0,0), nil, restrictSize, nil); self.textHeight = coreTextSize.height; // 创建 CGMutablePath CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, CGRectMake(0, 0, self.bounds.size.width, self.textHeight)); // 创建 ctFrame self.ctFrame = CTFramesetterCreateFrame(self.framesetter, CFRangeMake(0, 0), path, NULL); CFRelease(path); }@end

在ViewController的StoryBoard中拖入一个UIView,让它继承自 GCDisplayView
CoreText|CoreText 学习笔记(上)
文章图片

把StoryBoard中的这个View拖入到ViewController中作为属性,设置它的高度
// //ViewController.m //CoreTextPureText // //Created by 崇 on 2018/11/8. //Copyright ? 2018 崇. All rights reserved. //#import "ViewController.h" #import "GCDisplayView.h"@interface ViewController () @property (weak, nonatomic) IBOutlet GCDisplayView *disView; @end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; // 设置高度 CGRect frame = CGRectMake(self.disView.frame.origin.x, self.disView.frame.origin.y, self.disView.frame.size.width, self.disView.textHeight); self.disView.frame = frame; }- (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. }@end

运行结果如下图

CoreText|CoreText 学习笔记(上)
文章图片





总结 如果没有看过唐巧的原文而是先看我这篇文章,那你肯定会迷糊,因为我去掉了很多的细节,写的都是自己学习过后的心得,这些细节我要是搬过来就有点不摇碧莲了,如果想要了解还是请移步 ==> 基于 CoreText 的排版引擎
唐巧的原文中将纯文本绘制一直写到支持富文本,而且做了很优雅的架构设计,将数据源就是源字符串和字体相关设置都做成JSON格式的文件,方便批量操作。
在 基于 CoreText 的排版引擎中写了几个辅助类,主要就是把我写的 demo 中的 - (void)createCTFrame 方法提出去分别实现。其实CoreText绘制只需要有一个CTFrame就足够了,这个CTFrame可以在本类中实现和保存,也可以像唐巧一样提炼出去,做更好的架构。CTFrame谁都不依赖(比如:drawRect 方法或者 context绘制上下文),而我们需要设置的所有文本的属性又都会包含在CTFrame中,所以CTFrame完全可以拿出去,会显得更加灵活。
另外就是要说一下 drawRect 方法,当时在看 基于 CoreText 的排版引擎的时候就有疑问,那就是代码的执行顺序。由于没怎么用过 drawRect 所以去查了一下。它的调用时机很晚,对于本类而言 drawRect 的调用在初始化完成以后,对于使用这个View的controller而言 drawRect 在viewDidLoad之后,快要显示的时候才会调用。所以你大可以放心把 ctFrame 拿出去做各种设置, drawRect 方法不太可能会比你的方法先执行。如果有对 drawRect 执行顺序感兴趣的朋友,可以到网上搜一搜,一大把有关的文章。
后面还会继续介绍CoreText图文混排。
未 完 待 续 Coming Soon ~~~

    推荐阅读