CoreText知識(shí)積累

image.png

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

image.png

origin表示的是原點(diǎn) 基線表示的是過原點(diǎn)的x軸票编,ascent表示的是CTRun頂線距離基線的距離褪储,descent表示的是底線距離底線的距離

首先需要設(shè)置一個(gè)回調(diào)的結(jié)構(gòu)體,主要用來獲取圖片的寬高慧域,圖片距離頂部基線的距離鲤竹,還有圖片距離底部基線的距離

image.png

下圖中綠色線條表示基線,黃色線條表示下行高度昔榴,綠色線條到紅框最頂部的距離為上行高度辛藻,而黃色線條到紅框底部的距離為行間距。因此行高的計(jì)算公式是lineHeight = Ascent + |Descent| + Leading

CoreText幾個(gè)比較重要的概念

image.png

CoreText會(huì)把一行里連在一起相同屬性的文字合在一起作為一個(gè)CTRun互订,每一行是一個(gè)CTLine吱肌,多行合在一起組成CTFrame。如上圖仰禽,第一行的文字有兩種樣式氮墨,第一部分是加粗,第二部分是斜體吐葵,因?yàn)闃邮讲煌苑殖闪藘蓚€(gè)CTRun规揪,CTLine包含了這兩個(gè)CTRun,CTFrame包含了所有CTLine折联。

下面這個(gè)是coreText 繪制富文本的工作流程

image.png

逐行繪制

image.png

第一步

  獲取上下文 也就是獲取畫布

  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://qilishare.org/2016/03/01/IOS%E5%AF%8C%E6%96%87%E6%9C%AC-Coretext%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B%EF%BC%88%E4%B8%80%EF%BC%89/

http://www.saitjr.com/ios/use-coretext-make-typesetting-picture-and-text.html

https://juejin.im/entry/57ce6d5767f3560057b3002c(swift)

https://junyixie.github.io/2017/03/04/iOS-Core-Text/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市赵抢,隨后出現(xiàn)的幾起案子剧蹂,更是在濱河造成了極大的恐慌,老刑警劉巖烦却,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宠叼,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡其爵,警方通過查閱死者的電腦和手機(jī)冒冬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來摩渺,“玉大人窄驹,你說我怎么就攤上這事≈ぢ撸” “怎么了乐埠?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)囚企。 經(jīng)常有香客問我丈咐,道長(zhǎng),這世上最難降的妖魔是什么龙宏? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任棵逊,我火速辦了婚禮,結(jié)果婚禮上银酗,老公的妹妹穿的比我還像新娘辆影。我一直安慰自己徒像,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布蛙讥。 她就那樣靜靜地躺著锯蛀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪次慢。 梳的紋絲不亂的頭發(fā)上旁涤,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音迫像,去河邊找鬼劈愚。 笑死,一個(gè)胖子當(dāng)著我的面吹牛闻妓,可吹牛的內(nèi)容都是我干的菌羽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼由缆,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼算凿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起犁功,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤氓轰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后浸卦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體署鸡,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年限嫌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了靴庆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡怒医,死狀恐怖炉抒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情稚叹,我是刑警寧澤焰薄,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站扒袖,受9級(jí)特大地震影響塞茅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜季率,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一野瘦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦鞭光、人聲如沸吏廉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽席覆。三九已至,卻和暖如春啡省,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背髓霞。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工卦睹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人方库。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓结序,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親纵潦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子徐鹤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355