CoreText知識積累

image.png

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

image.png

origin表示的是原點 基線表示的是過原點的x軸,ascent表示的是CTRun頂線距離基線的距離,descent表示的是底線距離底線的距離

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

image.png

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

CoreText幾個比較重要的概念

image.png

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

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

image.png

逐行繪制

image.png

第一步

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

  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://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)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容