CoreText
原理
為了方便使用,需要先創(chuàng)建一個自定義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)建了一個
CGContextRef上下文。 - 因為CoreText的坐標系是以右下角為原點,所以我們將CoreText坐標系翻轉一下。
- 創(chuàng)建了一個
CGPath,來完成繪制文字的區(qū)域。CoreText支持矩形、環(huán)形。因為用整個View來做顯示區(qū)域,所以我們通過self.bounds來創(chuàng)建CGPath。 - 在CoreText我們要渲染的文字需要用
NSAttributedString來創(chuàng)建,它允許你對文字顏色、字體、大小設置不同的樣式。 -
CTFramesetterRef是CoreText非常重要的一個類,他管理了你得字體和文本渲染塊(CTFrame)。最簡單的創(chuàng)建就是通過NSAttributedString來創(chuàng)建一個CTFramesetterRef。然后通過剛創(chuàng)建的CTFramesetterRef來創(chuàng)建一個CTFrameRef。CTFramesetterCreateFrame的參數(shù)分別是frameSetter、將要顯示文字的Range(這里?長度attString.length)、文本要顯示的區(qū)域(剛剛創(chuàng)建的path)和frameAttributes(可以為空)。 - 通過
CTFrameDraw在給定的context里繪制frame。 - 最后不要忘了清理資源。
CoreText對象模型

Unknown.jpg
在CTFrame內部是由多個CTline組成,每行CTline又是由多個CTRun組成
每個CTRun代表一組風格一致的文本(CTline和CTRun的創(chuàng)建不需要我們管理)
在CTRun中我們可以設置代理來指定繪制此組文本的寬高和排列方式等信息
我們通過NSAttributedString創(chuàng)建一個CTFramesetter,這時候會自動創(chuàng)建一個 CTTypesetter實例,它負責管理字體,下面通過CTFramesetter來創(chuàng)建一個或多個frame來渲染文字。然后Core Text會根據(jù)frame的大小自動創(chuàng)建CTLine(每行對應一個CTLine)和CTRun(相同格式的一個或多個相鄰字符組成一個CTRun)。
舉例來說,Core Text將創(chuàng)建一個CTRun來繪制一些紅色文字,然后創(chuàng)建一個CTRun來繪制純文本,然后再創(chuàng)建一個CTRun來繪制加粗文字等等。要注意,你不需要自己創(chuàng)建CTRun,Core Text將根據(jù)NSAttributedString的屬性來自動創(chuàng)建CTRun。每個CTRun對象對應不同的屬性,正因此,你可以自由的控制字體、顏色、字間距等等信息。
#import "CoreTextData.h"
@interface CoreTextData : NSObject
/** 文本繪制的區(qū)域大小 */
@property (nonatomic, assign) CTFrameRef ctFrame;
/** 文本繪制區(qū)域高度 */
@property (nonatomic, assign) CGFloat height;
/** 文本中存儲圖片信息數(shù)組 */
@property (nonatomic, strong) NSMutableArray *imageArray;
/** 文本中存儲鏈接信息數(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代理設置一個空白的字符給定寬高,最后在利用CGContextDrawImage將其繪制
// 獲取CTFrame中所有的line
NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
NSUInteger lineCount = [lines count];
// 利用CGPoint數(shù)組獲取所有l(wèi)ine的起始坐標
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
// 獲取圖片數(shù)組中第一個圖片信息
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的代理信息,若無代理信息則直接進入下次循環(huán)
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
if (delegate == nil) {
continue;
}
// 若有代理信息,判斷代理信息是否為字典,不是直接進入下次循環(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);
// 起點坐標
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坐標系不同所以要做個對應轉換
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);
//獲取當前行的rect信息
CGRect flippedRect = [self getLineBounds:line point:linePoint];
//將CoreText坐標轉換為UIKit坐標
CGRect rect = CGRectApplyAffineTransform(flippedRect, transform);
//判斷點是否在Rect當中
if (CGRectContainsPoint(rect, point)) {
//獲取點在line行中的位置
CGPoint relativePoint = CGPointMake(point.x - CGRectGetMinX(rect), point.y - CGRectGetMinY(rect));
//獲取點中字符在line中的位置(在屬性文字中是第幾個字)
CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint);
//判斷此字符是否在鏈接屬性文字當中
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行的寬度信息的同時得到其他信息
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