CoreText
原理
為了方便使用闯袒,需要先創(chuàng)建一個(gè)自定義UIView惹挟,我們將在
drawRect
函數(shù)里使用CoreText搪花。
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddEllipseInRect(path, NULL, self.bounds);
NSAttributedString *attString = [[NSAttributedString alloc] initWithString:@"Hello World"];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attString.length), path, NULL);
CTFrameDraw(frame, context);
CFRelease(frame);
CGPathRelease(path);
CGContextRelease(context);
}
- 我們首先創(chuàng)建了一個(gè)
CGContextRef
上下文搓谆。 - 因?yàn)镃oreText的坐標(biāo)系是以右下角為原點(diǎn)刁愿,所以我們將CoreText坐標(biāo)系翻轉(zhuǎn)一下绰寞。
- 創(chuàng)建了一個(gè)
CGPath
,來(lái)完成繪制文字的區(qū)域铣口。CoreText支持矩形滤钱、環(huán)形。因?yàn)橛谜麄€(gè)View來(lái)做顯示區(qū)域脑题,所以我們通過(guò)self.bounds
來(lái)創(chuàng)建CGPath
件缸。 - 在CoreText我們要渲染的文字需要用
NSAttributedString
來(lái)創(chuàng)建,它允許你對(duì)文字顏色旭蠕、字體停团、大小設(shè)置不同的樣式。 -
CTFramesetterRef
是CoreText非常重要的一個(gè)類掏熬,他管理了你得字體和文本渲染塊(CTFrame)
佑稠。最簡(jiǎn)單的創(chuàng)建就是通過(guò)NSAttributedString
來(lái)創(chuàng)建一個(gè)CTFramesetterRef
。然后通過(guò)剛創(chuàng)建的CTFramesetterRef
來(lái)創(chuàng)建一個(gè)CTFrameRef
旗芬。CTFramesetterCreateFrame
的參數(shù)分別是frameSetter
舌胶、將要顯示文字的Range
(這里?長(zhǎng)度attString.length)、文本要顯示的區(qū)域(剛剛創(chuàng)建的path
)和frameAttributes
(可以為空)疮丛。 - 通過(guò)
CTFrameDraw
在給定的context
里繪制frame
幔嫂。 - 最后不要忘了清理資源。
CoreText對(duì)象模型
在CTFrame內(nèi)部是由多個(gè)CTline組成誊薄,每行CTline又是由多個(gè)CTRun組成
每個(gè)CTRun代表一組風(fēng)格一致的文本(CTline和CTRun的創(chuàng)建不需要我們管理)
在CTRun中我們可以設(shè)置代理來(lái)指定繪制此組文本的寬高和排列方式等信息
我們通過(guò)NSAttributedString
創(chuàng)建一個(gè)CTFramesetter
履恩,這時(shí)候會(huì)自動(dòng)創(chuàng)建一個(gè) CTTypesetter
實(shí)例,它負(fù)責(zé)管理字體,下面通過(guò)CTFramesetter
來(lái)創(chuàng)建一個(gè)或多個(gè)frame來(lái)渲染文字呢蔫。然后Core Text會(huì)根據(jù)frame的大小自動(dòng)創(chuàng)建CTLine
(每行對(duì)應(yīng)一個(gè)CTLine)和CTRun
(相同格式的一個(gè)或多個(gè)相鄰字符組成一個(gè)CTRun)切心。
舉例來(lái)說(shuō),Core Text將創(chuàng)建一個(gè)CTRun來(lái)繪制一些紅色文字片吊,然后創(chuàng)建一個(gè)CTRun
來(lái)繪制純文本绽昏,然后再創(chuàng)建一個(gè)CTRun
來(lái)繪制加粗文字等等。要注意俏脊,你不需要自己創(chuàng)建CTRun
全谤,Core Text將根據(jù)NSAttributedString
的屬性來(lái)自動(dòng)創(chuàng)建CTRun
。每個(gè)CTRun
對(duì)象對(duì)應(yīng)不同的屬性爷贫,正因此认然,你可以自由的控制字體补憾、顏色、字間距等等信息卷员。
#import "CoreTextData.h"
@interface CoreTextData : NSObject
/** 文本繪制的區(qū)域大小 */
@property (nonatomic, assign) CTFrameRef ctFrame;
/** 文本繪制區(qū)域高度 */
@property (nonatomic, assign) CGFloat height;
/** 文本中存儲(chǔ)圖片信息數(shù)組 */
@property (nonatomic, strong) NSMutableArray *imageArray;
/** 文本中存儲(chǔ)鏈接信息數(shù)組 */
@property (nonatomic, strong) NSMutableArray *linkArray;
@end
@implementation CoreTextData
- (void)setCtFrame:(CTFrameRef)ctFrame {
if (_ctFrame != ctFrame) {
if (_ctFrame != nil) {
CFRelease(_ctFrame);
}
CFRetain(ctFrame);
_ctFrame = ctFrame;
}
}
- (void)dealloc {
if (_ctFrame != nil) {
CFRelease(_ctFrame);
_ctFrame = nil;
}
}
- (void)setImageArray:(NSArray *)imageArray {
_imageArray = imageArray;
[self fillImagePosition];
}
- (void)fillImagePosition {
if (self.imageArray.count == 0) {
return;
}
// 此處利用CTRun代理設(shè)置一個(gè)空白的字符給定寬高余蟹,最后在利用CGContextDrawImage將其繪制
// 獲取CTFrame中所有的line
NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
NSUInteger lineCount = [lines count];
// 利用CGPoint數(shù)組獲取所有l(wèi)ine的起始坐標(biāo)
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
// 獲取圖片數(shù)組中第一個(gè)圖片信息
int imgIndex = 0;
CoreTextImageData * imageData = self.imageArray[0];
for (int i = 0; i < lineCount; ++i) {
// 如果不存在圖片則返回
if (imageData == nil) {
break;
}
// 存在圖片信息則獲取圖片具體位置信息
// 獲取每行信息
CTLineRef line = (__bridge CTLineRef)lines[i];
// 得到每行的CTRun信息,并遍歷
NSArray * runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
for (id runObj in runObjArray) {
CTRunRef run = (__bridge CTRunRef)runObj;
NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
// 獲取CTRun的代理信息子刮,若無(wú)代理信息則直接進(jìn)入下次循環(huán)
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
if (delegate == nil) {
continue;
}
// 若有代理信息威酒,判斷代理信息是否為字典,不是直接進(jìn)入下次循環(huán)
NSDictionary * metaDic = CTRunDelegateGetRefCon(delegate);
if (![metaDic isKindOfClass:[NSDictionary class]]) {
continue;
}
CGRect runBounds;
CGFloat ascent;
CGFloat descent;
// 找到CTRunDelegate中的寬度并給上升和下降高度賦值
runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
runBounds.size.height = ascent + descent;
// 獲取CTRun在x上的偏移量
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
// 起點(diǎn)坐標(biāo)
runBounds.origin.x = lineOrigins[i].x + xOffset;
runBounds.origin.y = lineOrigins[i].y;
runBounds.origin.y -= descent;
// 獲取路徑挺峡,并利用路徑獲取繪制視圖的Rect
CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
CGRect colRect = CGPathGetBoundingBox(pathRef);
CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
// 保存圖片位置信息
imageData.imagePosition = delegateBounds;
imgIndex++;
if (imgIndex == self.imageArray.count) {
imageData = nil;
break;
} else {
imageData = self.imageArray[imgIndex];
}
}
}
}
@end
#import "CoreTextImageData.h"
@interface CoreTextImageData : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) CGRect imagePosition;
@end
#import "CoreTextLinkData.h"
@implementation CoreTextLinkData
+ (CoreTextLinkData *)touchLinkInView:(UIView *)view atPoint:(CGPoint)point data:(CoreTextData *)data {
CTFrameRef ctFrame = data.ctFrame;
CFArrayRef lines = CTFrameGetLines(ctFrame);
if (lines == nil) {
return nil;
}
CFIndex linesCount = CFArrayGetCount(lines);
CoreTextLinkData *linkdata = nil;
CGPoint linesOrigins[linesCount];
CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), linesOrigins);
//由于CoreText和UIKit坐標(biāo)系不同所以要做個(gè)對(duì)應(yīng)轉(zhuǎn)換
CGAffineTransform transform = CGAffineTransformMakeTranslation(0, view.bounds.size.height);
transform = CGAffineTransformScale(transform, 1, -1);
for (int i = 0; i < linesCount; i ++) {
CGPoint linePoint = linesOrigins[i];
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
//獲取當(dāng)前行的rect信息
CGRect flippedRect = [self getLineBounds:line point:linePoint];
//將CoreText坐標(biāo)轉(zhuǎn)換為UIKit坐標(biāo)
CGRect rect = CGRectApplyAffineTransform(flippedRect, transform);
//判斷點(diǎn)是否在Rect當(dāng)中
if (CGRectContainsPoint(rect, point)) {
//獲取點(diǎn)在line行中的位置
CGPoint relativePoint = CGPointMake(point.x - CGRectGetMinX(rect), point.y - CGRectGetMinY(rect));
//獲取點(diǎn)中字符在line中的位置(在屬性文字中是第幾個(gè)字)
CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint);
//判斷此字符是否在鏈接屬性文字當(dāng)中
linkdata = [self linkAtIndex:idx linkArray:data.linkArray];
break;
}
}
return linkdata;
}
+ (CGRect)getLineBounds:(CTLineRef)line point:(CGPoint)point {
//配置line行的位置信息
CGFloat ascent = 0;
CGFloat descent = 0;
CGFloat leading = 0;
//在獲取line行的寬度信息的同時(shí)得到其他信息
CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat height = ascent + descent;
return CGRectMake(point.x, point.y, width, height);
}
+ (CoreTextLinkData *)linkAtIndex:(CFIndex)i linkArray:(NSArray *)linkArray {
CoreTextLinkData *linkdata = nil;
for (CoreTextLinkData *data in linkArray) {
if (NSLocationInRange(i, data.range)) {
linkdata = data;
break;
}
}
return linkdata;
}
@end