CoreText(一)簡單使用

iOS中的文字渲染

在iOS出現(xiàn)早期,顯示特性文本唯一可行的辦法就是使用UIWebView和利用HTML來處理定制特性。不過這種方法實現(xiàn)起來很困難,且性能很糟糕。iOS3.2中加入了CoreText,它將Mac平臺的NSAttributedString的全部功能帶到了移動平臺。不過CoreText有些復雜不太實用。
在TextKit推出之前,iOS的文本渲染是個有一定難度且復雜的話題。TextKit的第一次發(fā)布是作為iOS7的一部分,TextKit不是一個傳統(tǒng)意義上的框架。相反,TextKit是對于更好地處理文本展示對象及其特性的一組增強功能在術語上的表示。

TextKit

Text Kit is built on top of Core Text, so it provides the same speed and power.

??是蘋果官方給出的結(jié)構(gòu)圖:

那么TextKit包含什么內(nèi)容呢?先想一下實現(xiàn)一段富文本需要做的事:第一步,生成NSAttributedString屬性字符串對象(通過AttributeName設置不同的樣式);第二步,直接交給UILabel、UITextView、UITextField渲染出來,非常方便。TextKit在這中間幫我們做了絕大部分工作,并且支持子類化、提供代理和一套全面的通知以實現(xiàn)深度定制。下面是官方文檔里關于TextKit主要類之間的關系圖:


主要涉及三個類:NSTextStorage、NSLayoutManager和NSTextContainer:
NSTextStorage用來保存和管理textView要展示的文本,它是NSMutableAttributedString的子類;
NSLayoutManager管理文本的排版布局;
NSTextContainer定義文本的展示區(qū)域;

從官方文檔和注釋可以大概了解這三個類的作用和提供出來的一些接口,但個人感覺直接理解起來還是有點抽象。
我們不妨先思考下一個文本(帶屬性的字符串),怎么渲染到屏幕上?拋開最底層的渲染原理,其實還是通過圖形接口一個一個字符繪制出來的,這種“笨方法”我們可以通過CoreText實現(xiàn),而TextKit也是在這個基礎上設計的易用的面相對象一套東西。了解這些底層方法之后再回過頭來看TextKit的設計,就不那么抽象了。

CoreText

既然是一個字符一個字符繪制出來的,那么就需要知道每個字符的字體、所在位置、坐標等等信息,在CoreText中用CTFrameRef來表示,有了frame之后使用
CTFrameDraw方法在上下文中繪制。(實際上CTFrame是一個比較“大”的概念,包含整個文本的信息,后面再詳細討論frame里面有什么東東)

void CTFrameDraw(
    CTFrameRef frame,
    CGContextRef context )

還是先從一個簡單的demo說起吧,我們創(chuàng)建一個CTSimpleView繼承自UIView,在它的drawRect方法里面用CoreText繪制一段文本:

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    /* 文本的內(nèi)容CFAttributedStringRef */
    CFAttributedStringRef astr = CFAttributedStringCreate(kCFAllocatorSystemDefault, 
                                                          CFSTR("HelloWord!"), NULL);

    /* 根據(jù)文本內(nèi)容創(chuàng)建一個framesetter對象(創(chuàng)建frame時使用) */
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(astr);
    
    /* 渲染文本用到的路徑:可繪制區(qū)域(創(chuàng)建frame時使用) */
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    
    /* 根據(jù)framesetter和path得到frame:包含字體信息和坐標信息 */
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, CFAttributedStringGetLength(astr)), path, NULL);
    
    // 在context中繪制
    CTFrameDraw(frame, context);
    
    // 釋放cf對象
    CFRelease(frame);
    CFRelease(path);
    CFRelease(framesetter);
}

很好理解,有了文本內(nèi)容和路徑,創(chuàng)建frame對象在調(diào)用draw方法就可以繪制出來。運行后會發(fā)現(xiàn)文本是倒著的,是因為iOS的坐標系統(tǒng)是y軸向下的,與Mac平臺相反,所以要做下坐標變換:

/* CTM:Current transformation matrix 上下文當前變換矩陣 */
/* 這里是因為CT的坐標系(y軸向上)和UIKit的坐標系(y軸向下)的區(qū)別,
 * 需要做變換:(x,y)->(x,-y+height) */
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);

一般的文章包括一些源碼里都會像??這么寫,這里簡單討論下變換矩陣和仿射變換:

/* 如果把上面兩句話調(diào)換下順序會怎樣?*/

    CGContextScaleCTM(context, 1.0, -1.0);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);

/* 會發(fā)現(xiàn)文本沒了,為什么?(x,y)->(x,-y+height)明明是y先乘-1在加height的呀
 * 我們看下CGContextScaleCTM這類函數(shù)的注釋,會發(fā)現(xiàn)它們都是對CTM(current 
 * graphics state's transformation matrix)做的變換,對當前繪圖狀態(tài)的變換矩
 * 陣做變換,都是左乘的(另一篇文章http://www.itdecent.cn/p/09c1d32e43fc專門
 * 討論了對變換做變換的概念,可以理解為左乘的矩陣會影響到后面的矩陣) 所以這里從上
 * 往下的函數(shù)順序,在坐標變換時是反著來的:先加了height,在乘-1 變成了:(x,y)->
 * (x,-(y+height))了,自然不在我們的繪制范圍內(nèi)了。 */

/* 在說下什么是仿射變換:有的地方說就是線性變換+平移,經(jīng)過變換后,直線還是直線,平行線還是平行線。
 * 通俗點說,對二維坐標(x,y)變換后的坐標(f(x,y),g(x,y)) 中f和g都是關于x,y的二元一次函數(shù):
 * f(x,y) = ax+by+e;  g(x,y) = cx+dy+f   用tx、ty表示平移的量e和f,就是
 * CGAffineTransform的定義: */

struct CGAffineTransform {
  CGFloat a, b, c, d;
  CGFloat tx, ty;
};

/* 對應變換矩陣
 *  a  c  0
 *  b  d  0
 * fx  fy 1
 * 只是右邊一列固定是(0 0 1) */

/* 所以上邊兩行也可以寫成:*/
   CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, self.bounds.size.height));

再回到開始的問題,我們用CTFrameDraw繪制的是整個文本frame,那么CTFrameRef里面都有什么?打個斷點看下:

簡單一句“HelloWord!”的frame竟包含了這么多東西,別慌,一點一點看:
首先是visible string range,很好理解就是文本“HelloWord!”的range,
path是我們定義的CGPath 可繪制區(qū)域,
attributes是null(我們剛才的代碼并沒有設置attributes),
(重點來了)lines:發(fā)現(xiàn)從第二行開始到最后其實是一個CTLine對象,所以frame包含一些通用的屬性(range、path、attributes)和一組CTLine(lines),詳細信息還在CTLine中:

再看CTLine里包含了什么:
run count:(這個先不管,往后看就知道了)
string range:這個和frame里的一樣都是(0,10),因為我們的frame只有這一個line
width:寬度,不用解釋了吧

A/D/L:Ascent(上高)、Descent(下高)、Leading(行距)這三個屬性直接看官方文檔里的圖,一目了然:

glyph count:這個也好理解,line包含的字符個數(shù)
再往下又是一組CTRun對象,run里面文本內(nèi)容、字體等相信信息都有了,到這其實frame的結(jié)構(gòu)已經(jīng)很清晰了:
一段文本的frame包含多個行(line),每行里面分成屬性相同的子串(run),再就是每個字符(glyph)了:

CTLine、CTRun都有相應的draw方法,run作為一個相對基本的單位(其實還有基于字符的CTFontDrawGlyphs,這次先不討論)提供CTRunDelegate幾個回調(diào)方法可以使用,見CTRunDelegate.h:

typedef struct
{
    CFIndex                         version;
    CTRunDelegateDeallocateCallback dealloc;
    CTRunDelegateGetAscentCallback  getAscent;
    CTRunDelegateGetDescentCallback getDescent;
    CTRunDelegateGetWidthCallback   getWidth;
} CTRunDelegateCallbacks;

我們可以自己定義ascent、descent和width。

關于CoreText的簡單用法就是這些,利用這些api可以實現(xiàn)圖文混排和鏈接等稍微復雜些的文本。比如圖文混排,我們可以用一個空白占位符代替,根據(jù)圖像的大小用CTRunDelegate指定空白符為相應大小,在對應位置上繪制上圖片即可,剩下的就是坐標轉(zhuǎn)換、對應等一些“雜事”了。

可以參考唐巧的《iOS開發(fā)進階》中關于CoreText的章節(jié),照著示例敲一遍基本就理解大概了。更深入的研究可以看一些源碼(比如YYText),后續(xù)再更新這方面的一些心得。

最后編輯于
?著作權(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)容

  • 卷首語 歡迎來到 objc.io 第五期! 我們希望你跟我們一樣為 iOS 7 的發(fā)布而感到興奮。選擇這個做為本期...
    評評分分閱讀 648評論 0 4
  • 系列文章: CoreText實現(xiàn)圖文混排 CoreText實現(xiàn)圖文混排之點擊事件 CoreText實現(xiàn)圖文混排之文...
    老司機Wicky閱讀 40,611評論 221 432
  • blog.csdn.net CoreText實現(xiàn)圖文混排 - 博客頻道 CoreText實現(xiàn)圖文混排 也好久沒來寫...
    K_Gopher閱讀 702評論 0 0
  • CoreText是iOS/OSX中文本顯示的一個底層框架,它是用C語言寫成的,有快速簡單的優(yōu)勢。iOS中的Text...
    小貓仔閱讀 5,119評論 2 9
  • 發(fā)現(xiàn) 關注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 15,074評論 4 61

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