很久以前寫的文章搬到這里來放著。
iOS開發(fā)中經(jīng)常會遇到做一些文字排版的需求,文字圖片混排的需求蛾绎,在iOS7 以前一般都使用CoreText來處理這樣的需求,iOS7之后多了一個TextKit 可以選擇鸦列,當(dāng)然TextKit是對CoreText的封裝租冠。
CoreText 是用于處理文字和字體的底層技術(shù),它直接和Core Graphics交互薯嗤;Core Graphics能夠直接處理字體和字形顽爹,將文字渲染到界面上,它是基礎(chǔ)庫中唯一能夠處理字形的模塊骆姐。
1.字符(Character)和字形(Glyphs)
排版系統(tǒng)中文本顯示的一個重要的過程就是字符到字形的轉(zhuǎn)換镜粤,字符是信息本身的元素,而字形是字符的圖形表現(xiàn)玻褪,字符還會有其它表征比如發(fā)音肉渴。 字符在計算機中其實就是一個編碼,某個字符集中的編碼带射,比如Unicode字符集同规,就囊括了大多數(shù)存在的字符。 而字形則是圖形,一般都存儲在字體文件中券勺,字形也有它的編碼绪钥,也就是它在字體中的索引。 一個字符可以對應(yīng)多個字形(不同的字體关炼,或者同種字體的不同樣式:粗體斜體等)程腹;多個字符也可能對應(yīng)一個字形,比如字符的連寫( Ligatures)儒拂。
Roman Ligatures
下面就來詳情看看字形的各個參數(shù)也就是所謂的字形度量Glyph Metrics
- bounding box(邊界框 bbox)跪楞,這是一個假想的框子,它盡可能緊密的裝入字形侣灶。
- baseline(基線)甸祭,一條假想的線,一行上的字形都以此線作為上下位置的參考,在這條線的左側(cè)存在一個點叫做基線的原點褥影,
- ascent(上行高度)從原點到字體中最高(這里的高深都是以基線為參照線的)的字形的頂部的距離池户,ascent是一個正值
- descent(下行高度)從原點到字體中最深的字形底部的距離,- descent是一個負值(比如一個字體原點到最深的字形的底部的距離為2凡怎,那么descent就為-2)
- linegap(行距)校焦,linegap也可以稱作leading(其實準確點講應(yīng)該叫做External leading),行高lineHeight則可以通過 ascent + |descent| + linegap 來計算。
一些Metrics專業(yè)知識還可以參考Free Type的文檔 Glyph metrics统倒,其實iOS就是使用Free Type庫來進行字體渲染的寨典。
2.坐標系
首先不得不說 蘋果編程中的坐標系花樣百出,經(jīng)常讓開發(fā)者措手不及房匆。 傳統(tǒng)的Mac中的坐標系的原點在左下角耸成,比如NSView默認的坐標系,原點就在左下角浴鸿。但Mac中有些View為了其實現(xiàn)的便捷將原點變換到左上角井氢,像NSTableView的坐標系坐標原點就在左上角。iOS UIKit的UIView的坐標系原點在左上角岳链。
往底層看花竞,Core Graphics的context使用的坐標系的原點是在左下角。而在iOS中的底層界面繪制就是通過Core Graphics進行的掸哑,那么坐標系列是如何變換的呢约急? 在UIView的drawRect方法中我們可以通過UIGraphicsGetCurrentContext()來獲得當(dāng)前的Graphics Context。drawRect方法在被調(diào)用前苗分,這個Graphics Context被創(chuàng)建和配置好厌蔽,你只管使用便是。如果你細心俭嘁,通過CGContextGetCTM(CGContextRef c)可以看到其返回的值并不是CGAffineTransformIdentity躺枕,通過打印出來看到值為
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform transform = CGContextGetCTM(context);
NSLog(@"%@",NSStringFromCGAffineTransform(transform));
[2, 0, 0, -2, 0, 202]//[a=2, b=0, c=0, d=-2, tx=0, ty=202]
Core Text一開始便是定位于桌面的排版系統(tǒng)服猪,使用了傳統(tǒng)的原點在左下角的坐標系供填,所以它在繪制文本的時候都是參照左下角的原點進行繪制的拐云。 但是iOS的UIView的drawRect方法的context被做了次flip,如果你啥也不做處理近她,直接在這個context上進行Core Text繪制叉瘩,你會發(fā)現(xiàn)文字是鏡像且上下顛倒。 如圖所示
翻轉(zhuǎn)坐標:
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
3.CoreText排版步驟
使用CoreText進行文字排版的步驟:
1.準備文字也就是 NSMutableAttributedString/ NSAttributedString 對象粘捎。
2.根據(jù)NSMutableAttributedString創(chuàng)建CTFramesetter并初始化薇缅,同時系統(tǒng)自動的創(chuàng)建了CTTypesetter,CTTypesetter就是管理你的字體的類攒磨。它作為CTFrame對象的生產(chǎn)工廠泳桦,負責(zé)根據(jù)path生產(chǎn)對應(yīng)的CTFrame。
3.獲取CGPath娩缰,用于創(chuàng)建CTFrame灸撰。
4.根據(jù)CGPath產(chǎn)生對應(yīng)的CFFrame。
5.使用CTFrameDraw(ctFrame, context);進行繪制文本信息拼坎。
CTFrame結(jié)構(gòu):
CTFrame浮毯、CTLine、CTRun三者之間的關(guān)系:
CTFrame: 就好比一篇文章泰鸡,一篇文章會包含多個顯示的行
CTLine: 就是上面所說的文章中的每一行债蓝,而每一行又包含多個塊
CTRun: 就是一行中的很多的塊,而塊是指 一組共享 相同屬性 的字體 的 集合
Code:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
//每一個字形都不做圖形變換
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
//翻轉(zhuǎn)坐標
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
//測試文字
NSMutableAttributedString *astring = [[NSMutableAttributedString alloc] initWithString:@"測試富文本顯示"];
//設(shè)置astring 樣式
[astring addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:18] range:NSMakeRange(0, 2)];
[astring addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(2, 2)];
//繪制文本
//初始化ctFramesetter
CTFramesetterRef ctFramesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)astring); //創(chuàng)建path
CGMutablePathRef path = CGPathCreateMutable();
CGRect bounds = CGRectMake(0.0, 0.0, self.bounds.size.width, self.bounds.size.height);
CGPathAddRect(path, NULL, bounds); //ctFramesetter 根據(jù)path產(chǎn)生ctFrame
CTFrameRef ctFrame = CTFramesetterCreateFrame(ctFramesetter,CFRangeMake(0, 0), path, NULL); //繪制ctFrame
CTFrameDraw(ctFrame, context);
}
4.圖文混排
圖文混排需要Core Text是和Core Graphics配合使用的盛龄,一般是在UIView的drawRect方法中的Graphics Context上進行繪制的饰迹。 且Core Text真正負責(zé)繪制的是文本部分,圖片還是需要自己去手動繪制余舶,所以你必須關(guān)注很多繪制的細節(jié)部分蹦锋。只是Core Text可以通過CTRun的設(shè)置為你的圖片在文本繪制的過程中留出適當(dāng)?shù)目臻g。這個設(shè)置就使用到CTRunDelegate了欧芽,看這個名字大概就可以知道什么意思了莉掂,CTRunDelegate作為CTRun相關(guān)屬性或操作擴展的一個入口,使得我們可以對CTRun做一些自定義的行為千扔。為圖片留位置的方法就是加入一個空白的CTRun憎妙,自定義其ascent,descent曲楚,width等參數(shù)厘唾,使得繪制文本的時候留下空白位置給相應(yīng)的圖片。然后圖片在相應(yīng)的空白位置上使用Core Graphics接口進行繪制龙誊。
使用CTRunDelegateCreate可以創(chuàng)建一個CTRunDelegate抚垃,它接收兩個參數(shù),一個是callbacks結(jié)構(gòu)體,一個是所有callback調(diào)用的時候需要傳入的對象鹤树。 callbacks的結(jié)構(gòu)體為CTRunDelegateCallbacks铣焊,主要是包含一些回調(diào)函數(shù),比如有返回當(dāng)前run的ascent罕伯,descent曲伊,width這些值的回調(diào)函數(shù),至于函數(shù)中如何鑒別當(dāng)前是哪個run追他,可以在CTRunDelegateCreate的第二個參數(shù)來達到目的坟募,因為CTRunDelegateCreate的第二個參數(shù)會作為每一個回調(diào)調(diào)用時的入?yún)ⅰ?/p>
void RunDelegateDeallocCallback( void* refCon )
{
}
CGFloat RunDelegateGetAscentCallback( void *refCon )
{
NSString *imageName = (__bridge NSString *) refCon; return([UIImage imageNamed:imageName].size.height);
}
CGFloat RunDelegateGetDescentCallback( void *refCon )
{
return(0);
}
CGFloat RunDelegateGetWidthCallback( void *refCon )
{
NSString *imageName = (__bridge NSString *) refCon; return([UIImage imageNamed:imageName].size.width);
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
//每一個字形都不做圖形變換
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
//翻轉(zhuǎn)坐標
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
//測試文字
NSMutableAttributedString *astring = [[NSMutableAttributedString alloc] initWithString:@"測試富文本顯示"];
//設(shè)置astring 樣式
[astring addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:18] range:NSMakeRange(0, 2)];
[astring addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(2, 2)];
//圖片名稱
NSString *imgName = @"009@2x.png";
//CTRunDelegateCallbacks用于占位置卿拴,給圖片預(yù)留大小
CTRunDelegateCallbacks imageCallbacks;
imageCallbacks.version = kCTRunDelegateVersion1;
imageCallbacks.dealloc = RunDelegateDeallocCallback;
imageCallbacks.getAscent = RunDelegateGetAscentCallback;
imageCallbacks.getDescent = RunDelegateGetDescentCallback;
imageCallbacks.getWidth = RunDelegateGetWidthCallback;
CTRunDelegateRef ctRunDelegate = CTRunDelegateCreate(&imageCallbacks, (__bridge void * _Nullable)(imgName));
NSMutableAttributedString *imageAttrString = [[NSMutableAttributedString alloc] initWithString:@" "];
[imageAttrString addAttribute:(NSString *)kCTRunDelegateAttributeName value:(__bridge id)ctRunDelegate range:NSMakeRange(0, 1)];
CFRelease(ctRunDelegate);
//把圖片名字與所在的位置綁定类咧,方便后續(xù)繪制取出對應(yīng)圖片
[imageAttrString addAttribute:@"imageName" value:imgName range:NSMakeRange(0, 1)];
//圖片所在文字的位置
[astring insertAttributedString:imageAttrString atIndex:1];
//繪制文本
CTFramesetterRef ctFramesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)astring);
CGMutablePathRef path = CGPathCreateMutable();
CGRect bounds = CGRectMake(0.0, 0.0, self.bounds.size.width, self.bounds.size.height);
CGPathAddRect(path, NULL, bounds);
CTFrameRef ctFrame = CTFramesetterCreateFrame(ctFramesetter, CFRangeMake(0, 0), path, NULL);
CTFrameDraw(ctFrame, context);
//繪制圖片,遍歷找到每一個CTRun 判斷是否有圖片履婉,然后在繪制出CTRun中對應(yīng)的圖片
CFArrayRef lines = CTFrameGetLines(ctFrame);
CGPoint lineOrigins[CFArrayGetCount(lines)];
CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0; i < CFArrayGetCount(lines); i++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CGFloat lineAscent;
CGFloat lineDescent;
CGFloat lineLeading;
//獲取line的繪制矩形
CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading);
CFArrayRef runs = CTLineGetGlyphRuns(line);
for (int j = 0; j < CFArrayGetCount(runs); j++) {
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
CGFloat runAscent;
CGFloat runDescent;
CGPoint lineOrigin = lineOrigins[i];
NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
CGRect runRect;
runRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &runAscent, &runDescent, NULL);
runRect = CGRectMake(lineOrigin.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL), lineOrigin.y, runRect.size.width, runAscent+runDescent);
NSString *imageName = [attributes objectForKey:@"imageName"];
if (imageName) {
UIImage *img = [UIImage imageNamed:imageName];
if (img) {
CGRect imageRect;
imageRect.size = img.size;
imageRect.origin.x = lineOrigin.x + runRect.origin.x;
imageRect.origin.y = lineOrigin.y;
CGContextDrawImage(context, imageRect, img.CGImage);
}
}
}
}
CFRelease(ctFrame);
CFRelease(path);
CFRelease(ctFramesetter);
}
參考資料: