唐巧原博客地址:
基于 CoreText 的排版引擎
CoreText是相對來說非常底層的框架蘸际,在日常的iOS開發(fā)過程中遇到諸如大量文本排版绳匀、圖文混合排版或者文本鏈接點擊等情況询枚,選擇用CoreText去做框架底層還是相當優(yōu)選的台舱。
這些內(nèi)容在唐巧的博客中都詳細的給出了知市,有興趣的朋友可以去唐巧的博客里好好學習一下。我這里要寫的是活孩,在學習唐巧關于CoreText的文章時遇到的幾個問題物遇,結(jié)合原作者的文章,做個自我學習總結(jié)憾儒。
唐巧關于CoreText的介紹是循序漸進的询兴,先介紹的是純文本的排版,我也從這開始起趾,從不一樣的角度去看 CoreText 純文本排版诗舰。
CoreText 純文本排版
坐標系
在使用CoreText時需要注意坐標系的不同,在CoreText下坐標系的原點為視圖的左下角训裆,x軸向右為正方向眶根,y軸向上為正方向蜀铲。而我們平時的UIKit坐標系原點則是視圖的左上角,x軸向右為正方向属百,y軸向下為正方向记劝。如圖所示:
CoreText使用的整體流程
首先,使用CoreText繪制純文本是在UIView中渔呵,整個調(diào)用流程的入口是UIView的 drawRect
方法怒竿,每次創(chuàng)建一個新的UIView系統(tǒng)都會給你預先寫好的那部分代碼
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
接下來就是在 drawRect
方法中實現(xiàn)繪制的代碼了,總體流程結(jié)構如圖:
圖里面總結(jié)了基于 CoreText 的排版引擎原文中的架構扩氢,下面描述一下具體思路:
CoreText排版的入口是
drawRect
方法耕驰,所有繪制的代碼都要從這里開始-
首先第一步要在
drawRect
方法中獲取繪制上下文CGContextRef context = UIGraphicsGetCurrentContext();
-
第二步要反轉(zhuǎn)坐標系
CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0);
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
是初始化文本矩陣 Text Matrix,在繪制之前一定記得初始化文本矩陣 Text Matrix录豺,否則朦肘,結(jié)果將是不可預測的,就像使用非初始化內(nèi)存一樣CGContextTranslateCTM(context, 0, self.bounds.size.height);
向上平移一個View高度CGContextScaleCTM(context, 1.0, -1.0);
將CoreText坐標系的 y軸 反轉(zhuǎn)
第三步要在繪制之前要計算出繪制區(qū)域的總高度巩检,計算高度可以在下一步創(chuàng)建
CTFrame
時根據(jù)其參數(shù)CTFramesetter
獲得最后第四步要調(diào)用
CTFrameDraw()
函數(shù)進行繪制厚骗,完整的函數(shù)描述為CTFrameDraw(CTFrameRef _Nonnull frame, CGContextRef _Nonnull context)
示启,共需要兩個參數(shù):CTFrame
和CGContext
兢哭。CGContext
是前面第一步獲取過的參數(shù),下一步重點要說的就是最重要的參數(shù)CTFrame
創(chuàng)建CTFrame
創(chuàng)建 CTFrame
需要兩個參數(shù):CTFramesetter
和 CGMutablePath
夫嗓。
創(chuàng)建 CTFramesetter
需要富文本字符串(NSAttributedString
)迟螺,這個富文本字符串可以根據(jù)我們的需求自行創(chuàng)建所需的文本(NSString
)和樣式(attributes字典)。
NSDictionary *attributes = @{屬性字典};
NSAttributedString *content = [[NSAttributedString alloc] initWithString:@"要顯示的文本" attributes:attributes];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
這樣 CTFramesetter
就創(chuàng)建好了舍咖,接下來要用 CTFramesetter
計算出整個繪制區(qū)域的高度:
// 獲得要繪制的區(qū)域的高度
CGSize restrictSize = CGSizeMake(自定義的寬度, CGFLOAT_MAX);
CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), nil, restrictSize, nil);
CGFloat textHeight = coreTextSize.height;
繪制區(qū)域的總高度就是 textHeight
接下來創(chuàng)建 CGMutablePath
矩父,創(chuàng)建 CGMutablePath
需要兩個參數(shù):自定的寬度和計算好的高度
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
后即可以進行 CoreText使用的整體流程 中的第四步:調(diào)用 CTFrameDraw()
函數(shù)進行繪制排霉。至此繪制純文本的架構思路全部介紹完窍株。
CTFrameDraw(ctFrame, context);
問題:反轉(zhuǎn)坐標系為什么要向上平移一個View高度?
我畫了幾張示意圖攻柠,來說說為什么要平移球订。
- 黃色坐標表示CoreText坐標系
- 紅色坐標表示UIKit坐標系
- 灰色區(qū)域是手機屏幕
- 藍色區(qū)域是自定義的View,就是我們用來繪制的View
- 文本
Hello World瑰钮!
所在的白色區(qū)域正是繪制區(qū)域
然后調(diào)用 CGContextSetTextMatrix(context, CGAffineTransformIdentity);
初始化文本矩陣浪谴,并且調(diào)用CGContextScaleCTM(context, 1.0, -1.0);
將CoreText坐標系的 y軸 反轉(zhuǎn)开睡,會得到下面的圖像
可以看到因苹,其實在反轉(zhuǎn)CoreText坐標系的 y軸 后,圖像剛剛好被弄到View外面了篇恒,也就是說黑色虛線位置就是View扶檐,藍色區(qū)域的實際圖像我們是看不到的,所以我們一定要把藍色區(qū)域向上平移一整個View的高度婚度,才會回到原位蘸秘,如下圖
牛刀小試
接下來根據(jù)上面的思路寫一個小 demo,算是練練手蝗茁。寫這個demo暫不考慮代碼結(jié)構的優(yōu)化醋虏,優(yōu)化的代碼結(jié)構在基于 CoreText 的排版引擎可以找到。完全是為了快速記憶剛剛提到的那些邏輯哮翘,用最簡單的方式全部回顧一遍颈嚼。
創(chuàng)建一個繼承自UIView的類,用于繪制饭寺,取名 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
實現(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);
// 平移一個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 需要兩個參數(shù):CTFramesetter 和 CGMutablePath。
先創(chuàng)建 CTFramesetter艰匙,利用 CTFramesetter 計算出繪制區(qū)域高度后再創(chuàng)建 CGMutablePath限煞。
創(chuàng)建 CTFramesetter 需要先創(chuàng)建 NSAttributedString。
*/
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
NSMutableAttributedString *aStr1 = [[NSMutableAttributedString alloc] initWithString:@"創(chuàng)建 CTFrame 需要兩個參數(shù):CTFramesetter 和 CGMutablePath员凝。\n" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor redColor]}];
NSMutableAttributedString *aStr2 = [[NSMutableAttributedString alloc] initWithString:@"先創(chuàng)建 CTFramesetter署驻,利用 CTFramesetter 計算出繪制區(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中拖入一個UIView健霹,讓它繼承自 GCDisplayView
把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
運行結(jié)果如下圖
總結(jié)
如果沒有看過唐巧的原文而是先看我這篇文章,那你肯定會迷糊糖埋,因為我去掉了很多的細節(jié)宣吱,寫的都是自己學習過后的心得,這些細節(jié)我要是搬過來就有點不搖碧蓮了瞳别,如果想要了解還是請移步 ==> 基于 CoreText 的排版引擎
唐巧的原文中將純文本繪制一直寫到支持富文本征候,而且做了很優(yōu)雅的架構設計,將數(shù)據(jù)源就是源字符串和字體相關設置都做成JSON格式的文件祟敛,方便批量操作疤坝。
在 基于 CoreText 的排版引擎中寫了幾個輔助類,主要就是把我寫的 demo 中的 - (void)createCTFrame
方法提出去分別實現(xiàn)垒棋。其實CoreText繪制只需要有一個CTFrame就足夠了卒煞,這個CTFrame可以在本類中實現(xiàn)和保存,也可以像唐巧一樣提煉出去叼架,做更好的架構畔裕。CTFrame誰都不依賴(比如:drawRect
方法或者 context
繪制上下文)斩熊,而我們需要設置的所有文本的屬性又都會包含在CTFrame中聂受,所以CTFrame完全可以拿出去斥难,會顯得更加靈活贞让。
另外就是要說一下 drawRect
方法,當時在看 基于 CoreText 的排版引擎的時候就有疑問甜无,那就是代碼的執(zhí)行順序扛点。由于沒怎么用過 drawRect
所以去查了一下。它的調(diào)用時機很晚岂丘,對于本類而言 drawRect
的調(diào)用在初始化完成以后陵究,對于使用這個View的controller而言 drawRect
在viewDidLoad之后,快要顯示的時候才會調(diào)用奥帘。所以你大可以放心把 ctFrame 拿出去做各種設置铜邮, drawRect
方法不太可能會比你的方法先執(zhí)行。如果有對 drawRect
執(zhí)行順序感興趣的朋友寨蹋,可以到網(wǎng)上搜一搜松蒜,一大把有關的文章。
后面還會繼續(xù)介紹CoreText圖文混排已旧。