1. CoreText框架基礎(chǔ)
CoreText是Mac OS和iOS系統(tǒng)中處理文本的low-level API, 不管是使用OC還是swift, 實(shí)際我們使用CoreText都還是間接或直接使用C語言在寫代碼。CoreText是iOS和Mac OS中文本處理的根基, TextKit和WebKit都是構(gòu)建于其上。
常用類、屬性
CTFrameRef
CTFramesetterRef
CTLineRef
CTRunRef
CTTypesetterRef
CTGlyphInfoRef (NSGlyphInfo)
CTParagraphStyleRef (NSParagraphStyle)
CTFontRef (UIFont)
CFArrayRef (NSArray)
分析:
coreText 屬于怎樣一套API?
字體結(jié)構(gòu):
當(dāng)我們進(jìn)行字體繪制的時(shí)候很重要。

CTRun、CTFrame、CTLine

- CTFrame可以想象成畫布, 畫布的大小范圍由CGPath決定
- CTFrame由很多CTLine組成, CTLine表示為一行
- CTLine由多個(gè)CTRun組成, CTRun相當(dāng)于一行中的多個(gè)塊, 但是CTRun不需要你自己創(chuàng)建, 由NSAttributedString的屬性決定, 系統(tǒng)自動(dòng)生成。每個(gè)CTRun對(duì)應(yīng)不同屬性。
- CTFramesetter是一個(gè)工廠, 創(chuàng)建CTFrame, 一個(gè)界面上可以有多個(gè)CTFrame
- CTFrame就是一個(gè)基本畫布,然后一行一行繪制。 CoreText會(huì)自動(dòng)根據(jù)傳入的NSAttributedString屬性創(chuàng)建CTRun,包括字體樣式,顏色,間距等
更多詳細(xì)的基礎(chǔ)知識(shí)見末尾參考。
流程
如下圖所示,這就是CoreText的基本處理流程:

1、創(chuàng)建AttributedString,定義樣式
2、通過 CFAttributedStringRef 生成 CTFramesetter
3、通過CTFramesetter得到CTFrame
4、繪制 (CTFrameDraw)
5、如果有圖片存在,先在AttributedString 對(duì)應(yīng)位置添加占位符
6、通過回調(diào)函數(shù)確定圖片的寬高(CTRunDelegateCallbacks)
7、遍歷到對(duì)應(yīng)CTRun上、獲取對(duì)應(yīng)CGRect、繪制圖片(CGContextDrawImage)
2. 基本的文本樣式實(shí)操
CoreText是需要自己處理繪制,不像UILabel等最上層的控件 ,所以我們必須在drawRect中繪制,為了更好地使用,我們稍微封裝一下,自定義一個(gè)UIView。
我們?cè)谑褂蒙蠈拥目丶r(shí),坐標(biāo)系的原點(diǎn)在左上角,而底層的Core Graphics的坐標(biāo)系原點(diǎn)則是在左下角,以下是一個(gè)最基本的繪制示例:
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
//step 1:獲取當(dāng)前畫布的上下文
CGContextRef context = UIGraphicsGetCurrentContext();
//step 2:
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);
//step 3:
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"xXHhofiyYI這是一段中文,前面是大小寫"];
//step 4:
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attributedString length]), path, NULL);
//step 5:
CTFrameDraw(frame,context);
//step 6:
CFRelease(frame);
CFRelease(path);
CFRelease(framesetter);
//使用Create函數(shù)建立的對(duì)象引用,必須要使用CFRelease掉。
}
效果如下:

結(jié)果分析:發(fā)現(xiàn)文案是反的。原因就是因?yàn)閏oreText的坐標(biāo)系是和UIKit的坐標(biāo)系不一樣的:

如上圖,CoreText是基于CoreGraphics的,所以坐標(biāo)系原點(diǎn)是左下角,我們需要進(jìn)行翻轉(zhuǎn)。將Y軸從向上轉(zhuǎn)換為向下。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
翻轉(zhuǎn)后,下面來進(jìn)行一個(gè)最基本的富文本示例:
step 4 添加
[attr addAttribute:(NSString *)kCTBackgroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 10)];
[attr addAttribute:(NSString *)kCTFontAttributeName value:(__bridge id _Nonnull)(fontRef) range:NSMakeRange(0, 10)];
效果如下:

上面的繪制方式是基于CTFrame繪制,還可以按行和按run繪制:
按CTLine繪制
// 1.獲得CTLine數(shù)組
let lines = CTFrameGetLines(frame)
// 2.獲得行數(shù)
let numberOfLines = CFArrayGetCount(lines)
// 3.獲得每一行的origin, CoreText的origin是在字形的baseLine處的, 請(qǐng)參考字形圖
var lineOrigins = [CGPoint](count: numberOfLines, repeatedValue: CGPointZero)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins)
// 4.遍歷每一行進(jìn)行繪制
for index in 0..<numberOfLines {
let origin = lineOrigins[index]
// 參考: http://swifter.tips/unsafe/
let line = unsafeBitCast(CFArrayGetValueAtIndex(lines, index), CTLine.self)
// 設(shè)置每一行的位置
CGContextSetTextPosition(context, origin.x, origin.y)
// 開始一行的繪制
CTLineDraw(line, context)
}
按CTRun繪制
用下面函數(shù)替換CTLineDraw(line, context)這一句就可以了, 效果也如上面。
// 畫一行
func drawLine(line: CTLine, context: CGContext) {
let runs = CTLineGetGlyphRuns(line) as Array
runs.forEach { run in
CTRunDraw(run as! CTRun, context, CFRangeMake(0, 0))
}
}
}
3. 圖文混排
CoreText本身是不提供UIImage的繪制,所以UIImage肯定只能通過Core Graphics繪制,但是繪制時(shí)雙必須要知道此繪制單元的長(zhǎng)寬,慶幸的是CoreText繪制的最小單元CTRun提供了CTRunDelegate,也就是當(dāng)設(shè)置了kCTRunDelegateAttributeName過后,CTRun的繪制時(shí)所需的參考(長(zhǎng)寬等)將可從委托中獲取,我們即可通過此方法實(shí)現(xiàn)圖片的繪制。在需要繪制圖片的位置,提前預(yù)留空白占位。
CTRun有幾個(gè)委托用以實(shí)現(xiàn)CTRun的幾個(gè)參數(shù)的獲取。
以下是CTRunDelegateCallbacks的幾個(gè)委托代理 。
typedef struct
{
CFIndex version;
CTRunDelegateDeallocateCallback dealloc;
CTRunDelegateGetAscentCallback getAscent;
CTRunDelegateGetDescentCallback getDescent;
CTRunDelegateGetWidthCallback getWidth;
} CTRunDelegateCallbacks;
以下是一個(gè)最基本的圖片繪制原型:遍歷查詢圖片,即查找含有imgName attribute的CTRun,并繪制。
step 4 前面添加:
CTRunDelegateCallbacks imageCallBacks;
imageCallBacks.version = kCTRunDelegateCurrentVersion;
imageCallBacks.dealloc = ImgRunDelegateDeallocCallback;
imageCallBacks.getAscent = ImgRunDelegateGetAscentCallback;
imageCallBacks.getDescent = ImgRunDelegateGetDescentCallback;
imageCallBacks.getWidth = ImgRunDelegateGetWidthCallback;
NSString *imgName = @"test.jpg";
CTRunDelegateRef imgRunDelegate = CTRunDelegateCreate(&imageCallBacks, (__bridge void * _Nullable)(imgName));//我們也可以傳入其它參數(shù)
NSMutableAttributedString *imgAttributedStr = [[NSMutableAttributedString alloc]initWithString:@" "];
[imgAttributedStr addAttribute:(NSString *)kCTRunDelegateAttributeName value:(__bridge id)imgRunDelegate range:NSMakeRange(0, 1)];
CFRelease(imgRunDelegate);
#define kImgName @"imgName"
//圖片占位符添加
[imgAttributedStr addAttribute:kImgName value:imgName range:NSMakeRange(0, 1)];
[attributedString insertAttributedString:imgAttributedStr atIndex:30];
step 5 后面添加:
//繪制圖片
CFArrayRef lines = CTFrameGetLines(frame);
CGPoint lineOrigins[CFArrayGetCount(lines)];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);//獲取第行的起始點(diǎn)
for (int i = 0; i < CFArrayGetCount(lines); i++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CGFloat lineAscent;//上緣線
CGFloat lineDescent;//下緣線
CGFloat lineLeading;//行間距
CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading);//獲取此行的字形參數(shù)
//獲取此行中每個(gè)CTRun
CFArrayRef runs = CTLineGetGlyphRuns(line);
for(int j = 0;j< CFArrayGetCount(runs);j++){
CGFloat runAscent;//此CTRun上緣線
CGFloat runDescent;//此CTRun下緣線
CGPoint lineOrigin = lineOrigins[i];//此行起點(diǎn)
CTRunRef run = CFArrayGetValueAtIndex(runs, j);//獲取此CTRun
NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
CGRect runRect;
//獲取此CTRun的上緣線,下緣線,并由此獲取CTRun和寬度
runRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &runAscent, &runDescent, NULL);
//CTRun的X坐標(biāo)
CGFloat runOrgX = lineOrigin.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
runRect = CGRectMake(runOrgX,lineOrigin.y-runDescent,runRect.size.width,runAscent+runDescent );
NSString *imgName = [attributes objectForKey:kImgName];
if (imgName) {
UIImage *image = [UIImage imageNamed:imgName];
if(image){
CGRect imageRect ;
imageRect.size = image.size;
imageRect.origin.x = runRect.origin.x + lineOrigin.x;
imageRect.origin.y = lineOrigin.y;
CGContextDrawImage(context, imageRect, image.CGImage);
}
}
}
}
代理函數(shù):
#pragma mark - CTRunDelegateCallbacks
void ImgRunDelegateDeallocCallback( void* refCon ){
}
CGFloat ImgRunDelegateGetAscentCallback( void *refCon ){
NSString *imageName = (__bridge NSString *)refCon;
return [UIImage imageNamed:imageName].size.height;
}
CGFloat ImgRunDelegateGetDescentCallback(void *refCon){
return 0;
}
CGFloat ImgRunDelegateGetWidthCallback(void *refCon){
NSString *imageName = (__bridge NSString *)refCon;
return [UIImage imageNamed:imageName].size.width;
}
效果如下:

基于以上這個(gè)原型,我們可以封裝一個(gè)比較完整的富文本控件,比如定義HTML協(xié)議或者JSON,然后在內(nèi)部進(jìn)行解析,然后根據(jù)類型與相應(yīng)的屬性進(jìn)行繪制。
4. 圖片點(diǎn)擊事件
CoreText就是將內(nèi)容繪制到畫布上,自然沒有事件處理,我們要實(shí)現(xiàn)圖片與鏈接的點(diǎn)擊效果就需要使用觸摸事件了。當(dāng)點(diǎn)擊的位置在圖片的Rect中,那我們做相應(yīng)的操作即可,所以基本步驟如下:
記錄所有圖片所在畫布中作為一個(gè)CTRun的位置 -> 獲取每個(gè)圖片所在畫布中所占的Rect矩形區(qū)域 -> 當(dāng)點(diǎn)擊事件發(fā)生時(shí),判斷點(diǎn)擊的點(diǎn)是否在某個(gè)需要處理的圖片Rect內(nèi)。
這里為了演示的簡(jiǎn)單,我們直接在drawRect中記錄圖片的相應(yīng)坐標(biāo),但是一般我們會(huì)在CTRichView渲染之前對(duì)數(shù)據(jù)進(jìn)行相應(yīng)的處理,比如處理傳入的樣式數(shù)據(jù)、記錄圖片與鏈接等信息。
用于記錄圖片信息類
@interface CTImageData : NSObject
@property (nonatomic,strong) NSString *imgHolder;
@property (nonatomic,strong) NSURL *imgPath;
@property (nonatomic) NSInteger idx;
@property (nonatomic) CGRect imageRect;
@end
//記錄圖片信息
//以下操作僅僅是演示示例,實(shí)戰(zhàn)時(shí)請(qǐng)?jiān)阡秩局疤幚頂?shù)據(jù),做到最佳實(shí)踐。
if(!_imageDataArray){
_imageDataArray = [[NSMutableArray alloc]init];
}
BOOL imgExist = NO;
for (CTImageData *ctImageData in _imageDataArray) {
if (ctImageData.idx == idx) {
imgExist = YES;
break;
}
}
if(!imgExist){
CTImageData *ctImageData = [[CTImageData alloc]init];
ctImageData.imgHolder = imgName;
ctImageData.imageRect = imageRect;
ctImageData.idx = idx;
[_imageDataArray addObject:ctImageData];
}
- (void)setupEvents{
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(userTapGestureDetected:)];
[self addGestureRecognizer:tapRecognizer];
self.userInteractionEnabled = YES;
}
- (void)userTapGestureDetected:(UIGestureRecognizer *)recognizer{
CGPoint point = [recognizer locationInView:self];
//先判斷是否是點(diǎn)擊的圖片Rect
for(CTImageData *imageData in _imageDataArray){
CGRect imageRect = imageData.imageRect;
CGFloat imageOriginY = self.bounds.size.height - imageRect.origin.y - imageRect.size.height;
CGRect rect = CGRectMake(imageRect.origin.x,imageOriginY, imageRect.size.width, imageRect.size.height);
if(CGRectContainsPoint(rect, point)){
NSLog(@"tap image handle");
return;
}
}
//再判斷鏈接
}
5. 鏈接點(diǎn)擊事件
記錄鏈接信息類
@interface CTLinkData : NSObject
@property (nonatomic ,strong) NSString *text;
@property (nonatomic ,strong) NSString *url;
@property (nonatomic ,assign) NSRange range;
@end
記錄鏈接信息
if(!_linkDataArray){
_linkDataArray = [[NSMutableArray alloc]init];
}
CTLinkData *ctLinkData = [[CTLinkData alloc]init];
ctLinkData.text = [attributedString.string substringWithRange:linkRange];
ctLinkData.url = @"http://www.baidu.com";
ctLinkData.range = linkRange;
[_linkDataArray addObject:ctLinkData];
處理鏈接事件
- (void)userTapGestureDetected:(UIGestureRecognizer *)recognizer{
CGPoint point = [recognizer locationInView:self];
//先判斷是否是點(diǎn)擊的圖片Rect
//......
//再判斷鏈接
CFIndex idx = [self touchPointOffset:point];
if (idx != -1) {
for(CTLinkData *linkData in _linkDataArray){
if (NSLocationInRange(idx, linkData.range)) {
NSLog(@"tap link handle,url:%@",linkData.url);
break;
}
}
}
}
根據(jù)點(diǎn)擊點(diǎn)獲取字符串偏移
- (CFIndex)touchPointOffset:(CGPoint)point{
//獲取所有行
CFArrayRef lines = CTFrameGetLines(_ctFrame);
if(lines == nil){
return -1;
}
CFIndex count = CFArrayGetCount(lines);
//獲取每行起點(diǎn)
CGPoint origins[count];
CTFrameGetLineOrigins(_ctFrame, CFRangeMake(0, 0), origins);
//Flip
CGAffineTransform transform = CGAffineTransformMakeTranslation(0, self.bounds.size.height);
transform = CGAffineTransformScale(transform, 1.f, -1.f);
CFIndex idx = -1;
for (int i = 0; i< count; i++) {
CGPoint lineOrigin = origins[i];
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
//獲取每一行Rect
CGFloat ascent = 0.0f;
CGFloat descent = 0.0f;
CGFloat leading = 0.0f;
CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGRect lineRect = CGRectMake(lineOrigin.x, lineOrigin.y - descent, width, ascent + descent);
lineRect = CGRectApplyAffineTransform(lineRect, transform);
if(CGRectContainsPoint(lineRect,point)){
//將point相對(duì)于view的坐標(biāo)轉(zhuǎn)換為相對(duì)于該行的坐標(biāo)
CGPoint linePoint = CGPointMake(point.x-lineRect.origin.x, point.y-lineRect.origin.y);
//根據(jù)當(dāng)前行的坐標(biāo)獲取相對(duì)整個(gè)CoreText串的偏移
idx = CTLineGetStringIndexForPosition(line, linePoint);
}
}
return idx;
}
6、微博類型富文本實(shí)現(xiàn)異步繪制
當(dāng)我們涉及到圖文混排時(shí)候的高度計(jì)算
http://www.itdecent.cn/p/a7f55e456539
YYLable
1、解析@、超鏈接、圖片、表情
2、逐行逐Run異步繪制,
3、點(diǎn)擊高亮背景繪制
3、點(diǎn)擊效果、事件
參考