CoreText
CoreText是底層的API,它使用了許多C的函數(shù)(例如CTFramesetterCreateWithAttributedString
,CTFramesetterCreateFrame
)來代替OC的類和方法。
Core Text
是和Core Graphics
配合使用的,一般是在UIView的drawRect
方法中的Graphics Context
上進(jìn)行繪制的。Core Text
真正負(fù)責(zé)繪制的是文本部分,如果要繪制圖片,可以使用CoreText給圖片預(yù)留出位置,然后用Core Graphics
繪制。demo地址
CoreText
布局會(huì)用到attributed strings
(CFAttributedStringRef)和graphics paths
(CGPathRef)。attributed strings
封裝了文本的屬性,例如字體和顏色;graphics paths
定義了文本框的外形。
字形度量
字形度量就是字形的各個(gè)參數(shù):
bounding box(邊界框),這是一個(gè)假想的框子,它盡可能緊密的裝入字形。
baseline(基線),一條假想的線,一行上的字形都以此線作為上下位置的參考,在這條線的左側(cè)存在一個(gè)點(diǎn)叫做基線的原點(diǎn)。
ascent(上行高度),從原點(diǎn)到字體中最高(這里的高深都是以基線為參照線的)的字形的頂部的距離,ascent是一個(gè)正值。
descent(下行高度),從原點(diǎn)到字體中最深的字形底部的距離,descent是一個(gè)負(fù)值(比如一個(gè)字體原點(diǎn)到最深的字形的底部的距離為2,那么descent就為-2)。
linegap(行距),linegap也可以稱作leading(其實(shí)準(zhǔn)確點(diǎn)講應(yīng)該叫做External leading)。
leading,文檔說的很含糊,其實(shí)是上一行字符的descent到- 下一行的ascent之間的距離。
所以字體的高度是由三部分組成的:leading + ascent + descent。
字形和字符,一些Metrics專業(yè)知識還可以參考Free Type的文檔 Glyph metrics,其實(shí)iOS就是使用Free Type庫來進(jìn)行字體渲染的。蘋果文檔 Querying Font Metrics ,Text Layout。
CoreText對象模型
運(yùn)行時(shí)的Core Text對象形成一個(gè)層次結(jié)構(gòu),如圖1所示。 這個(gè)層次結(jié)構(gòu)的頂部是framesetter
對象(CTFramesetterRef
)。 使用attributed string
和graphics path
作為輸入,框架設(shè)置器會(huì)生成一個(gè)或多個(gè)文本框(CTFrameRef
)。 每個(gè)CTFrame
對象都代表一個(gè)段落。
從圖中可以看到,我們首先通過CFAttributeString來創(chuàng)建CTFramaeSetter,然后再通過CTFrameSetter來創(chuàng)建CTFrame。
在CTFrame內(nèi)部,是由多個(gè)CTLine來組成的,每個(gè)CTLine代表一行,每個(gè)CTLine是由多個(gè)CTRun來組成,每個(gè)CTRun代表一組顯示風(fēng)格一致的文本。
- CTFrameSetter: CTFrameSetter是通過CFAttributeString進(jìn)行初始化它負(fù)責(zé)根據(jù)path生產(chǎn)對應(yīng)的CTFrame;
- CTFrame:CTFrame可以想象成畫布, 畫布的大小范圍由CGPath決定。CTFrame由很多CTLine組成。 CTFrame可以通過CTFrameDraw函數(shù)直接繪制到context上,我們可以在繪制之前,操作CTFrame中的CTline,進(jìn)行一些參數(shù)的微調(diào);
- CTLine: CTLine可以看做Core Text繪制中的一行的對象,通過它可以獲得當(dāng)前行的line ascent, line descent, line heading,還可以獲得CTLine下的所有CTRun;
- CTRun: CTRun是一組共享相同attributes的集合體;
要繪制圖片,需要用CoreText的CTRun為圖片在繪制過程中留出空間。這個(gè)設(shè)置要用到CTRunDelegate。我們可以在要顯示圖片的地方,用一個(gè)特殊的空白字符代替,用CTRunDelegate為其設(shè)置ascent,descent,width等參數(shù),這樣在繪制文本的時(shí)候就會(huì)把圖片的位置留出來,用CGContextDrawImage方法直接繪制出來就行了。
創(chuàng)建CTRunDelegate:
CTRunDelegateRef __nullable CTRunDelegateCreate(
const CTRunDelegateCallbacks* callbacks,
void * __nullable refCon )
創(chuàng)建CTRunDelegate需要兩個(gè)參數(shù),一個(gè)是callbacks結(jié)構(gòu)體,還有一個(gè)是callbacks里的函數(shù)調(diào)用時(shí)需要傳入的參數(shù)。
typedef struct
{
CFIndex version;
CTRunDelegateDeallocateCallback dealloc;
CTRunDelegateGetAscentCallback getAscent;
CTRunDelegateGetDescentCallback getDescent;
CTRunDelegateGetWidthCallback getWidth;
} CTRunDelegateCallbacks;
callbacks是一個(gè)結(jié)構(gòu)體,主要包含了返回當(dāng)前CTRun的ascent,descent和width函數(shù)。
代碼
自定義一個(gè)繼承自UIView的子類CoreTextView;在.m文件里引入頭文件CoreText/CoreText.h重寫drawRect方法:
void RunDelegateDeallocCallback( void* refCon ){
}
CGFloat RunDelegateGetAscentCallback( void *refCon ){
NSString *imageName = (__bridge NSString *)refCon;
CGFloat height = [UIImage imageNamed:imageName].size.height;
return height;
}
CGFloat RunDelegateGetDescentCallback(void *refCon){
return 0;
}
CGFloat RunDelegateGetWidthCallback(void *refCon){
NSString *imageName = (__bridge NSString *)refCon;
CGFloat width = [UIImage imageNamed:imageName].size.width;
return width;
}
- (void)drawRect:(CGRect)rect{
[super drawRect:rect];
//得到當(dāng)前繪制畫布的上下文,用于將后續(xù)內(nèi)容繪制在畫布上
CGContextRef context = UIGraphicsGetCurrentContext();
//將坐標(biāo)系上下翻轉(zhuǎn)。對于底層的繪制引擎來說,屏幕的左下角是坐標(biāo)原點(diǎn)(0,0),而對于上層的UIKit來說,屏幕的左上角是坐標(biāo)原點(diǎn),為了之后的坐標(biāo)系按UIKit來做,在這里做了坐標(biāo)系的上下翻轉(zhuǎn),這樣底層和上層的(0,0)坐標(biāo)就是重合的了
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0,-1.0);
//創(chuàng)建繪制的區(qū)域,這里將UIView的bounds作為繪制區(qū)域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);
NSMutableAttributedString * attString = [[NSMutableAttributedString alloc] initWithString:@"海洋生物學(xué)家在太平洋里發(fā)現(xiàn)了一條與眾不同的鯨。一般藍(lán)鯨的“歌唱”頻率在十五到二十五赫茲,長須鯨子啊二十赫茲左右,而它的頻率在五十二赫茲左右。"];
//設(shè)置字體
[attString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:24] range:NSMakeRange(0, 5)];
[attString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:13] range:NSMakeRange(6, 2)];
[attString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:38] range:NSMakeRange(8, attString.length - 8)];
//設(shè)置文字顏色
[attString addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 11)];
[attString addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:NSMakeRange(11, attString.length - 11)];
NSString * imageName = @"jingyu";
CTRunDelegateCallbacks callbacks;
callbacks.version = kCTRunDelegateVersion1;
callbacks.dealloc = RunDelegateDeallocCallback;
callbacks.getAscent = RunDelegateGetAscentCallback;
callbacks.getDescent = RunDelegateGetDescentCallback;
callbacks.getWidth = RunDelegateGetWidthCallback;
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callbacks, (__bridge void * _Nullable)(imageName));
//空格用于給圖片留位置
NSMutableAttributedString *imageAttributedString = [[NSMutableAttributedString alloc] initWithString:@" "];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imageAttributedString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
CFRelease(runDelegate);
[imageAttributedString addAttribute:@"imageName" value:imageName range:NSMakeRange(0, 1)];
[attString insertAttributedString:imageAttributedString atIndex:1];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attString.length), path, NULL);
//把frame繪制到context里
CTFrameDraw(frame, context);
NSArray * lines = (NSArray *)CTFrameGetLines(frame);
NSInteger lineCount = lines.count;
CGPoint lineOrigins[lineCount];
//拷貝frame的line的原點(diǎn)到數(shù)組lineOrigins里,如果第二個(gè)參數(shù)里的length是0,將會(huì)從開始的下標(biāo)拷貝到最后一個(gè)line的原點(diǎn)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0; i < lineCount; i++) {
CTLineRef line = (__bridge CTLineRef)lines[I];
NSArray * runs = (__bridge NSArray *)CTLineGetGlyphRuns(line);
for (int j = 0; j < runs.count; j++) {
CTRunRef run = (__bridge CTRunRef)runs[j];
NSDictionary * dic = (NSDictionary *)CTRunGetAttributes(run);
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[dic objectForKey:(NSString *)kCTRunDelegateAttributeName];
if (delegate == nil) {
continue;
}
NSString * imageName = [dic objectForKey:@"imageName"];
UIImage * image = [UIImage imageNamed:imageName];
CGRect runBounds;
CGFloat ascent;
CGFloat descent;
runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
runBounds.size.height = ascent + descent;
CFIndex index = CTRunGetStringRange(run).location;
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, index, NULL);
runBounds.origin.x = lineOrigins[i].x + xOffset;
runBounds.origin.y = lineOrigins[i].y;
runBounds.size =image.size;
CGContextDrawImage(context, runBounds, image.CGImage);
}
}
//底層的Core Foundation對象由于不在ARC的管理下,需要自己維護(hù)這些對象的引用計(jì)數(shù),最后要釋放掉。
CFRelease(frame);
CFRelease(path);
}
運(yùn)行后效果如下:
異步繪制
上面的drawRect方法是在主線程里調(diào)用的,如果繪制的過程比較耗時(shí),可能會(huì)阻塞主線程,這時(shí)候可以將會(huì)值得過程發(fā)到子線程里進(jìn)行,繪制完成后將context轉(zhuǎn)成位圖,然后再把位圖在主線程里設(shè)置到view的layer里。
- (void)drawRect:(CGRect)rect{
[super drawRect:rect];
//將繪制過程放入到后臺(tái)線程中
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
UIGraphicsBeginImageContext(rect.size);
//得到當(dāng)前繪制畫布的上下文,用于將后續(xù)內(nèi)容繪制在畫布上
CGContextRef context = UIGraphicsGetCurrentContext();
//將坐標(biāo)系上下翻轉(zhuǎn)。對于底層的繪制引擎來說,屏幕的左下角是坐標(biāo)原點(diǎn)(0,0),而對于上層的UIKit來說,屏幕的左上角是坐標(biāo)原點(diǎn),為了之后的坐標(biāo)系按UIKit來做,在這里做了坐標(biāo)系的上下翻轉(zhuǎn),這樣底層和上層的(0,0)坐標(biāo)就是重合的了
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, rect.size.height);
CGContextScaleCTM(context, 1.0,-1.0);
//創(chuàng)建繪制的區(qū)域,這里將UIView的bounds作為繪制區(qū)域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, rect);
NSMutableAttributedString * attString = [[NSMutableAttributedString alloc] initWithString:@"海洋生物學(xué)家在太平洋里發(fā)現(xiàn)了一條與眾不同的鯨。一般藍(lán)鯨的“歌唱”頻率在十五到二十五赫茲,長須鯨子啊二十赫茲左右,而它的頻率在五十二赫茲左右。"];
//設(shè)置字體
[attString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:24] range:NSMakeRange(0, 5)];
[attString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:13] range:NSMakeRange(6, 2)];
[attString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:38] range:NSMakeRange(8, attString.length - 8)];
//設(shè)置文字顏色
[attString addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 11)];
[attString addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:NSMakeRange(11, attString.length - 11)];
NSString * imageName = @"jingyu";
CTRunDelegateCallbacks callbacks;
callbacks.version = kCTRunDelegateVersion1;
callbacks.dealloc = RunDelegateDeallocCallback;
callbacks.getAscent = RunDelegateGetAscentCallback;
callbacks.getDescent = RunDelegateGetDescentCallback;
callbacks.getWidth = RunDelegateGetWidthCallback;
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callbacks, (__bridge void * _Nullable)(imageName));
//空格用于給圖片留位置
NSMutableAttributedString *imageAttributedString = [[NSMutableAttributedString alloc] initWithString:@" "];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)imageAttributedString, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate);
CFRelease(runDelegate);
[imageAttributedString addAttribute:@"imageName" value:imageName range:NSMakeRange(0, 1)];
[attString insertAttributedString:imageAttributedString atIndex:1];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attString.length), path, NULL);
//把frame繪制到context里
CTFrameDraw(frame, context);
NSArray * lines = (NSArray *)CTFrameGetLines(frame);
NSInteger lineCount = lines.count;
CGPoint lineOrigins[lineCount];
//拷貝frame的line的原點(diǎn)到數(shù)組lineOrigins里,如果第二個(gè)參數(shù)里的length是0,將會(huì)從開始的下標(biāo)拷貝到最后一個(gè)line的原點(diǎn)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
for (int i = 0; i < lineCount; i++) {
CTLineRef line = (__bridge CTLineRef)lines[I];
NSArray * runs = (__bridge NSArray *)CTLineGetGlyphRuns(line);
for (int j = 0; j < runs.count; j++) {
CTRunRef run = (__bridge CTRunRef)runs[j];
NSDictionary * dic = (NSDictionary *)CTRunGetAttributes(run);
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[dic objectForKey:(NSString *)kCTRunDelegateAttributeName];
if (delegate == nil) {
continue;
}
NSString * imageName = [dic objectForKey:@"imageName"];
UIImage * image = [UIImage imageNamed:imageName];
CGRect runBounds;
CGFloat ascent;
CGFloat descent;
runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
runBounds.size.height = ascent + descent;
CFIndex index = CTRunGetStringRange(run).location;
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, index, NULL);
runBounds.origin.x = lineOrigins[i].x + xOffset;
runBounds.origin.y = lineOrigins[i].y;
runBounds.size =image.size;
CGContextDrawImage(context, runBounds, image.CGImage);
}
}
CGImageRef imageRef = CGBitmapContextCreateImage(context);
UIImage *image;
if (imageRef) {
image = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
}
UIGraphicsEndImageContext();
//在主線程中更新
dispatch_async(dispatch_get_main_queue(), ^{
self.layer.contents = (__bridge id _Nullable)(image.CGImage);
});
});
}
NSAttributedString
看yykit的demo里,微博的頁面的富文本使用了NSAttributedString,這里記錄下學(xué)習(xí)筆記。
這里要把"我家這個(gè)好忠犬啊~[喵喵] http://t.cn/Ry4UXdF //@我是呆毛芳子蜀黍w:這是什么鬼?[喵喵] //@清新可口喵醬圓臉星人是扭蛋狂魔:窩家這個(gè)超委婉的拒絕了窩"在手機(jī)上顯示成;
![Uploading 屏幕快照 2017-08-17 上午11.11.20_514440.png . . .]
@用戶名用到了正則匹配,可以得到一個(gè)nsrange的數(shù)組,是@用戶名的nsrange;
表情也是用到了正則匹配,得到每個(gè)表情的nsrange,從本地尋找表情對應(yīng)的圖片,然后用到了NSTextAttachment來生成NSAttributedString,然后把表情進(jìn)行了替換。
http://t.cn/Ry4UXdF這個(gè)鏈接被替換成了圖片和文字,圖片是從網(wǎng)絡(luò)上下載的??梢韵扰袛啾镜厥欠裼袌D片的緩存,如果沒有,先用占位圖生成NSTextAttachment,先顯示占位圖,等圖片下載完以后就重新替換掉圖片。
demo