唐巧原博客地址:
基于 CoreText 的排版引擎

CoreText是相對(duì)來(lái)說(shuō)非常底層的框架,在日常的iOS開(kāi)發(fā)過(guò)程中遇到諸如大量文本排版、圖文混合排版或者文本鏈接點(diǎn)擊等情況,選擇用CoreText去做框架底層還是相當(dāng)優(yōu)選的。
這些內(nèi)容在唐巧的博客中都詳細(xì)的給出了,有興趣的朋友可以去唐巧的博客里好好學(xué)習(xí)一下。我這里要寫(xiě)的是,在學(xué)習(xí)唐巧關(guān)于CoreText的文章時(shí)遇到的幾個(gè)問(wèn)題,結(jié)合原作者的文章,做個(gè)自我學(xué)習(xí)總結(jié)。
唐巧關(guān)于CoreText的介紹是循序漸進(jìn)的,先介紹的是純文本的排版,我也從這開(kāi)始,從不一樣的角度去看 CoreText 純文本排版。
CoreText 純文本排版
坐標(biāo)系
在使用CoreText時(shí)需要注意坐標(biāo)系的不同,在CoreText下坐標(biāo)系的原點(diǎn)為視圖的左下角,x軸向右為正方向,y軸向上為正方向。而我們平時(shí)的UIKit坐標(biāo)系原點(diǎn)則是視圖的左上角,x軸向右為正方向,y軸向下為正方向。如圖所示:


CoreText使用的整體流程
首先,使用CoreText繪制純文本是在UIView中,整個(gè)調(diào)用流程的入口是UIView的 drawRect 方法,每次創(chuàng)建一個(gè)新的UIView系統(tǒng)都會(huì)給你預(yù)先寫(xiě)好的那部分代碼
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
接下來(lái)就是在 drawRect 方法中實(shí)現(xiàn)繪制的代碼了,總體流程結(jié)構(gòu)如圖:

圖里面總結(jié)了基于 CoreText 的排版引擎原文中的架構(gòu),下面描述一下具體思路:
CoreText排版的入口是
drawRect方法,所有繪制的代碼都要從這里開(kāi)始-
首先第一步要在
drawRect方法中獲取繪制上下文CGContextRef context = UIGraphicsGetCurrentContext(); -
第二步要反轉(zhuǎn)坐標(biāo)系
CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0);CGContextSetTextMatrix(context, CGAffineTransformIdentity);是初始化文本矩陣 Text Matrix,在繪制之前一定記得初始化文本矩陣 Text Matrix,否則,結(jié)果將是不可預(yù)測(cè)的,就像使用非初始化內(nèi)存一樣CGContextTranslateCTM(context, 0, self.bounds.size.height);向上平移一個(gè)View高度CGContextScaleCTM(context, 1.0, -1.0);將CoreText坐標(biāo)系的 y軸 反轉(zhuǎn)
第三步要在繪制之前要計(jì)算出繪制區(qū)域的總高度,計(jì)算高度可以在下一步創(chuàng)建
CTFrame時(shí)根據(jù)其參數(shù)CTFramesetter獲得最后第四步要調(diào)用
CTFrameDraw()函數(shù)進(jìn)行繪制,完整的函數(shù)描述為CTFrameDraw(CTFrameRef _Nonnull frame, CGContextRef _Nonnull context),共需要兩個(gè)參數(shù):CTFrame和CGContext。CGContext是前面第一步獲取過(guò)的參數(shù),下一步重點(diǎn)要說(shuō)的就是最重要的參數(shù)CTFrame
創(chuàng)建CTFrame
創(chuàng)建 CTFrame 需要兩個(gè)參數(shù):CTFramesetter 和 CGMutablePath。
創(chuàng)建 CTFramesetter 需要富文本字符串(NSAttributedString),這個(gè)富文本字符串可以根據(jù)我們的需求自行創(chuàng)建所需的文本(NSString)和樣式(attributes字典)。
NSDictionary *attributes = @{屬性字典};
NSAttributedString *content = [[NSAttributedString alloc] initWithString:@"要顯示的文本" attributes:attributes];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
這樣 CTFramesetter 就創(chuàng)建好了,接下來(lái)要用 CTFramesetter 計(jì)算出整個(gè)繪制區(qū)域的高度:
// 獲得要繪制的區(qū)域的高度
CGSize restrictSize = CGSizeMake(自定義的寬度, CGFLOAT_MAX);
CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), nil, restrictSize, nil);
CGFloat textHeight = coreTextSize.height;
繪制區(qū)域的總高度就是 textHeight
接下來(lái)創(chuàng)建 CGMutablePath,創(chuàng)建 CGMutablePath需要兩個(gè)參數(shù):自定的寬度和計(jì)算好的高度
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, 自定寬度, textHeight));
CGMutablePath也有了,現(xiàn)在可以回頭創(chuàng)建 CTFrame 了
CTFrameRef ctFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
有了 CTFrame 后即可以進(jìn)行 CoreText使用的整體流程 中的第四步:調(diào)用 CTFrameDraw() 函數(shù)進(jìn)行繪制。至此繪制純文本的架構(gòu)思路全部介紹完。
CTFrameDraw(ctFrame, context);
問(wèn)題:反轉(zhuǎn)坐標(biāo)系為什么要向上平移一個(gè)View高度?
我畫(huà)了幾張示意圖,來(lái)說(shuō)說(shuō)為什么要平移。
- 黃色坐標(biāo)表示CoreText坐標(biāo)系
- 紅色坐標(biāo)表示UIKit坐標(biāo)系
- 灰色區(qū)域是手機(jī)屏幕
- 藍(lán)色區(qū)域是自定義的View,就是我們用來(lái)繪制的View
- 文本
Hello World!所在的白色區(qū)域正是繪制區(qū)域

然后調(diào)用 CGContextSetTextMatrix(context, CGAffineTransformIdentity);初始化文本矩陣,并且調(diào)用CGContextScaleCTM(context, 1.0, -1.0);將CoreText坐標(biāo)系的 y軸 反轉(zhuǎn),會(huì)得到下面的圖像

可以看到,其實(shí)在反轉(zhuǎn)CoreText坐標(biāo)系的 y軸 后,圖像剛剛好被弄到View外面了,也就是說(shuō)黑色虛線位置就是View,藍(lán)色區(qū)域的實(shí)際圖像我們是看不到的,所以我們一定要把藍(lán)色區(qū)域向上平移一整個(gè)View的高度,才會(huì)回到原位,如下圖

牛刀小試
接下來(lái)根據(jù)上面的思路寫(xiě)一個(gè)小 demo,算是練練手。寫(xiě)這個(gè)demo暫不考慮代碼結(jié)構(gòu)的優(yōu)化,優(yōu)化的代碼結(jié)構(gòu)在基于 CoreText 的排版引擎可以找到。完全是為了快速記憶剛剛提到的那些邏輯,用最簡(jiǎn)單的方式全部回顧一遍。
創(chuàng)建一個(gè)繼承自UIView的類(lèi),用于繪制,取名 GCDisplayView,源代碼如下:
頭文件
//
// GCDisplayView.h
//
// Created by 崇 on 2018.
// Copyright ? 2018 崇. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface GCDisplayView : UIView
@property (nonatomic, assign) CGFloat textHeight;
@end
實(shí)現(xiàn)文件
//
// GCDisplayView.m
//
// Created by 崇 on 2018.
// Copyright ? 2018 崇. All rights reserved.
//
#import "GCDisplayView.h"
#import <CoreText/CoreText.h>
@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) {
// 創(chuàng)建 CTFrame
[self createCTFrame];
}
return self;
}
- (void)drawRect:(CGRect)rect {
// 獲取繪制上下文
CGContextRef context = UIGraphicsGetCurrentContext();
// 初始化文本矩陣
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
// 平移一個(gè)View高度
CGContextTranslateCTM(context, 0, self.bounds.size.height);
// 反轉(zhuǎn) y 軸
CGContextScaleCTM(context, 1.0, -1.0);
// 繪制
CTFrameDraw(self.ctFrame, context);
// 釋放
CFRelease(self.ctFrame);
CFRelease(self.framesetter);
}
- (void)createCTFrame {
/*
創(chuàng)建 CTFrame 需要兩個(gè)參數(shù):CTFramesetter 和 CGMutablePath。
先創(chuàng)建 CTFramesetter,利用 CTFramesetter 計(jì)算出繪制區(qū)域高度后再創(chuàng)建 CGMutablePath。
創(chuàng)建 CTFramesetter 需要先創(chuàng)建 NSAttributedString。
*/
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
NSMutableAttributedString *aStr1 = [[NSMutableAttributedString alloc] initWithString:@"創(chuàng)建 CTFrame 需要兩個(gè)參數(shù):CTFramesetter 和 CGMutablePath。\n" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor redColor]}];
NSMutableAttributedString *aStr2 = [[NSMutableAttributedString alloc] initWithString:@"先創(chuàng)建 CTFramesetter,利用 CTFramesetter 計(jì)算出繪制區(qū)域高度后再創(chuàng)建 CGMutablePath。\n" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor blueColor]}];
NSMutableAttributedString *aStr3 = [[NSMutableAttributedString alloc] initWithString:@"創(chuàng)建 CTFramesetter 需要先創(chuàng)建NSAttributedString." attributes:@{(id)kCTForegroundColorAttributeName : [UIColor blackColor]}];
[attributedString appendAttributedString:aStr1];
[attributedString appendAttributedString:aStr2];
[attributedString appendAttributedString:aStr3];
// 用創(chuàng)建好的 attString 創(chuàng)建 framesetter
self.framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
// 獲得要繪制的區(qū)域的高度
CGSize restrictSize = CGSizeMake(self.bounds.size.width, CGFLOAT_MAX);
CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(self.framesetter, CFRangeMake(0,0), nil, restrictSize, nil);
self.textHeight = coreTextSize.height;
// 創(chuàng)建 CGMutablePath
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, self.bounds.size.width, self.textHeight));
// 創(chuàng)建 ctFrame
self.ctFrame = CTFramesetterCreateFrame(self.framesetter, CFRangeMake(0, 0), path, NULL);
CFRelease(path);
}
@end
在ViewController的StoryBoard中拖入一個(gè)UIView,讓它繼承自 GCDisplayView

把StoryBoard中的這個(gè)View拖入到ViewController中作為屬性,設(shè)置它的高度
//
// 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];
// 設(shè)置高度
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
運(yùn)行結(jié)果如下圖

總結(jié)
如果沒(méi)有看過(guò)唐巧的原文而是先看我這篇文章,那你肯定會(huì)迷糊,因?yàn)槲胰サ袅撕芏嗟募?xì)節(jié),寫(xiě)的都是自己學(xué)習(xí)過(guò)后的心得,這些細(xì)節(jié)我要是搬過(guò)來(lái)就有點(diǎn)不搖碧蓮了,如果想要了解還是請(qǐng)移步 ==> 基于 CoreText 的排版引擎
唐巧的原文中將純文本繪制一直寫(xiě)到支持富文本,而且做了很優(yōu)雅的架構(gòu)設(shè)計(jì),將數(shù)據(jù)源就是源字符串和字體相關(guān)設(shè)置都做成JSON格式的文件,方便批量操作。
在 基于 CoreText 的排版引擎中寫(xiě)了幾個(gè)輔助類(lèi),主要就是把我寫(xiě)的 demo 中的 - (void)createCTFrame 方法提出去分別實(shí)現(xiàn)。其實(shí)CoreText繪制只需要有一個(gè)CTFrame就足夠了,這個(gè)CTFrame可以在本類(lèi)中實(shí)現(xiàn)和保存,也可以像唐巧一樣提煉出去,做更好的架構(gòu)。CTFrame誰(shuí)都不依賴(比如:drawRect 方法或者 context繪制上下文),而我們需要設(shè)置的所有文本的屬性又都會(huì)包含在CTFrame中,所以CTFrame完全可以拿出去,會(huì)顯得更加靈活。
另外就是要說(shuō)一下 drawRect 方法,當(dāng)時(shí)在看 基于 CoreText 的排版引擎的時(shí)候就有疑問(wèn),那就是代碼的執(zhí)行順序。由于沒(méi)怎么用過(guò) drawRect 所以去查了一下。它的調(diào)用時(shí)機(jī)很晚,對(duì)于本類(lèi)而言 drawRect 的調(diào)用在初始化完成以后,對(duì)于使用這個(gè)View的controller而言 drawRect 在viewDidLoad之后,快要顯示的時(shí)候才會(huì)調(diào)用。所以你大可以放心把 ctFrame 拿出去做各種設(shè)置, drawRect 方法不太可能會(huì)比你的方法先執(zhí)行。如果有對(duì) drawRect 執(zhí)行順序感興趣的朋友,可以到網(wǎng)上搜一搜,一大把有關(guān)的文章。
后面還會(huì)繼續(xù)介紹CoreText圖文混排。