
CoreText 是用于處理文字和字體的底層技術(shù)。它直接和 Core Graphics(又被稱為 Quartz)打交道。Quartz 是一個 2D 圖形渲染引擎,能夠處理 OSX 和 iOS 中的圖形顯示。
Quartz 能夠直接處理字體(font)和字形(glyphs),將文字渲染到界面上,它是基礎(chǔ)庫中唯一能夠處理字形的模塊。因此,CoreText 為了排版,需要將顯示的文本內(nèi)容、位置、字體、字形直接傳遞給 Quartz。相比其它 UI 組件,由于 CoreText 直接和 Quartz 來交互,所以它具有高速的排版效果。
CoreText使用的優(yōu)勢:
1.api調(diào)用更底層,效率更高
2.可以在后臺渲染
3.實現(xiàn)復雜的圖文混排需求
4.渲染速度相比于uikit 跟 uiwebview更快
缺點:
1.基于c的api對于ios開發(fā)者不是很友好
2.內(nèi)存需要自己去控制,容易出現(xiàn)內(nèi)存泄露
下圖是 CoreText 的架構(gòu)圖,可以看到,CoreText 處于非常底層的位置,上層的 UI 控件(包括 UILabel,UITextField 以及 UITextView)和 UIWebView 都是基于 CoreText 來實現(xiàn)的。
CTRun

origin表示的是原點 基線表示的是過原點的x軸,ascent表示的是CTRun頂線距離基線的距離,descent表示的是底線距離底線的距離
首先需要設(shè)置一個回調(diào)的結(jié)構(gòu)體,主要用來獲取圖片的寬高,圖片距離頂部基線的距離,還有圖片距離底部基線的距離

下圖中綠色線條表示基線,黃色線條表示下行高度,綠色線條到紅框最頂部的距離為上行高度,而黃色線條到紅框底部的距離為行間距。因此行高的計算公式是lineHeight = Ascent + |Descent| + Leading
CoreText幾個比較重要的概念

CoreText會把一行里連在一起相同屬性的文字合在一起作為一個CTRun,每一行是一個CTLine,多行合在一起組成CTFrame。如上圖,第一行的文字有兩種樣式,第一部分是加粗,第二部分是斜體,因為樣式不同所以分成了兩個CTRun,CTLine包含了這兩個CTRun,CTFrame包含了所有CTLine。
下面這個是coreText 繪制富文本的工作流程

逐行繪制

第一步
獲取上下文 也就是獲取畫布
CGContextRef context = UIGraphicsGetCurrentContext();
第二步
做的是坐標系的反轉(zhuǎn),因為UIKit原點是在左上角 而CoreText原點是在左下角(CoreText使用的是笛卡爾坐標系)
CGContextSetTextMatrix(contextRef, CGAffineTransformIdentity);
CGContextTranslateCTM(contextRef, 0, self.bounds.size.height);
CGContextScaleCTM(contextRef, 1.0, -1.0);
第三步
創(chuàng)建一塊區(qū)域,用于展示coreText,可以自定義展示文字的范圍
比如我繪制了一個橢圓形 作為展示的區(qū)域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);
[圖片上傳失敗...(image-ef1f52-1517822365469)]
第四步
通過NSAttributeString來生成CTFramesetter,可以通過coreText提供的api來完成
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributed);
后面的傳參就是一個NSAttributeString類型的屬性字符串
第五步
創(chuàng)建CTFrame,通過coreText提供的一個API
CTFrameRef ctFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributed.length), path, NULL);
第一個參數(shù)就是上面創(chuàng)建的CTFramesetter實例,第二個參數(shù)傳入需要繪制的文字范圍
第三個參數(shù)傳入的是創(chuàng)建的展示范圍
最后
調(diào)用CTFrameDraw函數(shù)來繪制文字 傳入的參數(shù)一個是第五步創(chuàng)建的CTFrame實例,第二個是畫布
CTFrameDraw(ctFrame, contextRef);
最后 也是非常重要的一步 就是釋放掉創(chuàng)建的實例,因為ARC是不能管理CF開頭的對象
CFRelease(path);
CFRelease(framesetter);
CFRelease(ctFrame);
CoreText 圖文混排的原理
其原理就是 在要插入圖片的位置插入一個富文本類型的占位符,通過CTRunDelegate來設(shè)置圖片
|
<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;">/* 設(shè)置一個回調(diào)結(jié)構(gòu)體,告訴代理該回調(diào)那些方法 */
CTRunDelegateCallbacks callBacks;//創(chuàng)建一個回調(diào)結(jié)構(gòu)體,設(shè)置相關(guān)參數(shù)
memset(&callBacks,0,sizeof(CTRunDelegateCallbacks));//memset將已開辟內(nèi)存空間 callbacks 的首 n 個字節(jié)的值設(shè)為值 0, 相當于對CTRunDelegateCallbacks內(nèi)存空間初始化
callBacks.version = kCTRunDelegateVersion1;//設(shè)置回調(diào)版本,默認這個
callBacks.getAscent = ascentCallBacks;//設(shè)置圖片頂部距離基線的距離
callBacks.getDescent = descentCallBacks;//設(shè)置圖片底部距離基線的距離
callBacks.getWidth = widthCallBacks;//設(shè)置圖片寬度</pre>
|
然后創(chuàng)建一個CTRunDelegateRef
|
<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;">NSDictionary * dicPic = @{@"height":@129,@"width":@400};//創(chuàng)建一個圖片尺寸的字典,初始化代理對象需要
CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)dicPic);//創(chuàng)建代理</pre>
|
將上面創(chuàng)建的回調(diào)結(jié)構(gòu)體傳到CTRunDelegateRef中,目前就完成了圖片尺寸與代理之間的綁定
三個回調(diào)的代理方法
|
<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;">static CGFloat ascentCallBacks(void * ref) { return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"height"] floatValue]; }
static CGFloat descentCallBacks(void * ref) { return 0; }
static CGFloat widthCallBacks(void * ref) { return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"width"] floatValue]; }</pre>
|
圖片插入之前的準備工作已經(jīng)完成了,下面是圖片的插入操作:
首先創(chuàng)建一個富文本類型的圖片占位符,綁定我們代理
|
<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;"> unichar placeHolder = 0xFFFC;//創(chuàng)建空白字符
NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];//已空白字符生成字符串
NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];//用字符串初始化占位符的富文本
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttrStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);//給字符串中的范圍中字符串設(shè)置代理
CFRelease(delegate);//釋放(__bridge進行C與OC數(shù)據(jù)類型的轉(zhuǎn)換,C為非ARC,需要手動管理)</pre>
|
然后將我們創(chuàng)建的占位符插入到富文本中
|
<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;">[attributeStr insertAttributedString:placeHolderAttrStr atIndex:12];</pre>
|
現(xiàn)在我們就拿到了一個帶有空白占位符的屬性字符串。如果把現(xiàn)在的屬性字符串繪制到屏幕上,就是一個帶有寬度400 高度129的富文本。
如何做到圖文混排呢,我們只需要拿到占位符的坐標,然后在占位符的位置繪制相應的圖片大小就可以了。這個就是圖文混排的原理了,是不是非常簡單。
首先將上面帶有空白占位符的文本繪制出來
|
<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;">CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);//一個frame的工廠,負責生成frame
CGMutablePathRef path = CGPathCreateMutable();//創(chuàng)建繪制區(qū)域
CGPathAddRect(path, NULL, self.bounds);//添加繪制尺寸
NSInteger length = attributeStr.length;
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0,length), path, NULL);//工廠根據(jù)繪制區(qū)域及富文本(可選范圍,多次設(shè)置)設(shè)置frame
CTFrameDraw(frame, context);//根據(jù)frame繪制文字</pre>
|
要繪制圖片調(diào)用一個函數(shù)即可
CGContextDrawImage(context,imgFrm, image.CGImage)
上面函數(shù)中 context表示的是畫布的上下文,image.cgimage也可以拿到。最重要的就是imgFrm的獲取了。
首先獲取到CTFrame中所有CTLine的起點
|
<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;">NSArray * arrLines = (NSArray *)CTFrameGetLines(frame);//根據(jù)frame獲取需要繪制的線的數(shù)組
NSInteger count = [arrLines count];//獲取線的數(shù)量
CGPoint points[count];//建立起點的數(shù)組(cgpoint類型為結(jié)構(gòu)體,故用C語言的數(shù)組)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);//獲取起點</pre>
|
最后就是遍歷我們frame中所有的CTRun,檢查每一個CTRun是否綁定了圖片。如果綁定了圖片,那么根據(jù)CTRun所在的CTLine的origin以及CTRun在CTLine中的橫向偏移來計算出CTRun的原點。
加上CTRun的尺寸,也就變成了CTRun的尺寸。
|
<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;">for (int i = 0; i < count; i ++) {//遍歷線的數(shù)組
CTLineRef line = (__bridge CTLineRef)arrLines[i];
NSArray * arrGlyphRun = (NSArray *)CTLineGetGlyphRuns(line);//獲取GlyphRun數(shù)組(GlyphRun:高效的字符繪制方案)
for (int j = 0; j < arrGlyphRun.count; j ++) {//遍歷CTRun數(shù)組
CTRunRef run = (__bridge CTRunRef)arrGlyphRun[j];//獲取CTRun
NSDictionary * attributes = (NSDictionary *)CTRunGetAttributes(run);//獲取CTRun的屬性
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];//獲取代理
if (delegate == nil) {//非空
continue;
}
NSDictionary * dic = CTRunDelegateGetRefCon(delegate);//判斷代理字典
if (![dic isKindOfClass:[NSDictionary class]]) {
continue;
}
CGPoint point = points[i];//獲取一個起點
CGFloat ascent;//獲取上距
CGFloat descent;//獲取下距
CGRect boundsRun;//創(chuàng)建一個frame
boundsRun.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
boundsRun.size.height = ascent + descent;//取得高
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);//獲取x偏移量
boundsRun.origin.x = point.x + xOffset;//point是行起點位置,加上每個字的偏移量得到每個字的x
boundsRun.origin.y = point.y - descent;//計算原點
CGPathRef path = CTFrameGetPath(frame);//獲取繪制區(qū)域
CGRect colRect = CGPathGetBoundingBox(path);//獲取剪裁區(qū)域邊框
CGRect imageBounds = CGRectOffset(boundsRun, colRect.origin.x, colRect.origin.y);
return imageBounds;
}</pre>
|
外層for循環(huán)呢,是為了取到所有的CTLine。
類型轉(zhuǎn)換什么的我就不多說了,然后通過CTLineGetGlyphRuns獲取一個CTLine中的所有CTRun。
里層for循環(huán)是檢查每個CTRun。
通過CTRunGetAttributes拿到該CTRun的所有屬性。
通過kvc取得屬性中的代理屬性。
接下來判斷代理屬性是否為空。因為圖片的占位符我們是綁定了代理的,而文字沒有。以此區(qū)分文字和圖片。
如果代理不為空,通過CTRunDelegateGetRefCon取得生成代理時綁定的對象。判斷類型是否是我們綁定的類型,防止取得我們之前為其他的富文本綁定過代理。
如果兩條都符合,ok,這就是我們要的那個CTRun。
開始計算該CTRun的frame吧。
獲取原點和獲取寬高被。
通過CTRunGetTypographicBounds取得寬,ascent和descent。有了上面的介紹我們應該知道圖片的高度就是ascent+descent了吧。
接下來獲取原點。
CTLineGetOffsetForStringIndex獲取對應CTRun的X偏移量。
取得對應CTLine的原點的Y,減去圖片的下邊距才是圖片的原點,這點應該很好理解。
至此,我們已經(jīng)獲得了圖片的frame了。因為只綁定了一個圖片,所以直接return就好了,如果多張圖片可以繼續(xù)遍歷返回數(shù)組。
獲取到圖片的frame,我們就可以繪制圖片了,用上面介紹的方法。
記錄一個coreText中 獲取點擊字符 range的方法
// 將點擊的位置轉(zhuǎn)換成字符串的偏移量,如果沒有找到,則返回-1
-
(CFIndex)touchContentOffsetInView:(UIView *)view atPoint:(CGPoint)point data:(HTLSearchOptimizeCoreTextData *)data {
CTFrameRef textFrame = data.ctFrame;
CFArrayRef lines = CTFrameGetLines(textFrame);
if (!lines) {
return -1;}
CFIndex count = CFArrayGetCount(lines);
// 獲得每一行的origin坐標
CGPoint origins[count];
CTFrameGetLineOrigins(textFrame, CFRangeMake(0,0), origins);
// 翻轉(zhuǎn)坐標系
CGAffineTransform transform = CGAffineTransformMakeTranslation(0, view.bounds.size.height);
transform = CGAffineTransformScale(transform, 1.f, -1.f);
CFIndex idx = -1;
for (int i = 0; i < count; i++) {
CGPoint linePoint = origins[i]; CTLineRef line = CFArrayGetValueAtIndex(lines, i); // 獲得每一行的CGRect信息 CGRect flippedRect = [self getLineBounds:line point:linePoint]; CGRect rect = CGRectApplyAffineTransform(flippedRect, transform); if (CGRectContainsPoint(rect, point)) { // 將點擊的坐標轉(zhuǎn)換成相對于當前行的坐標 CGPoint relativePoint = CGPointMake(point.x-CGRectGetMinX(rect), point.y-CGRectGetMinY(rect)); // 獲得當前點擊坐標對應的字符串偏移 idx = CTLineGetStringIndexForPosition(line, relativePoint); }}
return idx;
}
-
(CGRect)getLineBounds:(CTLineRef)line point:(CGPoint)point {
CGFloat ascent = 0.0f;
CGFloat descent = 0.0f;
CGFloat leading = 0.0f;
CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat height = ascent + descent;
return CGRectMake(point.x, point.y - descent, width, height);
}
http://ivanyuan.farbox.com/post/coretextyu-textkitru-men
http://www.saitjr.com/ios/use-coretext-make-typesetting-picture-and-text.html