CoreText 是用于處理文字和字體的底層技術(shù)席噩。它直接和 Core Graphics(又被稱為 Quartz)打交道昼伴。Quartz 是一個(gè) 2D 圖形渲染引擎员寇,能夠處理 OSX 和 iOS 中的圖形顯示。
Quartz 能夠直接處理字體(font)和字形(glyphs)缤沦,將文字渲染到界面上塘偎,它是基礎(chǔ)庫中唯一能夠處理字形的模塊疗涉。因此,CoreText 為了排版吟秩,需要將顯示的文本內(nèi)容咱扣、位置、字體涵防、字形直接傳遞給 Quartz闹伪。相比其它 UI 組件,由于 CoreText 直接和 Quartz 來交互武学,所以它具有高速的排版效果祭往。
CoreText使用的優(yōu)勢(shì):
1.api調(diào)用更底層伦意,效率更高
2.可以在后臺(tái)渲染
3.實(shí)現(xiàn)復(fù)雜的圖文混排需求
4.渲染速度相比于uikit 跟 uiwebview更快
缺點(diǎn):
1.基于c的api對(duì)于ios開發(fā)者不是很友好
2.內(nèi)存需要自己去控制火窒,容易出現(xiàn)內(nèi)存泄露
下圖是 CoreText 的架構(gòu)圖,可以看到驮肉,CoreText 處于非常底層的位置熏矿,上層的 UI 控件(包括 UILabel,UITextField 以及 UITextView)和 UIWebView 都是基于 CoreText 來實(shí)現(xiàn)的。
CTRun
origin表示的是原點(diǎn) 基線表示的是過原點(diǎn)的x軸票编,ascent表示的是CTRun頂線距離基線的距離褪储,descent表示的是底線距離底線的距離
首先需要設(shè)置一個(gè)回調(diào)的結(jié)構(gòu)體,主要用來獲取圖片的寬高慧域,圖片距離頂部基線的距離鲤竹,還有圖片距離底部基線的距離
下圖中綠色線條表示基線,黃色線條表示下行高度昔榴,綠色線條到紅框最頂部的距離為上行高度辛藻,而黃色線條到紅框底部的距離為行間距。因此行高的計(jì)算公式是lineHeight = Ascent + |Descent| + Leading
CoreText幾個(gè)比較重要的概念
CoreText會(huì)把一行里連在一起相同屬性的文字合在一起作為一個(gè)CTRun互订,每一行是一個(gè)CTLine吱肌,多行合在一起組成CTFrame。如上圖仰禽,第一行的文字有兩種樣式氮墨,第一部分是加粗,第二部分是斜體吐葵,因?yàn)闃邮讲煌苑殖闪藘蓚€(gè)CTRun规揪,CTLine包含了這兩個(gè)CTRun,CTFrame包含了所有CTLine折联。
下面這個(gè)是coreText 繪制富文本的工作流程
逐行繪制
第一步
獲取上下文 也就是獲取畫布
CGContextRef context = UIGraphicsGetCurrentContext();
第二步
做的是坐標(biāo)系的反轉(zhuǎn)粒褒,因?yàn)閁IKit原點(diǎn)是在左上角 而CoreText原點(diǎn)是在左下角(CoreText使用的是笛卡爾坐標(biāo)系)
CGContextSetTextMatrix(contextRef, CGAffineTransformIdentity);
CGContextTranslateCTM(contextRef, 0, self.bounds.size.height);
CGContextScaleCTM(contextRef, 1.0, -1.0);
第三步
創(chuàng)建一塊區(qū)域,用于展示coreText诚镰,可以自定義展示文字的范圍
比如我繪制了一個(gè)橢圓形 作為展示的區(qū)域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);
[圖片上傳失敗...(image-ef1f52-1517822365469)]
第四步
通過NSAttributeString來生成CTFramesetter,可以通過coreText提供的api來完成
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributed);
后面的傳參就是一個(gè)NSAttributeString類型的屬性字符串
第五步
創(chuàng)建CTFrame奕坟,通過coreText提供的一個(gè)API
CTFrameRef ctFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributed.length), path, NULL);
第一個(gè)參數(shù)就是上面創(chuàng)建的CTFramesetter實(shí)例,第二個(gè)參數(shù)傳入需要繪制的文字范圍
第三個(gè)參數(shù)傳入的是創(chuàng)建的展示范圍
最后
調(diào)用CTFrameDraw函數(shù)來繪制文字 傳入的參數(shù)一個(gè)是第五步創(chuàng)建的CTFrame實(shí)例清笨,第二個(gè)是畫布
CTFrameDraw(ctFrame, contextRef);
最后 也是非常重要的一步 就是釋放掉創(chuàng)建的實(shí)例月杉,因?yàn)锳RC是不能管理CF開頭的對(duì)象
CFRelease(path);
CFRelease(framesetter);
CFRelease(ctFrame);
CoreText 圖文混排的原理
其原理就是 在要插入圖片的位置插入一個(gè)富文本類型的占位符,通過CTRunDelegate來設(shè)置圖片
|
<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;">/* 設(shè)置一個(gè)回調(diào)結(jié)構(gòu)體抠艾,告訴代理該回調(diào)那些方法 */
CTRunDelegateCallbacks callBacks;//創(chuàng)建一個(gè)回調(diào)結(jié)構(gòu)體苛萎,設(shè)置相關(guān)參數(shù)
memset(&callBacks,0,sizeof(CTRunDelegateCallbacks));//memset將已開辟內(nèi)存空間 callbacks 的首 n 個(gè)字節(jié)的值設(shè)為值 0, 相當(dāng)于對(duì)CTRunDelegateCallbacks內(nèi)存空間初始化
callBacks.version = kCTRunDelegateVersion1;//設(shè)置回調(diào)版本,默認(rèn)這個(gè)
callBacks.getAscent = ascentCallBacks;//設(shè)置圖片頂部距離基線的距離
callBacks.getDescent = descentCallBacks;//設(shè)置圖片底部距離基線的距離
callBacks.getWidth = widthCallBacks;//設(shè)置圖片寬度</pre>
|
然后創(chuàng)建一個(gè)CTRunDelegateRef
|
<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;">NSDictionary * dicPic = @{@"height":@129,@"width":@400};//創(chuàng)建一個(gè)圖片尺寸的字典检号,初始化代理對(duì)象需要
CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)dicPic);//創(chuàng)建代理</pre>
|
將上面創(chuàng)建的回調(diào)結(jié)構(gòu)體傳到CTRunDelegateRef中,目前就完成了圖片尺寸與代理之間的綁定
三個(gè)回調(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>
|
圖片插入之前的準(zhǔn)備工作已經(jīng)完成了腌歉,下面是圖片的插入操作:
首先創(chuàng)建一個(gè)富文本類型的圖片占位符,綁定我們代理
|
<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進(jìn)行C與OC數(shù)據(jù)類型的轉(zhuǎn)換齐苛,C為非ARC翘盖,需要手動(dòng)管理)</pre>
|
然后將我們創(chuàng)建的占位符插入到富文本中
|
<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;">[attributeStr insertAttributedString:placeHolderAttrStr atIndex:12];</pre>
|
現(xiàn)在我們就拿到了一個(gè)帶有空白占位符的屬性字符串。如果把現(xiàn)在的屬性字符串繪制到屏幕上,就是一個(gè)帶有寬度400 高度129的富文本凹蜂。
如何做到圖文混排呢馍驯,我們只需要拿到占位符的坐標(biāo)阁危,然后在占位符的位置繪制相應(yīng)的圖片大小就可以了。這個(gè)就是圖文混排的原理了汰瘫,是不是非常簡(jiǎn)單狂打。
首先將上面帶有空白占位符的文本繪制出來
|
<pre style="margin: 0px; tab-size: 4; white-space: pre-wrap;">CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);//一個(gè)frame的工廠,負(fù)責(zé)生成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)用一個(gè)函數(shù)即可
CGContextDrawImage(context,imgFrm, image.CGImage)
上面函數(shù)中 context表示的是畫布的上下文趴乡,image.cgimage也可以拿到。最重要的就是imgFrm的獲取了蝗拿。
首先獲取到CTFrame中所有CTLine的起點(diǎn)
|
<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];//建立起點(diǎn)的數(shù)組(cgpoint類型為結(jié)構(gòu)體浙宜,故用C語言的數(shù)組)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);//獲取起點(diǎn)</pre>
|
最后就是遍歷我們frame中所有的CTRun,檢查每一個(gè)CTRun是否綁定了圖片蛹磺。如果綁定了圖片粟瞬,那么根據(jù)CTRun所在的CTLine的origin以及CTRun在CTLine中的橫向偏移來計(jì)算出CTRun的原點(diǎn)。
加上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];//獲取一個(gè)起點(diǎn)
CGFloat ascent;//獲取上距
CGFloat descent;//獲取下距
CGRect boundsRun;//創(chuàng)建一個(gè)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是行起點(diǎn)位置,加上每個(gè)字的偏移量得到每個(gè)字的x
boundsRun.origin.y = point.y - descent;//計(jì)算原點(diǎn)
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獲取一個(gè)CTLine中的所有CTRun
辛慰。
里層for循環(huán)是檢查每個(gè)CTRun区匠。
通過CTRunGetAttributes拿到該CTRun的所有屬性
。
通過kvc取得屬性中的代理屬性
帅腌。
接下來判斷代理屬性是否為空
驰弄。因?yàn)閳D片的占位符我們是綁定了代理的,而文字沒有速客。以此區(qū)分文字和圖片戚篙。
如果代理不為空,通過CTRunDelegateGetRefCon取得生成代理時(shí)綁定的對(duì)象
溺职。判斷類型
是否是我們綁定的類型岔擂,防止取得我們之前為其他的富文本綁定過代理
。
如果兩條都符合浪耘,ok乱灵,這就是我們要的那個(gè)CTRun
。
開始計(jì)算該CTRun的frame
吧七冲。
獲取原點(diǎn)
和獲取寬高
被痛倚。
通過CTRunGetTypographicBounds
取得寬,ascent和descent癞埠。有了上面的介紹我們應(yīng)該知道圖片的高度就是ascent+descent了吧。
接下來獲取原點(diǎn)。
CTLineGetOffsetForStringIndex獲取對(duì)應(yīng)CTRun的X偏移量
洪橘。
取得對(duì)應(yīng)CTLine的原點(diǎn)的Y帚戳,減去圖片的下邊距才是圖片的原點(diǎn)
,這點(diǎn)應(yīng)該很好理解通铲。
至此毕莱,我們已經(jīng)獲得了圖片的frame
了。因?yàn)橹唤壎艘粋€(gè)圖片颅夺,所以直接return就好了朋截,如果多張圖片可以繼續(xù)遍歷返回?cái)?shù)組。
獲取到圖片的frame吧黄,我們就可以繪制圖片了部服,用上面介紹的方法。
記錄一個(gè)coreText中 獲取點(diǎn)擊字符 range的方法
// 將點(diǎn)擊的位置轉(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坐標(biāo)
CGPoint origins[count];
CTFrameGetLineOrigins(textFrame, CFRangeMake(0,0), origins);
// 翻轉(zhuǎn)坐標(biāo)系
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)) { // 將點(diǎn)擊的坐標(biāo)轉(zhuǎn)換成相對(duì)于當(dāng)前行的坐標(biāo) CGPoint relativePoint = CGPointMake(point.x-CGRectGetMinX(rect), point.y-CGRectGetMinY(rect)); // 獲得當(dāng)前點(diǎn)擊坐標(biāo)對(duì)應(yīng)的字符串偏移 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